Webhooks
Stop polling. Receive signed events when state changes.
CiteFlow can push events to an HTTPS endpoint of your choice. Configure one or more endpoints at /dashboard/api/webhooks.
Up to 5 endpoints per workspace. Each endpoint subscribes to one or more event types and gets its own signing secret.
Event catalog
| Type | Fires when |
|---|---|
audit.completed | Audit reaches terminal complete state. |
audit.failed | Audit reaches terminal failed state. |
audit.cancelled | Audit reaches terminal cancelled state (via POST /audit/{id}:cancel). |
balance.low | Balance drops below low_balance_threshold. Debounced 24h per threshold crossing. |
Envelope
Every event posts JSON with a stable envelope:
{
"id": "evt_01jbq6t8tjk0vfgz5gd9p4d4qa",
"type": "audit.completed",
"created_at": "2026-05-27T14:32:54.622Z",
"data": { "...": "event-specific payload" }
}audit.* data
{
"audit_id": "vH4kZcLm9YxQrTpA2gWb",
"status": "complete",
"type": "seo",
"url": "https://example.com",
"scores": {
"overall": 76, "seo": 81, "aeo": null, "geo": null
},
"failure_reason": null,
"credits": { "charged": 80, "refunded": 0 },
"created_at": "...",
"completed_at": "..."
}scores is null on audit.failed and audit.cancelled;
failure_reason is null on audit.completed.
balance.low data
{
"balance": 320,
"balance_usd": "$3.20",
"threshold": 500,
"recommended_topup_tier": "starter"
}Headers CiteFlow sends
POST /your/webhook HTTP/1.1
Content-Type: application/json
X-CiteFlow-Webhook-Id: evt_01jbq6t8tjk0vfgz5gd9p4d4qa
X-CiteFlow-Webhook-Timestamp: 1748400000
X-CiteFlow-Webhook-Signature: v1=base64(hmac-sha256(secret, "evt_…1748400000.<body>"))Verifying the signature
The signed string is ${id}.${timestamp}.${rawBody}. The signature
header may carry multiple comma-separated v1= values during the
7-day grace period after a secret rotation.
Node.js
import crypto from 'node:crypto';
function verify(rawBody: string, headers: Record<string, string>, secret: string) {
const id = headers['x-citeflow-webhook-id'];
const ts = headers['x-citeflow-webhook-timestamp'];
const sig = headers['x-citeflow-webhook-signature'];
if (!id || !ts || !sig) return false;
// Reject anything older than 5 minutes to defeat replay attacks.
if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${id}.${ts}.${rawBody}`)
.digest('base64');
return sig
.split(',')
.some((part) =>
crypto.timingSafeEqual(
Buffer.from(part.replace('v1=', '')),
Buffer.from(expected),
),
);
}Python
import hmac, hashlib, base64, time
def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
id_ = headers['x-citeflow-webhook-id']
ts = headers['x-citeflow-webhook-timestamp']
sig = headers['x-citeflow-webhook-signature']
if abs(int(time.time()) - int(ts)) > 300:
return False
signed = f"{id_}.{ts}.".encode() + raw_body
expected = base64.b64encode(
hmac.new(secret.encode(), signed, hashlib.sha256).digest()
).decode()
for part in sig.split(','):
if hmac.compare_digest(part.replace('v1=', ''), expected):
return True
return FalseBoth functions must read the raw body bytes before any JSON parsing — signature is computed over the literal bytes CiteFlow sent.
Delivery & retries
- 5-second HTTP timeout per attempt.
- Retries: 5 attempts over ~24 hours with exponential back-off.
- After 10 consecutive failures, the endpoint is auto-disabled and an email goes to the workspace contact.
- Concurrency cap: 3 simultaneous deliveries per endpoint.
Replay
curl -X POST \
"https://www.citeflow.io/api/v1/webhooks/deliveries/wd_…/replay" \
-H "Authorization: Bearer ckf_…"Re-fires the original payload with a fresh timestamp + signature. Use when your endpoint was down during the retry window or you want to reprocess events without touching CiteFlow.
409 WEBHOOK_ENDPOINT_DISABLED if the endpoint is currently disabled
— re-enable from the dashboard first.
Secret rotation
Click Rotate secret in the dashboard. The old secret remains valid for 7 days; signatures during the grace window carry both:
X-CiteFlow-Webhook-Signature: v1=<old>,v1=<new>Your verifier should accept either.