webhooks developer reference
Subscribe to events from a clawborrator session. HMAC-SHA256-signed POSTs, exponential-backoff retries, at-least-once delivery. Same shape as Stripe webhooks.
Sign in to manage webhooks for your account.
…
1. catalog of event types
All events except agent.* are session-scoped: a webhook fires only for events on sessions the subscription's owning user can see (own + shared-into).
| type | fires when | payload |
|---|
2. subscribe
3. your subscriptions
| id | url | events | status | last fire |
|---|
deliveries — subscription
Most-recent first. Refresh after firing a test to see the row appear.
| id | type | status | attempts | queued | delivered/next |
|---|
4. verifying signatures
Hub signs every POST as X-Clawborrator-Signature: t=<unix>,v1=<hex> where v1 = hmac_sha256(secret, "<unix>." + raw_body). Reject deliveries older than ~5 min to defeat replay.
node.js
import crypto from 'node:crypto';
function verify(headerSig, rawBody, secret) {
const m = /^t=(\d+),v1=([a-f0-9]+)$/.exec(headerSig);
if (!m) return false;
const [, t, v1] = m;
if (Math.abs(Date.now()/1000 - Number(t)) > 300) return false;
const expected = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`).digest('hex');
// constant-time compare
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
python
import hmac, hashlib, time, re
def verify(header_sig: str, raw_body: bytes, secret: str) -> bool:
m = re.fullmatch(r"t=(\d+),v1=([a-f0-9]+)", header_sig)
if not m: return False
t, v1 = m.group(1), m.group(2)
if abs(time.time() - int(t)) > 300: return False
mac = hmac.new(secret.encode(), f"{t}.".encode() + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, v1)
express handler
app.post('/hub-webhook', express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.header('x-clawborrator-signature');
if (!verify(sig, req.body, SECRET))
return res.status(401).send('bad signature');
const { eventId, type, ts, payload } = JSON.parse(req.body.toString());
// dedup on eventId — retries reuse it
if (alreadyProcessed(eventId)) return res.sendStatus(200);
handle(type, payload);
res.sendStatus(200);
});
5. retry behavior
| attempt | delay | cumulative |
|---|---|---|
| 1 | immediate | 0 |
| 2 | +30s | 30s |
| 3 | +2m | 2m 30s |
| 4–7 | +5m each | up to 22m 30s |
Any 2xx accepts. Any 4xx (except 408 / 429) is permanent — dead-lettered immediately, no retry. 5xx / 408 / 429 / timeouts retry. After 7 dead deliveries in a row the subscription itself flips to unhealthy and stops getting new events.
6. local development with ngrok
# expose your local server
ngrok http 3000
# https://abc-123.ngrok-free.app
# subscribe
curl -X POST https://next.clawborrator.com/api/v1/webhooks \
-H "Authorization: Bearer $CLAW_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://abc-123.ngrok-free.app/hub-webhook",
"events": ["chat.event","permission.requested"]
}'
# fire a synthetic test
curl -X POST https://next.clawborrator.com/api/v1/webhooks/<id>/test \
-H "Authorization: Bearer $CLAW_TOKEN"