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
- 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. - Compare in constant time. Use
timingSafeEqual/hmac.compare_digest, never===on hex strings. - Dedupe on
event_id. Deliveries are at-least-once (retries can double-deliver after a timeout you actually served). Track processedevent_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
- Any 2xx within 10 seconds acknowledges the delivery.
- Anything else (or a timeout) schedules a retry: 1m / 5m / 30m / 2h / 6h, then dead-letter.
- Do heavy work after responding (queue it) — a slow handler causes duplicate deliveries via timeout retries.
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.