Webhooks API
Subscribe to events, fire test deliveries, inspect history, rotate secrets, and use the long-tail retry workflow.
Webhooks API
External systems react to RogerIQ events. Inline retries handle transient failures; a Cloudflare Workflow handles the long tail (up to 12 hours).
Endpoints
| Method | Path | Scope |
|---|---|---|
GET | /v1/projects/{projectId}/webhooks | read |
POST | /v1/projects/{projectId}/webhooks | webhooks:write |
GET | /v1/projects/{projectId}/webhooks/{webhookId} | read |
PATCH | /v1/projects/{projectId}/webhooks/{webhookId} | webhooks:write |
DELETE | /v1/projects/{projectId}/webhooks/{webhookId} | webhooks:write |
POST | /v1/projects/{projectId}/webhooks/{webhookId}/test | webhooks:write |
GET | /v1/projects/{projectId}/webhooks/{webhookId}/deliveries | read |
POST | /v1/projects/{projectId}/webhooks/{webhookId}/rotate-secret | webhooks:write |
Create
bashcurl -X POST https://api.rogeriq.com/api/v1/projects/prj_xxx/webhooks \ -H "X-API-Key: riq_xxx" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/rogeriq-hook", "events": ["conversation.*", "message.created"] }'
bashrogeriq webhooks create \ --url https://example.com/rogeriq-hook \ --events 'conversation.*,message.created'
tsconst wh = await roger.webhooks.create({ url: "https://example.com/rogeriq-hook", events: ["conversation.*", "message.created"],});// wh.secret is populated — store it.
The response includes secret exactly once. Store it for signature
verification.
Events
| Event | Fired when |
|---|---|
conversation.created | A new conversation opens |
conversation.updated | Subject / status / priority / assignee / tags change |
conversation.resolved | Conversation transitions to resolved |
message.created | New message posted (customer / agent / AI) |
contact.created | Contact created via API, widget, or form |
contact.updated | Contact fields change |
agent.escalated | AI agent escalates to a human |
agent.action_executed | Agent performs an automation action |
form.submitted | Public form receives a submission |
webhook.test | Sent by the test endpoint |
Wildcards supported: * (everything), conversation.* (prefix).
Test delivery
bashcurl -X POST https://api.rogeriq.com/api/v1/projects/prj_xxx/webhooks/wh_xxx/test \ -H "X-API-Key: riq_xxx"
json{ "data": { "delivered": true, "status_code": 200, "duration_ms": 143 }}
Test events skip the long-tail retry workflow and carry
event: "webhook.test" in the payload so receivers can filter.
Deliveries
bashcurl https://api.rogeriq.com/api/v1/projects/prj_xxx/webhooks/wh_xxx/deliveries \ -H "X-API-Key: riq_xxx"
Last 50 attempts with status code, duration, attempt number, and status
(delivered / failed).
Rotate secret
bashcurl -X POST https://api.rogeriq.com/api/v1/projects/prj_xxx/webhooks/wh_xxx/rotate-secret \ -H "X-API-Key: riq_xxx"
Returns { secret: "whsec_..." }. The old secret stops working
immediately — update your receiver first.
Delivery semantics
Inline retries (within the original request worker invocation):
| Attempt | Wait before |
|---|---|
| 1 | 0 |
| 2 | 1s |
| 3 | 5s |
| 4 | 30s |
Network errors, 5xx, 408, 425, and 429 trigger retry. 4xx (other than 408/425/429) bail out immediately — your endpoint said "no".
Long-tail retries (Cloudflare Workflow scheduled after inline exhaustion):
| Attempt | Wait before |
|---|---|
| 5 | 1m |
| 6 | 5m |
| 7 | 30m |
| 8 | 2h |
| 9 | 12h |
The workflow re-fetches the webhook row on each attempt, so disabling or rotating mid-retry behaves correctly.
After 10 consecutive failures the webhook is auto-disabled. Re-enable
with PATCH .../status=active.
Verifying signatures
Use @rogeriq/sdk:
tsimport { verifyWebhookSignature } from "@rogeriq/sdk";const rawBody = await req.text();const sig = req.headers.get("x-rogeriq-signature");if (!(await verifyWebhookSignature(rawBody, sig, process.env.WEBHOOK_SECRET!))) { return new Response("invalid signature", { status: 401 });}
Or manually with Node crypto:
tsimport crypto from "node:crypto";const expected = "sha256=" + crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex");if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { throw new Error("invalid signature");}
Headers on every delivery
| Header | Use |
|---|---|
X-RogerIQ-Event | Event type |
X-RogerIQ-Delivery | Unique delivery id — dedupe on this for at-least-once safety |
X-RogerIQ-Signature | sha256=<hex> |
X-RogerIQ-Timestamp | Unix seconds — reject if drift > 5 min |
X-RogerIQ-Attempt | 1-9 (1 = first try, 5-9 = long-tail) |
Rotate a webhook secret if it is logged, pasted into a client, or shared with a system that should no longer receive events.