Verifying a message came from us
Every delivery is signed. Verify the signature before you act on a message, and reject anything that fails.
Each endpoint you register has its own signing secret, which we generate and give you when the endpoint is set up. It is a shared secret: hold it the way you would any credential, and tell us if it needs rotating.
The secret arrives with a whsec_ prefix. Strip the prefix and base64 decode what remains. The decoded bytes are the key you pass to HMAC, not the string as it arrives.
Three headers travel with each delivery:
| Header | What it is |
|---|---|
| id | A unique identifier for this delivery. The same message retried keeps the same id. |
| timestamp | When we sent it, in unix seconds. |
| signature | One or more signatures, space separated. Each entry is a version label and a base64 signature joined by a comma, such as v1,WCnukQBQ.... |
To verify: build the signed content as the id, the timestamp and the raw request body joined by full stops, compute HMAC SHA-256 over it with the decoded secret, and base64 encode the result using the standard alphabet. Then split the signature header on spaces. For each entry, split on the comma, skip any entry whose version label is not v1, and compare your result against the signature part. Compare in constant time. More than one signature can be present during a secret rotation, and the message is valid if any of them matches.
Use the raw body exactly as received. Re-serialising the JSON first will change the bytes and the signature will not match.
Reject a message whose timestamp is more than five minutes from your own clock, in either direction. That stops an old delivery being replayed at you.
A worked example
These values let you check an implementation end to end. The secret is a test value, not one of ours.
secret whsec_Y2FzYXAtZG9jcy1leGFtcGxlLWtleQ==
decoded key casap-docs-example-key
id msg_2gTaB9xQkLmNpRsV4wYz
timestamp 1759708800
body {"type":"dispute.updated","disputeUuid":"3f8e2c1a-9b47-4d6e-a1f0-7c2d5b8e4a91","occurredAt":"2026-10-06T12:00:00Z","data":{}}
The signed content is those three parts joined by full stops:
msg_2gTaB9xQkLmNpRsV4wYz.1759708800.{"type":"dispute.updated","disputeUuid":"3f8e2c1a-9b47-4d6e-a1f0-7c2d5b8e4a91","occurredAt":"2026-10-06T12:00:00Z","data":{}}
HMAC SHA-256 over that with the decoded key, base64 encoded, gives the signature:
v1,WCnukQBQ5M0qeEk4qWPiIhOd2eEJ3VBEfZte5ZfeZg8=
An implementation that reproduces that signature from that secret and body is reading the headers correctly.
Standard webhook verification libraries implement all of this for you, and the exact header names are confirmed with the secret at onboarding.