> For the complete documentation index, see [llms.txt](https://docs.omni.integratedcommerce.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.omni.integratedcommerce.io/webhooks/svix/verifying-signatures.md).

# Verifying signatures

Validate the svix-id, svix-timestamp, and svix-signature headers on every Svix delivery before trusting it, with an official Svix library.

Every Svix delivery is signed, and your handler must verify the signature before doing anything else.

{% hint style="warning" %}
A webhook endpoint is a public URL that accepts POSTs from the internet. Without signature verification, anyone who learns the URL can post a forged `sale.completed` and have your system ship goods for a payment that never happened. Never skip this.
{% endhint %}

If your deliveries carry `x-fsk-wh-chksm` instead of the `svix-*` headers, you are on legacy webhooks - see [Verifying the checksum](/webhooks/legacy/verifying-the-checksum.md).

## The headers

Every Svix delivery includes three headers used to verify authenticity:

| Header           | Meaning                                                                               |
| ---------------- | ------------------------------------------------------------------------------------- |
| `svix-id`        | The unique message id for this delivery. Constant across retries of the same message. |
| `svix-timestamp` | The time the message was sent (Unix seconds).                                         |
| `svix-signature` | One or more space-delimited signatures, each in the form `v1,<base64 signature>`.     |

## How the signature is computed

The signature is an HMAC-SHA256 over the string `{svix-id}.{svix-timestamp}.{raw request body}`, keyed with your endpoint's signing secret (the base64-decoded portion after the `whsec_` prefix). The result is base64-encoded and must match one of the signatures in the `svix-signature` header - multiple signatures may be present while a secret rotation is in progress.

{% hint style="danger" %}
Use the raw JSON exactly as received in the HTTP request body - no reformatting, indentation changes, or whitespace modifications. Any framework that parses and re-serializes the body before you compute the HMAC will produce a signature that never matches.
{% endhint %}

## Use an official library

Verify signatures with one of the official [Svix libraries](https://docs.svix.com/receiving/verifying-payloads/how) rather than implementing the check by hand. The libraries also enforce a timestamp tolerance that protects against replay attacks - a hand-rolled HMAC comparison that ignores `svix-timestamp` will happily accept a genuine signed payload that an attacker captured and replayed days later.

The libraries handle, in one call: the exact signing string, base64 handling, multiple signatures during rotation, constant-time comparison, and timestamp tolerance.

{% tabs %}
{% tab title="Node.js" %}

```javascript
import { Webhook } from "svix";
import express from "express";

const app = express();
const wh = new Webhook(process.env.WEBHOOK_SIGNING_SECRET);

// express.raw is important: the verifier needs the unparsed body.
app.post("/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = wh.verify(req.body, {
      "svix-id": req.header("svix-id"),
      "svix-timestamp": req.header("svix-timestamp"),
      "svix-signature": req.header("svix-signature"),
    });
  } catch {
    return res.sendStatus(400);
  }

  try {
    await enqueue(event); // persist before acknowledging
  } catch {
    return res.sendStatus(503);
  }
  return res.sendStatus(202);
});
```

{% endtab %}

{% tab title="Python" %}

```python
from svix.webhooks import Webhook, WebhookVerificationError
from flask import Flask, request, abort
import os

app = Flask(__name__)
wh = Webhook(os.environ["WEBHOOK_SIGNING_SECRET"])

@app.post("/webhooks")
def webhooks():
    try:
        # request.data is the raw body, not the parsed JSON.
        wh.verify(request.data, dict(request.headers))
    except WebhookVerificationError:
        abort(400)

    enqueue(request.get_json())
    return "", 202
```

{% endtab %}

{% tab title="C#" %}

```csharp
app.MapPost("/webhooks", async (HttpRequest request) =>
{
    using var reader = new StreamReader(request.Body);
    var payload = await reader.ReadToEndAsync();

    var wh = new Svix.Webhook(
        Environment.GetEnvironmentVariable("WEBHOOK_SIGNING_SECRET"));

    try
    {
        var headers = new System.Net.WebHeaderCollection();
        foreach (var name in new[] { "svix-id", "svix-timestamp", "svix-signature" })
            headers[name] = request.Headers[name].ToString();
        wh.Verify(payload, headers);
    }
    catch (Svix.Exceptions.WebhookVerificationException)
    {
        return Results.BadRequest();
    }

    Enqueue(payload);
    return Results.Accepted();
});
```

{% endtab %}
{% endtabs %}

The examples use an application-provided `enqueue` / `Enqueue` function. It must save the event durably before returning, and fail the request if the save fails. Keep JSON-parsing middleware after the raw-body webhook route.

## Order of operations

{% stepper %}
{% step %}

## Read the raw body

Before any JSON parsing or middleware transforms it.
{% endstep %}

{% step %}

## Verify the signature

Reject with `400` if verification fails. Do not log the payload contents of an unverified request as though it were genuine.
{% endstep %}

{% step %}

## Deduplicate on event ID

Retries, replays from the portal, and dual delivery while you move from legacy all mean the same event can arrive more than once. See [Deduplicate on event.id](/webhooks/webhooks.md#deduplicate-on-event-id).
{% endstep %}

{% step %}

## Save, acknowledge, then process

Save the verified event to a durable queue, then return `2xx` promptly. Process the queued event asynchronously. Return a non-`2xx` response if saving fails.
{% endstep %}
{% endstepper %}

## This is not the same as redirect verification

The [Hosted Payment Form redirect hash](/hosted-payment-form/redirect-verification.md) is a separate mechanism with a different input, secret, and encoding. Verifying one does not verify the other. See the [comparison table](/hosted-payment-form/redirect-verification.md#this-is-not-the-same-as-webhook-verification).
