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:

HeaderDescription
svix-idUnique identifier of the webhook message.
svix-timestampUnix timestamp (in seconds) of when the message was sent.
svix-signatureThe 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:

  1. Checks that all three headers are present.
  2. Checks that the timestamp is valid and recent, to protect against replayed requests. By default a message is accepted for 5 minutes.
  3. Recalculates the signature with HMAC-SHA512 and compares it to svix-signature using 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 POST requests.
  • 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.

LanguagePackageDetails
.NETCM.Email.WebhookVerificationNuGet
Pythoncm-email-webhook-verificationPyPI
Rustcm-email-webhook-verificationcrates.io · docs.rs

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.

FailureMeaning
Missing headersOne or more of the svix-* headers is not present on the request.
Invalid timestampThe svix-timestamp header is not a valid timestamp.
Timestamp expiredThe timestamp is outside the allowed tolerance window.
Invalid signatureThe 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 2xx status once you have accepted the event. Do slow work asynchronously where possible, so you respond quickly.
  • Verification failed: return 401 Unauthorized and 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-id of 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.

Did this page help you?