Skip to main content

Why you must verify

Your webhook URL is a public HTTPS endpoint. Anyone who learns it can send it a well-formed JSON body that looks exactly like a CaseXchange event. The X-CaseXchange-Signature header is the only thing that proves a delivery came from CaseXchange and arrived unaltered, so verify it on every request — including webhook.test deliveries — before you read a single field of the body. The signing secret is per subscription. It is returned exactly once, by the call that generates it: the response to POST /webhooks, and thereafter the response to each POST /webhooks/{id}/rotate-secret — which returns a new secret, never the old one. It can never be read back, so store it the moment you receive it. See Managing subscriptions for the create flow.

The signature header

Every delivery carries one signature header:
Only one v1 value is ever sent per delivery — there is no dual signing. The signed message is the timestamp, a literal ., and the body, in that order, with nothing else in between. The same header format is used on every delivery, including the synthetic webhook.test body and the retries of a failed delivery. A retried delivery is signed afresh on every attempt, so t and v1 change between attempts while X-CaseXchange-Delivery stays the same.

Verification steps

1

Read the raw body bytes

Capture the request body exactly as it arrived, before any JSON middleware touches it. In Express use express.raw({ type: 'application/json' }) on the webhook route; in Django read request.body. If your framework has already turned the body into an object, you no longer have the bytes that were signed.
2

Parse t and v1 from the header

Split X-CaseXchange-Signature on ,, then each part on the first =. Look the header up case-insensitively — HTTP header names are not case-sensitive, and most frameworks normalise them to lower case.
3

Reject a stale timestamp

Reject the delivery if t is not a finite number or is more than about 300 seconds away from your current clock. This blunts replay of a captured delivery.
4

Compute the expected signature

Compute HMAC-SHA256 over the string "<t>.<rawBody>" using the secret as the key, and hex-encode the result.
5

Compare in constant time

Compare your value with v1 using a constant-time function — crypto.timingSafeEqual in Node, hmac.compare_digest in Python. Check the lengths first where the function requires it. Never compare with ==.
6

Only then parse the JSON

Once the signature matches, parse the body, deduplicate on X-CaseXchange-Delivery, store it, and return 2xx. Do the real work asynchronously.
If verification fails, do not process the body. Return a non-2xx status so the delivery is recorded as failed and retried — see Reliability for the retry schedule. A silently accepted forged request looks identical to a real one in your system.

Reference implementations

Node.js

This is the reference verifier. rawBody must be the exact bytes CaseXchange sent — a Buffer or the undecoded body string — and the function must run before any JSON.parse.

Express wiring

Mount express.raw on the webhook route so req.body is a Buffer, not a parsed object. Keep express.json() for the rest of your app, but make sure it does not run first on this path.

Python

The same rules in Python. raw_body must be bytes: request.body in Django, request.get_data() in Flask.
Call verify before json.loads. Once it returns True, parse the body, deduplicate on the X-CaseXchange-Delivery header, persist, and respond 2xx.

Computing a signature by hand

To check your implementation against a known input, compute v1 from the shell. Use printf rather than echo so no trailing newline is added to the signed message.
The output is the v1 value for that timestamp and body. Any byte difference in BODY — a space, a reordered key, a different number format — produces a different digest, which is why you must sign the raw bytes and never a re-serialised copy.
Echo mode gives you real inputs to test against. Each capture from GET /webhooks/{id}/echo/captures includes rawBody (the exact signed bytes), the signature header under headers, and signatureDetails.timestamp. Feed those into the one-liner above or into your verifier and confirm you get a match. See Testing webhooks.

Common mistakes

Rotating the secret

POST /webhooks/{id}/rotate-secret generates a new secret for the subscription and returns it. It requires a standard or full key.
The 200 response is the subscription object with its new secret — the only time this secret is ever returned:

What rotation does

Rotation takes effect immediately. There is no grace window and no dual signing: from the moment the call returns, every outbound delivery — including retries of deliveries that were created before the rotation — is signed with the new secret alone. The previous secret never signs again.
Because there is no overlap on the CaseXchange side, the overlap has to live in your verifier.
1

Deploy a verifier that accepts both secrets

Change your endpoint so it tries the current secret first and, on failure, a second “incoming” secret slot. Deploy this before you rotate, while the second slot is still empty — the point is that the handler can hold two secrets by the time the new one exists.
2

Rotate and store the new secret

Call POST /webhooks/{id}/rotate-secret, then write the returned secret into the second slot in your secrets manager. Deliveries signed with the new secret now verify on the second attempt; any in-flight retries also carry the new signature.
3

Confirm deliveries verify with the new secret

Watch GET /webhooks/{id}/deliveries — or trigger POST /webhooks/{id}/test — and confirm new deliveries land as DELIVERED. Check your own logs that they matched on the new secret, not the old one.
4

Drop the old secret

Once you are satisfied, remove the old value and promote the new one to the primary slot. A few minutes is enough; the old secret never signs another delivery after the rotation.

If you forget to update your verifier

Your endpoint keeps computing signatures with the old secret, so every delivery fails verification. Provided your handler returns a non-2xx status on a bad signature, this is recoverable:
  1. Each failed delivery is retried after 30 s, 2 min, 10 min and 30 min — about 43 minutes end to end.
  2. Every retry is re-signed with the current secret. As soon as your verifier is updated, the next attempt succeeds.
  3. After the fifth failed attempt a delivery is EXHAUSTED and is never retried. The event is lost as a push; reconcile it from GET /status-updates or the case lists.
  4. After 10 consecutive exhausted deliveries the subscription is deactivated (isActive: false). Update your secret, then re-enable it with PATCH /webhooks/{id} and { "isActive": true }.
See Reliability for the full retry and deactivation behaviour.

Echo captures and the 24-hour read-back window

Echo-mode captures report a signatureValid flag. For 24 hours after a rotation, that read-back check also accepts the previous secret, so a capture whose delivery was signed just before you rotated still reads as signatureValid: true rather than looking like a broken signature.
The 24 hours applies only to the capture read-back check. Outbound deliveries — to your URL or to the echo endpoint — are always signed with the current secret alone, from the moment the rotation call returns. Do not read the 24 hours as a grace window for your endpoint; there is none.
The practical consequence: after a rotation, a capture that reads as valid does not prove your endpoint would have accepted the delivery. To check your own verifier, take the capture’s rawBody and signature header and run them through your code with the secret your endpoint currently holds. See Testing webhooks for how to enable echo mode and read captures.

Handling the secret

  • Store it in a secrets manager, the same way you store the API key. Never commit it to source control or bake it into an image.
  • Never log it. Scrub it from error reports, request dumps and crash traces the same way you scrub the API key.
  • Keep one secret per subscription. If you run several subscriptions, key your secret store by subscription id.
  • Rotate on any suspicion of exposure — a leaked log, a departed contractor, a compromised host — using the procedure above. Rotation needs a standard or full key.
  • If the secret is lost, rotate. There is no endpoint that reads it back.

Next steps

Anatomy of a delivery

Headers, the envelope, and how to deduplicate and order what you receive.

Reliability

Retry schedule, delivery statuses, and how a subscription gets deactivated.

Testing webhooks

Test deliveries and echo mode for checking your verifier against real signed bytes.

Managing subscriptions

Create, update and delete subscriptions; where the secret comes from.