# Verify signatures

How your customers confirm a webhook really came from you, using the Standard Webhooks scheme.

Anyone can post to a customer's URL. A signature lets the customer prove a request came from you and wasn't altered. Dispatch signs every delivery using the [Standard Webhooks](https://www.standardwebhooks.com) scheme, so your customers can verify with any of its open-source libraries (the same ones Svix customers use) instead of writing cryptography.

## Headers

Each delivery carries:

| Header | Meaning |
|---|---|
| `webhook-id` | The message ID (`msg_…`). The same on every retry, so use it to ignore duplicates. |
| `webhook-timestamp` | When this attempt was sent, in Unix seconds. Reject requests that are too old. |
| `webhook-signature` | One or more space-separated `v1,<base64>` signatures. |
| `x-ensue-delivery-id` | The delivery's own ID, for support requests. |

## How the signature is made

1. Take the string `"<webhook-id>.<webhook-timestamp>.<raw body>"`.
2. Compute its HMAC-SHA256 with the endpoint's secret. The secret is `whsec_` followed by base64; **decode the base64** to get the key bytes.
3. Base64-encode the result and prefix it with `v1,`.

The header lists more than one signature during a [secret rotation](/dispatch/endpoints/#rotate-a-secret). A request is genuine if **any** of them matches.

## Verify with a library

Your customer installs a Standard Webhooks library and passes it the secret, the headers and the **raw** body.

```js
import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.ENSUE_WEBHOOK_SECRET); // "whsec_…"

app.post("/hooks", express.raw({ type: "application/json" }), (req, res) => {
  try {
    const event = wh.verify(req.body.toString(), {
      "webhook-id": req.header("webhook-id"),
      "webhook-timestamp": req.header("webhook-timestamp"),
      "webhook-signature": req.header("webhook-signature"),
    });
    // event.type, event.data …
    res.sendStatus(200);
  } catch {
    res.sendStatus(400);
  }
});
```

Libraries exist for JavaScript, Python, Go, Java, Ruby, PHP, Rust, C# and more. The library also rejects a timestamp outside its tolerance (five minutes by default), which stops an attacker replaying a captured request later.

## What to tell your customers

1. **Verify before you parse.** Use the exact raw body bytes. Re-serialising JSON changes the bytes and breaks the signature.
2. **Keep the secret secret.** It's shown with the endpoint, and can be rotated at any time.
3. **Return `2xx` quickly**, then process in the background. Anything else is retried. See [Retries and recovery](/dispatch/retries-and-recovery/).
4. **De-duplicate on `webhook-id`.** Delivery is at least once.

> [!TIP]
> Put these four points in your public webhook documentation. Customers who verify correctly are customers who never open a support ticket.

## Finding the secret

`GET /v1/dispatch/endpoints/:id/secret` returns an endpoint's current secret. Rotation keeps the previous secret signing for 24 hours, so customers can move to the new one with no downtime.