# Handle webhooks

> Verify the HMAC signature, respect the replay window, and respond before Hatched retries.

Source: https://docs.hatched.live/docs/guides/handle-webhooks

Webhooks are how your backend reacts to buddy events. Hatched signs every
request so you can trust the payload came from us and hasn't been tampered
with.

## Subscribe

1. Dashboard → Settings → Webhooks → **Add endpoint**.
2. Pick the event types you care about
   ([catalogue](/docs/reference/webhook-payloads)).
3. Copy the signing secret **once** — we don't show it again.

Programmatically:

```ts
await hatched.webhooks.create({
  url: 'https://your-app.com/api/webhooks/hatched',
  events: ['buddy.leveled_up', 'badge.awarded', 'streak.milestone'],
});
```

## Verify with the SDK helper

`@hatched/sdk-js` ships a static helper for signature verification:

```ts
import { WebhooksResource } from '@hatched/sdk-js';

export async function POST(req: Request) {
  const rawBody = await req.text();
  const signature = req.headers.get('hatched-signature') ?? '';

  const valid = WebhooksResource.verifySignature(
    rawBody,
    signature,
    process.env.HATCHED_WEBHOOK_SECRET!,
  );
  if (!valid) return new Response('invalid signature', { status: 400 });

  const event = JSON.parse(rawBody);
  await handle(event);
  return new Response(null, { status: 202 });
}
```

The header format is `t=<unix_ts>,v1=<hmac_sha256_hex>`. The helper
verifies the HMAC, rejects timestamps older than `toleranceSeconds`
(default 300), and uses `timingSafeEqual` under the hood.

> Sign over **raw body bytes**. A JSON `parse`→`stringify` round-trip
> reorders keys and breaks the signature. Read the body as `Buffer` or
> `string` **before** any framework middleware parses it as JSON.

## Manual verification (without the SDK)

```ts
import crypto from 'node:crypto';

export function verifyHatchedSignature(header: string, rawBody: Buffer, secret: string) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const ts = parts.t;
  const sig = parts.v1;
  if (!ts || !sig) return false;

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

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${rawBody.toString('utf8')}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(sig, 'hex'),
    Buffer.from(expected, 'hex'),
  );
}
```

## Respond quickly

- Return a 2xx within 10 seconds, or Hatched retries the delivery.
- Retry schedule: **+5s, +30s, +5min**. After the third failure the
  delivery is marked `failed` in the delivery log but the state in Hatched
  is already correct.
- If your handler is expensive, ack fast and push to a queue.

## Idempotency

Every webhook carries a unique `deliveryId`. Dedupe against it before
side-effects:

```ts
if (await alreadyHandled(event.deliveryId)) return ack();
await recordHandled(event.deliveryId);
await doTheWork(event);
```

## Replay from the delivery log

Dashboard → Developers → Webhook deliveries shows every attempt with
payload, headers, and response. Replay failed deliveries once your
endpoint is healthy:

```ts
await hatched.webhooks.replay(deliveryId);
```

## Framework examples

- [Next.js route handler](/docs/guides/nextjs-integration) — App Router,
  raw body, signature verify.
- [Express middleware](/docs/guides/express-integration) — `express.raw`
  + signature verify before JSON parsing.
- [Edge runtimes](/docs/guides/edge-runtimes) — Workers/Vercel Edge notes.
