Webhook Verification
Verify that webhooks really come from CM's Email Gateway before you process them.
We sign every webhook we send to your endpoint. Verifying that signature before you act on a request tells you that the message really came from us, that it was not changed on the way, and that it is not an old message being replayed.
Anyone who knows your endpoint URL can send a request to it. Without verification, your application cannot tell real events apart from forged ones.
How it works
When our Email Gateway sends a webhook, it creates a signature from the message and a secret key that only you and our Email Gateway know. The signature is sent along with the request in three HTTP headers:
| Header | Description |
|---|---|
svix-id | Unique identifier of the webhook message. |
svix-timestamp | Unix timestamp (in seconds) of when the message was sent. |
svix-signature | The signature of the message. |
When your endpoint receives the request, it repeats the calculation with its own copy of the secret key and checks the result. The verification:
- Checks that all three headers are present.
- Checks that the timestamp is valid and recent, to protect against replayed requests. By default a message is accepted for 5 minutes.
- Recalculates the signature with HMAC-SHA512 and compares it to
svix-signatureusing a constant-time comparison.
If any of these checks fail, the request must be rejected and not processed.
Before you start
You need:
- A webhook secret key. You can find it under Settings > Integration.
- A publicly reachable HTTPS endpoint that accepts
POSTrequests. - Access to the raw request body. The signature is calculated over the exact content that CM's Email Gateway sent.
Verify the raw body
If your framework parses and re-serializes the JSON before you verify it, the signature will not match. Always pass the unparsed request body to the verification.
Protect your secret key
Keep the secret key out of your source code. Store it in an environment variable or a secrets manager.
Verify with an SDK
We recommend using one of our SDKs instead of implementing the check yourself. They handle the header checks, timestamp validation, signature comparison and payload parsing, and they raise clear errors when verification fails.
The source code of all SDKs is available on GitHub. Each package page contains installation instructions and framework examples.
In every SDK the flow is the same:
validator = WebhookValidator(secret_key)
on incoming POST request:
payload = raw request body (unparsed)
headers = svix-id, svix-timestamp, svix-signature
try:
event = validator.verify(payload, headers)
process(event)
respond 200
on verification error:
respond 401
The timestamp tolerance (5 minutes by default) can be changed when you create the validator. A shorter window gives an attacker less time to replay a captured request. A longer window is more forgiving of clock differences between servers.
Verification errors
The SDKs report four kinds of failures. The exact class names differ per language, so see the package documentation.
| Failure | Meaning |
|---|---|
| Missing headers | One or more of the svix-* headers is not present on the request. |
| Invalid timestamp | The svix-timestamp header is not a valid timestamp. |
| Timestamp expired | The timestamp is outside the allowed tolerance window. |
| Invalid signature | The signature does not match the message. Wrong secret key, or the body was changed. |
You can treat all failures the same way (reject the request) or handle them separately, for example for logging.
Responding to webhooks
- Verification succeeded: return a
2xxstatus once you have accepted the event. Do slow work asynchronously where possible, so you respond quickly. - Verification failed: return
401 Unauthorizedand do not process the payload. Do not reveal why verification failed in the response.
Best practices
- Verify before you process. Do not act on the payload until verification has passed.
- Verify the raw body. Parse the JSON only after verification, or let the SDK do it for you.
- Protect the secret key. Keep it out of source control and rotate it if you suspect it has leaked.
- Handle duplicates. Store the
svix-idof messages you have processed and skip repeats, so your handler stays safe if a message is delivered more than once. - Keep your server clock in sync (NTP). Clock drift is the most common reason for expired timestamps.
Troubleshooting
Invalid signature on every request
- The secret key is wrong, for example from another environment or with extra whitespace.
- The body was modified before verification. Typical causes are framework JSON parsing and re-serialization, middleware that trims or re-encodes the body, or automatic model binding. Read the raw body instead.
- A proxy or gateway in front of your application changes the body.
Missing headers
- A proxy, load balancer or API gateway removes or renames the
svix-*headers. Check which headers your application actually receives.
Timestamp expired
- The clock on your server is incorrect.
- The request waited in a queue for longer than the tolerance before it was verified. Verify as soon as the request arrives.
- You are resending a captured request while testing. Signed requests are only valid for the tolerance window, so trigger a new event instead.
Updated 1 day ago