> ## Documentation Index
> Fetch the complete documentation index at: https://docs.amps.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify signatures

> Confirm a webhook came from Amps before you act on it.

Every webhook is signed with your endpoint's signing secret. Verify the signature on every request, and reject any that fail.

## Headers

Each request carries three headers. You need all three to verify.

| Header | |
| - | - |
| `svix-id` | Unique ID for this message. Stays the same across retries. |
| `svix-timestamp` | When the message was signed, in Unix seconds. |
| `svix-signature` | One or more signatures, space-separated, each `v1,<base64>`. |

The signing secret is on the endpoint in **Settings → Webhooks** in the [dashboard](https://app.amps.ai). It starts with `whsec_`. Sandbox and live endpoints have different secrets.

## Verify with the Svix library

Amps signs webhooks with Svix, so you can use the `svix` package.

```bash theme={null}
npm install svix
```

The signature covers the exact bytes Amps sent. Verify the raw body, not a parsed and re-serialised copy.

<Tabs>
  <Tab title="Express">
    ```ts theme={null}
    import express from "express";
    import { Webhook } from "svix";

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

    // Use express.raw on this route so req.body is the unparsed Buffer.
    app.post("/webhooks/amps", express.raw({ type: "application/json" }), (req, res) => {
      let event: Record<string, unknown>;
      try {
        event = wh.verify(req.body, {
          "svix-id": req.header("svix-id") ?? "",
          "svix-timestamp": req.header("svix-timestamp") ?? "",
          "svix-signature": req.header("svix-signature") ?? "",
        }) as Record<string, unknown>;
      } catch {
        return res.status(400).send("Invalid signature");
      }

      // event is the parsed, verified payload.
      handleEvent(event);
      res.sendStatus(204);
    });
    ```
  </Tab>

  <Tab title="Next.js">
    ```ts theme={null}
    // app/api/webhooks/amps/route.ts
    import { Webhook } from "svix";

    const wh = new Webhook(process.env.AMPS_WEBHOOK_SECRET!);

    export async function POST(req: Request) {
      const body = await req.text();

      let event: Record<string, unknown>;
      try {
        event = wh.verify(body, {
          "svix-id": req.headers.get("svix-id") ?? "",
          "svix-timestamp": req.headers.get("svix-timestamp") ?? "",
          "svix-signature": req.headers.get("svix-signature") ?? "",
        }) as Record<string, unknown>;
      } catch {
        return new Response("Invalid signature", { status: 400 });
      }

      await handleEvent(event);
      return new Response(null, { status: 204 });
    }
    ```
  </Tab>
</Tabs>

`verify` throws if the signature doesn't match or the timestamp is more than 5 minutes from now. On success it returns the parsed payload.

## Verify without the library

If you can't use `svix`, compute the signature yourself:

1. Build the signed content: `{svix-id}.{svix-timestamp}.{raw body}`.
2. Base64-decode the part of your secret after `whsec_`. That's the key.
3. Compute HMAC-SHA256 of the signed content with the key, and base64-encode it.
4. Compare it, in constant time, with each `v1,` signature in `svix-signature`. Accept if any match.
5. Reject if `svix-timestamp` is more than 5 minutes from now.

```ts theme={null}
import crypto from "node:crypto";

function verifyAmpsWebhook(rawBody: string, headers: Record<string, string>, secret: string): boolean {
  const id = headers["svix-id"];
  const timestamp = headers["svix-timestamp"];
  const signatures = headers["svix-signature"];
  if (!id || !timestamp || !signatures) return false;

  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto
    .createHmac("sha256", key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest();

  return signatures.split(" ").some((entry) => {
    const [version, sig] = entry.split(",");
    if (version !== "v1" || !sig) return false;
    const received = Buffer.from(sig, "base64");
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}
```

## Troubleshooting

| Symptom | Likely cause |
| - | - |
| Every request fails | The body was parsed before verifying. Use the raw body. |
| Every request fails | Wrong secret. Sandbox and live endpoints have different secrets. |
| Some requests fail | Your server clock is off by more than 5 minutes. |

## Next steps

<CardGroup cols={2}>
  <Card title="Webhooks" icon="bell" href="/guides/webhooks">
    Endpoint setup, retries, and duplicates.
  </Card>

  <Card title="Events" icon="list" href="/guides/webhooks/events">
    Every event and its payload.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.