Docs / Verifying webhooks

Verifying webhook signatures

Every delivery is signed. Verify before you trust — it takes four lines.

What we send

POST /your-endpoint HTTP/1.1
Content-Type: application/json
X-Signature: 92d33ce5997859ec537a804240c682c1b87c92254fc7325e99a27d6cd841dd7f
X-Event-Id: cmshauqa300emlh0yp9cygl9q

{"api_version":"2026-08","event_id":"cmshauqa300emlh0yp9cygl9q", ...}

X-Signature = hex( HMAC-SHA256( secret, rawBody ) ), where secret is the whsec_… value returned once when the watch was created.

The three rules

  1. Verify the RAW body. Compute the HMAC over the exact bytes received — before any JSON parsing. Parsing and re-serializing changes key order and whitespace and will break the signature. In Express use express.raw(); in Next.js disable the body parser for this route; in Fastify use the raw-body plugin.
  2. Compare in constant time. Use timingSafeEqual / hmac.compare_digest, never === on hex strings.
  3. Dedupe on event_id. Deliveries are at-least-once (retries can double-deliver after a timeout you actually served). Track processed event_ids and skip repeats.

Node (SDK)

import { constructEvent } from '@tronhooks/sdk'; // verifies + parses, throws on mismatch

app.post('/tron-hook', express.raw({ type: 'application/json' }), (req, res) => {
  const event = constructEvent(req.body, req.header('x-signature'), process.env.WHSEC);
  if (alreadyProcessed(event.event_id)) return res.sendStatus(200); // idempotency
  // ... credit the deposit ...
  res.sendStatus(200);
});

Node (no SDK)

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, signatureHex, secret) {
  const expected = createHmac('sha256', secret).update(rawBody).digest();
  const given = Buffer.from(signatureHex ?? '', 'hex');
  return expected.length === given.length && timingSafeEqual(expected, given);
}

Python

import hmac, hashlib

def verify(raw_body: bytes, signature_hex: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_hex or "")

Responding

Secret hygiene: the whsec_ secret is shown exactly once and stored encrypted on our side. If you lose it, delete the watch and create a new one. Treat it like a password.

Missed deliveries while your endpoint was down? GET /v1/events has the full history for reconciliation.

API reference →