1. Poll the request, with a wait
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.
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.
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.
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;
}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.