Skip to content
Start a Dossier
Developers

Following long work: events and webhooks

A dossier takes 20–30 minutes; a Lode CLI run, a minute or three. Three ways to know when something moved, from simplest to most hands-off.

1. Poll the request, with a wait

Long-poll one request
curl https://api.codossier.com/v1/requests/01a1… \
  -H "Authorization: Bearer cdk_live_<id>_<secret>" \
  -H "Prefer: wait=60"

The call returns as soon as the request changes, or after 60 seconds with the current state. next.poll_after_seconds says how soon to ask again. This is what agents do.

2. The events feed

One stream for the whole office: every request status change, in order, each with a cursor. Keep the last cursor and ask for what came after it; with Prefer: wait=N the call holds until there is something new. Nothing is ever lost — the feed is the ledger itself, read by its row version.

Tail the feed
curl "https://api.codossier.com/v1/events?after=786873&limit=100" \
  -H "Authorization: Bearer cdk_live_<id>_<secret>" \
  -H "Prefer: wait=60"

{
  "events": [
    { "schema": "codossier.event/1.0", "cursor": "786890", "request": "01a1…", "intent": "open_matter", "matter": "d2d9…",
      "status": "completed", "stage": { "code": "ready", "label": "ready" }, "updated_at": "2026-10-08T19:01:40Z",
      "link": "https://api.codossier.com/v1/requests/01a1…" }
  ],
  "cursor": "786890",
  "next": { "poll_after_seconds": 30 }
}

3. Webhooks

The office's designated user registers endpoints on the Developers page (https; the host must be on the office's delivery list) and chooses the events: request.completed, request.needs_input, request.failed, request.working, or *. We POST one envelope per event — ids, status and stage, never matter content — and retry on failure: after 1 minute, 5, 30, 2 hours, then every 6 hours for a day. Answer 2xx to acknowledge; 410 to tell us to stop.

A delivery
POST https://hooks.yourfirm.com/codossier
Codossier-Event: request.completed
Codossier-Event-Id: 3f2c…
Codossier-Delivery-Attempt: 1
Codossier-Signature: t=1760000000, v1=4b9d…

{
  "schema": "codossier.webhook/1.0",
  "id": "3f2c…", "event": "request.completed",
  "request": "01a1…", "intent": "open_matter", "matter": "d2d9…",
  "status": "completed", "stage": { "code": "ready", "label": "ready" },
  "occurred_at": "2026-10-08T19:01:40Z", "cursor": "786890",
  "link": "https://api.codossier.com/v1/requests/01a1…"
}

Verify the signature

v1 is HMAC-SHA256 over {t}.{raw body} with the endpoint's secret, hex-encoded. Compute it over the exact bytes you received and compare in constant time; reject if t is older than five minutes. For 24 hours after you rotate the secret, deliveries also carry v0 signed with the previous one, so a rotation never drops a delivery.

Node
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, signatureHeader, rawBody) {
  const parts = Object.fromEntries(signatureHeader.split(",").map((kv) => kv.trim().split("=")));
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return ok && fresh;
}
Which one?

An agent in a conversation: poll with a wait. A system that runs all day: the events feed, one cursor in a database. A system that must not poll: webhooks, with the feed as the backstop for anything missed.