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 so it works in every modern runtime without a crypto dependency.
  • timingSafeEqual comparison prevents timing-based key recovery.

Headers on every delivery

HeaderContents
X-RogerIQ-EventEvent type, e.g. conversation.created
X-RogerIQ-DeliveryDelivery id (whd_...) — store for idempotency
X-RogerIQ-Signaturesha256=<hex>
X-RogerIQ-TimestampUnix seconds — reject if drift > 5 min for replay protection
X-RogerIQ-Attempt1 for first try, 2-5 for retries
User-AgentRogerIQ-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,});
Ask a question... ⌘I