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

EventFires when
memory.ingest.completedA document finished indexing
memory.ingest.dlp_flaggedThe perimeter masked something
memory.ingest.auto_routedContent was classified — carries needs_review
memory.entity.createdA new entity entered the graph
memory.entity.contradiction_detectedTwo sources disagreed
memory.entity.contradiction_resolvedSomeone resolved a disagreement
memory.health.assessedA health score was computed
Was this page helpful?
Report an issue

On this page