svix
library for your language and call verify(). The rest of this page covers where the secret comes
from, the headers involved, and the details that make verification fail in practice.
Why this is not optional
Your endpoint is a public URL that accepts JSON and does something valuable with it — records a sale, credits an account, pays a commission. The URL is the only other thing standing between an attacker and that behaviour, and URLs leak constantly: browser history, proxy logs, error trackers, screenshots, a pasted stack trace, an ex-employee’s laptop. Without a signature check, anyone holding your URL canPOST this:
Get your signing secret
Each endpoint has its own secret. Open your campaign, go to Settings, then Advanced, then Webhooks, open the endpoint from its … menu with View Details, and find Signing Secret. It is masked until you select the eye icon, and there is a button to copy it. The value looks likewhsec_ followed by a base64 string. The official libraries accept it exactly
as shown — you do not need to strip the prefix yourself.
Store it the way you store a database password:
.env
The signature headers
Three headers travel with every delivery.svix-signature can carry several values separated by spaces. A delivery is authentic if any one
of them matches — this is what lets an endpoint keep working through a secret rotation. Never
compare the header as a whole string; parse it into parts.
Verify with the official library
The libraries handle header parsing, multiple signatures, timestamp tolerance, and constant-time comparison. Use them unless your language has none.verify(payload, headers) shape.
Use the raw body
This is the single most common reason verification fails on a correct implementation. The signature is computed over the exact bytes Referly sent. If your framework parses the JSON and you re-serialise it before verifying, the bytes change — key order, whitespace, number formatting, unicode escaping — and the signature will never match, no matter how correct everything else is.Verify manually
If your language has no official library, the scheme is short enough to implement. Concatenate the message ID, the timestamp, and the raw body with dots, HMAC-SHA256 it with the decoded secret, and base64 the result.Manual verification (Node)
rawBodyis a string or byte buffer of the original request, not a re-serialised object.- The key is decoded from base64 after stripping
whsec_. Using the literal secret string as the HMAC key produces a signature that never matches. - Compare in constant time. A plain
===leaks timing information that can be used to forge a signature byte by byte. UsetimingSafeEqualor your language’s equivalent, and check lengths first since it throws on a mismatch. - Iterate the space-separated list. Accept the request if any
v1entry matches.
When verification fails
Return400 and do nothing else.
200
to make the noise stop.
A 400 counts as a failed delivery, so a legitimate message rejected by a broken implementation
enters the retry cycle and appears in your log with the response body you returned. That is useful:
fix the handler, then resend from the log rather than losing the event. See
Retries and logs.
Return a short, non-specific reason. Do not tell a caller why verification failed — that helps
someone probing your endpoint and helps nobody else.
Replay protection
A valid signature stays valid forever, so an attacker who captures one real delivery could send those exact bytes and headers again. Becausesvix-timestamp is part of the signed content, it cannot be
edited without breaking the signature — which makes it a reliable freshness check.
The official libraries reject deliveries whose timestamp is more than five minutes from your server’s
clock, in either direction. If you verify manually, implement the same window, as in the example
above.
A tight tolerance means your server clock matters. If verification fails on genuinely fresh
deliveries, check that NTP is running — clock drift produces exactly this symptom, usually in
containers or long-lived VMs.
svix-id deduplication for full replay protection: the window
stops old captures, and the ID stops a fresh one being processed twice.
During a secret rotation
When you rotate an endpoint’s secret, deploy the new value promptly — anything still verifying against the old one will reject deliveries, and each rejection is a failed delivery. To rotate without dropping anything in flight, accept either secret for the duration:Pair verification with idempotency
Verification proves a message is authentic. It does not prove you have not already processed it — retries and manual resends deliver the same authentic message again. Usesvix-id as the idempotency key. It is stable across every attempt of the same event:
200 for duplicates. A duplicate is not an error, and returning anything else sends the
message back into the retry cycle.
This is the receiving side. Idempotency when sending data to Referly — deduplicating sales you
report — is a separate mechanism, covered in
Idempotency.
Checklist
- Read the raw body before any middleware parses it.
- Verify with the official library, or HMAC-SHA256 over
id.timestamp.bodywith the decoded secret. - On failure, return
400and process nothing. - Enforce a five minute
svix-timestampwindow. - Deduplicate on
svix-idbefore doing any work. - Keep the secret server-side and out of version control, and rotate it if it is ever exposed.
Related
Manage endpoints
Find and rotate an endpoint’s signing secret.
Payload structure
What you can parse once the signature checks out.
Retries and logs
See rejected deliveries and resend them after a fix.
Test and replay
Send a signed test delivery to prove verification works.
Webhooks introduction
How deliveries work end to end.
Idempotency
The other direction — deduplicating what you send to Referly.