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

# Verify webhooks

> Check the signature on every Tolt webhook delivery before you act on it, and get a typed event back with @tolt/webhooks.

Your endpoint is public, so anyone can POST to it. Verify the signature with [`@tolt/webhooks`](/sdks/webhooks) and you get a typed event back, or an error.

\$ npm install @tolt/webhooks

**Next.js**

```ts app/api/webhooks/tolt/route.ts theme={"system"}
import { verifyWebhook, WebhookVerificationError } from "@tolt/webhooks";

export async function POST(req: Request) {
  try {
    const event = verifyWebhook({
      payload: await req.text(),
      headers: req.headers,
      secret: process.env.TOLT_WEBHOOK_SECRET!,
    });
    // handle event.type / event.data here
    return new Response("ok");
  } catch (err) {
    if (err instanceof WebhookVerificationError) {
      return new Response("invalid signature", { status: 400 });
    }
    throw err;
  }
}
```

**Express**

```ts server.ts theme={"system"}
import express from "express";
import { verifyWebhook, WebhookVerificationError } from "@tolt/webhooks";

const app = express();

app.post(
  "/api/webhooks/tolt",
  express.raw({ type: "application/json" }),
  (req, res) => {
    try {
      const event = verifyWebhook({
        payload: req.body.toString("utf8"),
        headers: req.headers,
        secret: process.env.TOLT_WEBHOOK_SECRET!,
      });
      // handle event.type / event.data here
      res.sendStatus(200);
    } catch (err) {
      if (err instanceof WebhookVerificationError) {
        return res.sendStatus(400);
      }
      throw err;
    }
  },
);
```

> [!WARNING]
>
> Verify the **raw** body. If your framework parses JSON first, the re-serialized bytes won't match the signature.

## What it checks

| Check | Fails when |
| - | - |
| Headers | `svix-id`, `svix-timestamp` or `svix-signature` is missing |
| Signature | The body or secret doesn't match (constant-time HMAC-SHA256 compare) |
| Freshness | The timestamp is more than 5 minutes off. Change it with `toleranceInSeconds` |

> **Verify without the package**
>
> Deliveries are signed by Svix, so any Svix verifier works. By hand: base64-decode the part of your secret after `whsec_`, compute HMAC-SHA256 over `${svix-id}.${svix-timestamp}.${rawBody}`, base64-encode it, and compare in constant time against each `v1,<signature>` entry in the space-separated `svix-signature` header. Reject timestamps more than about 5 minutes off, in either direction.


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