Skip to main content
Anyone who knows your endpoint URL can send it a request. Verify the signature on every delivery and reject anything that fails. Larynth signs webhooks with the Standard Webhooks scheme.

How the signature is built

  1. Take the endpoint secret. Remove the whsec_ prefix and base64-decode the rest. The result is the HMAC key.
  2. Build the signed content: <webhook-id>.<webhook-timestamp>.<raw body>, joined with dots.
  3. Compute HMAC-SHA256 of the signed content with the key, then base64-encode it.
  4. The signature is v1, followed by that value.
The webhook-signature header holds one or more signatures separated by spaces, for example v1,AAA= v1,BBB=. The request is valid if any v1 signature matches. Use the raw request body, exactly as received. If you parse the JSON and serialize it again, the bytes change and the signature no longer matches. Also check webhook-timestamp. Reject requests more than 5 minutes away from your clock. This stops an attacker from replaying an old request.

Test vector

Use this to check your code before you go live.

Verify with an SDK

The TypeScript (larynth-node) and Python (larynth-python) SDKs, both 0.1 beta, include a verifier. It checks the headers, the timestamp window and the signature in constant time, and throws when anything is wrong. See the TypeScript and Python pages for installation.
verifyWebhook and verify accept a single secret or a list of secrets. The default tolerance is 300 seconds. Pass toleranceSeconds (TypeScript) or tolerance_seconds (Python) to change it.

Verify without an SDK

This example reproduces the test vector above. Pass the current time instead of the fixed value in production.
Both examples print v1,49s/QnkINzN3EO848IVtTP/5Tpo+qBtrkQsYjg9+dhs= and then true.

Rotate a secret

POST /v1/webhooks/{id}/rotate-secret returns a new secret, shown once. For the next 24 hours the old secret stays valid and each delivery carries one signature per secret, so the header has two values. Deploy the new secret to your server during that window. If you verify with your own code, accept the request when any signature in the header matches. If you use the SDK, pass both secrets as a list until the old one expires.

Troubleshooting

  • Signature never matches. You are probably signing a parsed and re-serialized body. Use the raw bytes. In Express, read the body with express.raw({ type: "application/json" }).
  • Timestamp outside tolerance. Check your server clock.
  • Works for test events but not live ones. Confirm you use the secret of the endpoint that received the delivery. Each endpoint has its own secret.