Signature Verification
Verify Talonic webhook signatures with HMAC-SHA256 over the raw request body. Step-by-step guide with Python, Node.js, and openssl examples plus test vectors.
Webhook signature verification proves that an incoming webhook was sent by Talonic and not by an attacker who discovered your endpoint URL. Talonic signs every webhook delivery with HMAC-SHA256: it computes a keyed hash of the raw JSON request body using your webhook signing secret and sends the hex digest in the X-Talonic-Signature header as sha256=<hex>. Your server recomputes the same hash and compares the two values in constant time.
Webhooks are delivered as POST requests with a JSON body. Each delivery includes these headers:
X-Talonic-Event— The event type (e.g. extraction.complete).X-Talonic-Signature— HMAC-SHA256 signature of the raw request body, formattedsha256=<hex>. Sent only when a signing secret is configured on the webhook.X-Talonic-Delivery— Unique delivery ID (e.g.dlv_a1b2c3d4e5f67890) for idempotency. Stable across retries of the same delivery.Content-Type— Alwaysapplication/json.User-Agent— AlwaysTalonic-Webhooks/1.0.
Webhook payload example
{
"event": "extraction.complete",
"delivery_id": "dlv_a1b2c3d4e5f67890",
"timestamp": "2026-01-15T10:00:00.000Z",
"data": {
"document_id": "3d44a4dc-e3e4-4bca-b079-a9c85bf75026",
"extraction_id": "3d44a4dc-e3e4-4bca-b079-a9c85bf75026",
"filename": "invoice.pdf",
"field_count": 12,
"confidence_overall": 0.92
}
}Verification steps
- Read the raw request body as bytes or a UTF-8 string. Do not parse and re-serialize the JSON first: key reordering or whitespace changes will break the signature.
- Compute HMAC-SHA256 of the raw body using your webhook signing secret as the key.
- Hex-encode the digest.
- Compare it with the value after
sha256=in theX-Talonic-Signatureheader. - Use a constant-time comparison (
hmac.compare_digest,crypto.timingSafeEqual) to prevent timing attacks.
Python
Node.js (with Express raw-body capture)
express.json() parses the body before your handler runs, and JSON.stringify(req.body) rarely reproduces the original byte sequence. Capture the raw body (express.raw(), Flask request.get_data(), Django request.body) and hash that.Test vectors
Use these values to verify your signature implementation is correct before pointing Talonic at it:
Test inputs
Secret: whsec_test_secret_do_not_use_in_production
Payload: {"event":"extraction.complete","delivery_id":"dlv_a1b2c3d4e5f67890","timestamp":"2026-01-15T10:00:00.000Z","data":{"document_id":"3d44a4dc-e3e4-4bca-b079-a9c85bf75026","extraction_id":"3d44a4dc-e3e4-4bca-b079-a9c85bf75026","filename":"invoice.pdf","field_count":12,"confidence_overall":0.92}}
Expected signature: sha256=9832313c4adcbe269b8b7c11c1855f6d437f2089a15aa22ebd9f21d356d062edVerify with the command line
secret field when you [create the webhook configuration](create-webhook-config).