# Verify webhooks

Self signs every webhook delivery with **HMAC-SHA256**. Verify the signature before trusting the payload, and don't roll your own check: the SDK's `SelfWebhooks.verify(...)` does it correctly in one call and returns a typed event.

`verify(...)` checks two things:

- The signature matches the raw request body and your `whsec_...` signing secret.
- The timestamp is recent (a 5-minute tolerance, which defends against replay).

## Setup

You need:

- The **raw** request body (string or Buffer). Not the JSON-parsed object, signature verification operates on the byte string.
- The request headers from the delivery. Pass them through as-is; the SDK reads the signature and timestamp off them.
- The signing secret (`whsec_...`) for the webhook endpoint. You get it once, when you [add the endpoint](/docs/self-enterprise/dashboard/webhooks/); rotation works by deleting and recreating the endpoint.

## Next.js (App Router)

```ts
// app/api/webhooks/self/route.ts
import { SelfWebhooks, WebhookVerificationError } from '@selfxyz/enterprise-sdk';

export async function POST(req: Request) {
  const raw = await req.text(); // raw body, before any JSON parsing
  const headers = Object.fromEntries(req.headers); // pass all headers through

  try {
    const event = SelfWebhooks.verify(raw, headers, process.env.SELF_WEBHOOK_SECRET!);

    if (event.type === 'verification.completed') {
      // event.verification_id, event.external_uuid, event.proof_attributes, event.status
    }

    return new Response('ok', { status: 200 });
  } catch (err) {
    if (err instanceof WebhookVerificationError) {
      return new Response('bad signature', { status: 400 });
    }
    // Unknown payload shape (SelfValidationError) or server bug: log and 5xx so Self retries.
    return new Response('error', { status: 500 });
  }
}
```

Any other framework works the same way: read the raw request body before JSON parsing, pass `(rawBody, headers, secret)` to `SelfWebhooks.verify(...)`, and branch on `event.type`.

## Type narrowing

`event` is typed as `WebhookEvent`; narrow it on `event.type` and TypeScript does the rest:

```ts
if (event.type === 'verification.completed') {
  event.status; // 'valid' | 'invalid' | 'error' | 'expired'
  event.proof_attributes; // the enforced predicate config, echoed back
  event.nullifier; // uniqueness id (string | null), for Sybil resistance
  // event.proof and event.storage_uri are also available
}
```

## Common failure modes

- **Parsing the body before verification.** If a middleware or framework hook JSON-parses the request before your handler runs, the raw bytes are gone and verification will fail. In a Next.js route handler, read the body with `await req.text()` and don't call `req.json()` first. This is the most common failure by far.
- **Trimming or transforming.** A trailing newline added by your proxy will break the signature. Configure the proxy to pass the body unchanged.
- **Using the wrong secret.** Each endpoint has its own `whsec_...`. If you have multiple endpoints registered (e.g. staging + prod), don't share secrets across them.
- **Clock skew.** A drifted server clock can fail the timestamp tolerance check. Make sure NTP is healthy.

## Redeliveries

The same event can arrive more than once (a retry after a failed delivery, for example). The body and signature are valid each time, so verification succeeds normally. Make your handler idempotent, dedupe on the event's `verification_id`.

See [Best practices](/docs/self-enterprise/webhooks/best-practices/) for idempotency patterns.
