Skip to main content
Every Referly webhook delivery is signed. Verifying that signature is what tells you a request came from Referly and not from someone who found your URL. Referly signs using the Svix scheme, so the shortest correct answer is: install the official 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 can POST this:
Your handler cannot tell that apart from a real delivery. With a signature check, it can — the attacker cannot produce a valid signature without the secret.
Checking that the payload “looks right”, that the affiliate ID exists, or that the request came from a particular IP is not a substitute. Referly does not publish fixed egress IPs, and every field in the body is attacker-controlled. The signature is the only proof.

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 like whsec_ 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
Never put the secret in client-side code, a mobile app, or version control. Anyone with it can forge deliveries that pass your verification. If it leaks, rotate it — see Manage endpoints.
Because the secret is per endpoint, a handler serving several endpoints — one per program, say — needs to select the right secret for the request, usually by giving each endpoint its own path.

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.
Official libraries also exist for Ruby, PHP, Java, Kotlin, C#, and Rust, all with the same 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.
If a global body-parsing middleware runs before your route, it consumes the stream and you cannot recover the original bytes. In Express, mount express.json() after the webhook route, or exclude the webhook path from it. Verify first, parse second — parse the string you already verified.

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)
Four details that matter:
  • rawBody is 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. Use timingSafeEqual or your language’s equivalent, and check lengths first since it throws on a mismatch.
  • Iterate the space-separated list. Accept the request if any v1 entry matches.

When verification fails

Return 400 and do nothing else.
Do not log the payload as if it were real, do not write it to your database, and do not return 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. Because svix-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.
Combine the timestamp window with 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:
Deploy that, rotate, confirm the delivery log is clean, then remove the old secret. Leaving a retired secret accepted indefinitely defeats the point of rotating.

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. Use svix-id as the idempotency key. It is stable across every attempt of the same event:
Return 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.body with the decoded secret.
  • On failure, return 400 and process nothing.
  • Enforce a five minute svix-timestamp window.
  • Deduplicate on svix-id before doing any work.
  • Keep the secret server-side and out of version control, and rotate it if it is ever exposed.
Test the whole chain before you rely on it — the Testing tab on an endpoint sends a real signed delivery, so a passing test proves your verification works end to end. See Test and replay.

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.
Last modified on July 21, 2026