Webhooks
Register endpoints, verify signatures and handle redelivery correctly.
Chordian posts events to an HTTPS endpoint you register.
Register
POST/api/v1/webhooks
curl https://api.uni-flow.ai/api/v1/webhooks \
-H "x-api-key: $CHORDIAN_API_KEY" \
-H "content-type: application/json" \
-d '{
"url": "https://example.com/hooks/chordian",
"events": ["memory.ingest.completed", "memory.entity.contradiction_detected"]
}'The signing secret is returned once, in this response.
Payload
{
"event_id": "evt_9f2c8a1e4b7d",
"event": "memory.ingest.completed",
"occurred_at": "2026-07-30T14:22:05.184Z",
"scope": {
"tenant_id": "customer-abc123",
"workspace_id": "wsp_a1b2c3d4",
"memory_id": "csmbx-wsp_a1b2c3d4"
},
"payload": {
"doc_id": "ing_9f2c8a1e",
"vault_id": "v_sales"
}
}Verify before you trust
An unverified webhook endpoint is an unauthenticated write into your system.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody: string, signature: string, secret: string) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
// Length check first — timingSafeEqual throws on a length mismatch.
return a.length === b.length && timingSafeEqual(a, b);
}Verify against the raw body, before JSON parsing. Re-serialising a parsed body changes the bytes and the signature will not match — this is the single most common integration bug here.
Redelivery
Failures are retried at 1s, 2s, 4s, 8s and 30s, then dead-lettered.
Two consequences worth designing for:
You will see duplicates. Key on event_id and make reprocessing a no-op.
Slow is treated as failing. Acknowledge with 2xx immediately and do the work
asynchronously, or your latency becomes a retry storm.
export async function POST(request: Request) {
const raw = await request.text();
const signature = request.headers.get('x-chordian-signature') ?? '';
if (!verify(raw, signature, process.env.WEBHOOK_SECRET!)) {
return new Response('invalid signature', { status: 401 });
}
const event = JSON.parse(raw);
if (await alreadyProcessed(event.event_id)) {
return new Response(null, { status: 200 });
}
void enqueue(event); // do the work off the request path
return new Response(null, { status: 200 });
}Events
| Event | Fires when |
|---|---|
memory.ingest.completed | A document finished indexing |
memory.ingest.dlp_flagged | The perimeter masked something |
memory.ingest.auto_routed | Content was classified — carries needs_review |
memory.entity.created | A new entity entered the graph |
memory.entity.contradiction_detected | Two sources disagreed |
memory.entity.contradiction_resolved | Someone resolved a disagreement |
memory.health.assessed | A health score was computed |
Was this page helpful?Report an issue