Webhooks
Verify incoming webhook signatures using Web Crypto. Works in Node, Workers, Deno, browsers.
Webhook signature verification
tsimport { 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 so it works in every modern runtime without a crypto dependency.
timingSafeEqualcomparison 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
tsimport { 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
bashrogeriq 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:
tsimport { 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,});