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

# Webhooks

Verify incoming webhook signatures using Web Crypto. Works in Node, Workers, Deno, browsers.

# Webhook signature verification

```ts
import { verifyWebhookSignature } from "@rogeriq/sdk";

// In your HTTP handler:
const rawBody = await req.text();
const signature = req.headers.get("x-rogeriq-signature");

if (!(await verifyWebhookSignature(rawBody, signature, env.WEBHOOK_SECRET))) {
  return new Response("invalid signature", { status: 401 });
}

const event = JSON.parse(rawBody);
console.log(event.event, event.data);
```

**Read the body as a string before parsing JSON.** The signature is
computed over the raw bytes. If you parse first, re-serialize, then
verify, the byte-for-byte match will fail.

## How it works

- HMAC-SHA256 over the raw request body, keyed by your webhook's
  `secret` (returned once at create time).
- Uses the [Web Crypto API](https://developer.mozilla.org/docs/Web/API/Web_Crypto_API)
  so it works in every modern runtime without a crypto dependency.
- `timingSafeEqual` comparison prevents timing-based key recovery.

## Headers on every delivery

| Header | Contents |
|--------|----------|
| `X-RogerIQ-Event` | Event type, e.g. `conversation.created` |
| `X-RogerIQ-Delivery` | Delivery id (`whd_...`) — store for idempotency |
| `X-RogerIQ-Signature` | `sha256=<hex>` |
| `X-RogerIQ-Timestamp` | Unix seconds — reject if drift > 5 min for replay protection |
| `X-RogerIQ-Attempt` | 1 for first try, 2-5 for retries |
| `User-Agent` | `RogerIQ-Webhook/1.0` |

## Payload envelope

```json
{
  "id": "whd_abc123",
  "event": "conversation.created",
  "timestamp": 1700000000,
  "data": { ... }
}
```

The `data` shape depends on the event. Conversation events include the
full conversation object; message events include the message; contact
events include the contact.

## Cloudflare Workers example

```ts
import { verifyWebhookSignature } from "@rogeriq/sdk";

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const rawBody = await req.text();
    const sig = req.headers.get("x-rogeriq-signature");
    if (!(await verifyWebhookSignature(rawBody, sig, env.WEBHOOK_SECRET))) {
      return new Response("invalid signature", { status: 401 });
    }

    const event = JSON.parse(rawBody);
    switch (event.event) {
      case "conversation.created":
        // ...
        break;
      case "message.created":
        // ...
        break;
    }

    return new Response("ok");
  },
};
```

## Idempotency

The `X-RogerIQ-Delivery` header is unique per attempt. The same delivery
re-sent during a retry has the same `X-RogerIQ-Delivery` so you can
deduplicate. Store recently-seen delivery ids in KV / Redis for a
~24h window.

## Test deliveries

```bash
rogeriq webhooks test wh_xxx
```

Or via API: `POST /api/v1/projects/:pid/webhooks/:id/test`. Test events
have `event: "webhook.test"` and skip the long-tail retry workflow.

## Forwarding signed bodies

If you need to forward a webhook through a proxy and re-sign:

```ts
import { signWebhookPayload } from "@rogeriq/sdk";

const forwardSig = await signWebhookPayload(rawBody, downstreamSecret);
await fetch(downstreamUrl, {
  method: "POST",
  headers: { "X-RogerIQ-Signature": forwardSig, "Content-Type": "application/json" },
  body: rawBody,
});
```