Webhooks
krabs delivers signed, retried, at-least-once webhooks for every event your agents care about.
Subscribing
From the dashboard, or CLI:
krabs webhooks create \ --url https://yourapp.com/krabs \ --events "deal.*,contact.upsert"
Output:
webhook_id: whk_01HGZ9X4QY8M2N7P3R5T6V8W
url: https://yourapp.com/krabs
events: deal.*, contact.upsert
secret: whsec_2v8x4n6q1jpz9w8x1y0c5b6d4f8g
↑ copy now, you will not see this againThe signing secret is shown once. krabs stores only a hash and uses the plaintext to sign outgoing payloads.
Event types
Patterns support * as a single-segment wildcard. deal.* matches deal.created and deal.stage_changed but not deal.line.added; use deal.** for the latter.
| event | fires when |
|---|---|
contact.created | a new contact row is written |
contact.updated | any field on a contact changes |
deal.created | a new deal is opened |
deal.stage_changed | a deal moves between pipeline stages |
task.completed | a task transitions to done |
interaction.received | an inbound message lands on any channel |
Payload shape
Every delivery is one JSON object. The data field carries the full record after the change; the actor field names the agent or human that caused it.
{
"id": "evt_01HG7Z4X9Q8M2N7P3R5T6V8W",
"type": "deal.stage_changed",
"created_at": "2026-05-16T14:22:08Z",
"data": {
"id": "dl_01HG…",
"name": "Acme Q3 expansion",
"stage": "negotiation",
"previous_stage": "proposal",
"amount": 48000,
"currency": "USD"
},
"actor": {
"kind": "agent",
"id": "agent_drafts",
"label": "drafts"
},
"run_id": "run_01HG7Z4X9Q8M2N7P3R5T6V8W"
}Signing & verification
krabssigns every payload with HMAC-SHA256 using the webhook’s secret. The signature lands in X-Krabs-Signature as t=<timestamp>,v1=<hex>. Verify in Node.js:
import crypto from "node:crypto";
export function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=") as [string, string])
);
const t = parts.t;
const sig = parts.v1;
if (!t || !sig) return false;
const signed = `${t}.${rawBody}`;
const expected = crypto
.createHmac("sha256", secret)
.update(signed)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(sig, "hex");
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}Reject the request if the signature does not verify or the timestamp is more than five minutes off wall-clock.
Retries
Failed deliveries retry with exponential backoff: +30s, +5m, +25m. Three attempts total, capped at thirty minutes. After the third failure the delivery is marked failed and surfaced in the dashboard.
Replay
Every delivery is logged. Replay any delivery — failed or successful — from the dashboard or CLI:
krabs webhooks replay dlv_01HG7Z4X9Q8M2N7P3R5T6V8W
Replay sends the original payload with the original signature, plus a new X-Krabs-Delivery-Id header so your handler can dedupe. Replayed events do not count against retry budgets.