Skip to main content

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, formatted sha256=<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 — Always application/json.
  • User-Agent — Always Talonic-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

  1. 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.
  2. Compute HMAC-SHA256 of the raw body using your webhook signing secret as the key.
  3. Hex-encode the digest.
  4. Compare it with the value after sha256= in the X-Talonic-Signature header.
  5. Use a constant-time comparison (hmac.compare_digest, crypto.timingSafeEqual) to prevent timing attacks.

Python

Node.js (with Express raw-body capture)

The most common verification failure is hashing a re-serialized body instead of the raw bytes. Middleware like 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=9832313c4adcbe269b8b7c11c1855f6d437f2089a15aa22ebd9f21d356d062ed

Verify with the command line

The test secret above is for development only. Never use it in production. Set your real signing secret via the secret field when you [create the webhook configuration](create-webhook-config).