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

# Verify webhooks with the TypeScript SDK

> Verify Darwin webhook signatures with Darwin.webhooks.verify in @meetdarwin/api: Express and Next.js route handlers, typed event payloads, local testing with sign, and secret rotation.

Darwin signs every delivery with [Standard Webhooks](https://www.standardwebhooks.com/). `Darwin.webhooks.verify(payload, headers, secret, toleranceSeconds = 300)` checks the signature and the timestamp and returns the parsed event, or throws a `WebhookVerificationError` whose `reason` says why.

> [!NOTE]
>
> **`verify` is async** (it returns a Promise), because it uses Web Crypto so that it runs the same on Node and edge runtimes. Always `await` it.

Pass the **raw** request body, exactly as received. A body that was parsed and turned back into JSON will not verify.

Respond with a 2xx quickly and do slow work afterwards. Darwin retries failed deliveries with the same `webhook-id`, so dedupe on `event.id`. See [Delivery and retries](https://docs.meetdarwin.ai/api-reference/webhooks/delivery.md).

```ts Express theme={null}
import express from "express";
import { Darwin, WebhookVerificationError } from "@meetdarwin/api";

const app = express();

app.post("/webhooks/darwin", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = await Darwin.webhooks.verify(req.body, req.headers, process.env.DARWIN_WEBHOOK_SECRET!);
  } catch (error) {
    if (error instanceof WebhookVerificationError) return res.status(400).send(error.message);
    throw error;
  }

  switch (event.type) {
    case "meeting.booked":
      console.log("Booked:", event.data.object?.invitee.email, event.data.object?.start_at);
      break;
    case "lead.captured":
      console.log("Lead:", event.data.object?.email);
      break;
  }
  res.sendStatus(200);
});
```

```ts Next.js theme={null}
// app/api/webhooks/darwin/route.ts
import { Darwin, WebhookVerificationError } from "@meetdarwin/api";

export async function POST(request: Request) {
  const body = await request.text();
  try {
    const event = await Darwin.webhooks.verify(body, request.headers, process.env.DARWIN_WEBHOOK_SECRET!);
    if (event.type === "lead.captured" && event.data.object) {
      await addToCrm(event.data.object);
    }
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.message, { status: 400 });
    throw error;
  }
}
```

In Express, `express.raw` must run on this route before any JSON body parser, so `req.body` is a Buffer. The Next.js handler works in the Node.js and Edge runtimes alike.

## Event payloads

`event.data.object` is the contact, conversation, meeting, experiment or knowledge source the event is about, as the REST API returns it, or `null` if it no longer exists. `lead.captured` also carries `data.conversation`; `knowledge.source_failed` carries `data.error`. After a privacy erasure an event's payload becomes `{ erased: true }`. Narrowing on `event.type` gives you the right type for `data.object`. Every type: [Events and payloads](https://docs.meetdarwin.ai/api-reference/webhooks/events.md).

The test event sent by `webhookEndpoints.test()` has type `webhook.test`.

## Test locally

To exercise your receiver locally, `Darwin.webhooks.sign(body, secret)` returns the headers Darwin would send.

## Secret rotation

During a secret rotation (`webhookEndpoints.rotateSecret`), deliveries carry a signature for each secret; `verify` accepts the delivery if any of them matches the secret you pass.


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