Preview — not for production reliance
Documentation

Docs For relying parties

Consuming the change feed

Follow GET /v1/changes to learn when to re-resolve: cursors of the form e<epoch>.s<seq>, at-least-once delivery, and the 410 resync.

GET /v1/changes is the public, ordered feed of lifecycle events. It tells you when to resolve again. It is a hint, never a credential: the signed assertion from /v1/resolve is the only authority, and nothing in the feed changes what you accept.

The response

json
{
  "events": [
    {
      "id": "evt_5a8c1f4h7k0m3p6s9v2x5z8b1d",
      "event_type": "binding.activated",
      "at": "2026-10-05T11:58:12.000Z",
      "subject": { "trust_domain": "agents.payments.example.com" },
      "data": {
        "binding_id": "bnd_9r2t5w8z1b4d7f0h3k6n9q2s5v",
        "organization_id": "org_7k3m9q2x8d5f1h4j6n0p2r4t8v",
        "state": "active",
        "binding_version": 1,
        "bundle_sha256": "q7Yd3nXk0aV2pLw9sT4uHc8BzE1mR6fJ5gN0oIxK2tA"
      }
    }
  ],
  "next_cursor": "ZTNmYTg1ZjY0NTcxNzQ1NjIuczQyMQ",
  "has_more": false
}

Query parameters: cursor (omit it to start from the beginning of the retained feed) and limit (default 100, at most 500). Each may be given once. Responses are Cache-Control: public, max-age=10, and the route allows 60 requests per minute per client IP (429 rate_limited beyond that).

Event types

event_type subject Meaning
binding.activated trust_domain The first published version is active
binding.rotated trust_domain A new version is served (rotation_kind; overlap_until for an add step)
binding.suspended, binding.resumed trust_domain Temporarily not endorsed, or endorsed again
binding.withdrawn trust_domain No longer endorsed (terminal)
binding.expired trust_domain Passed expires_at without renewal (terminal)
delegation.activated domain (the child) A delegation is in force
delegation.withdrawn, delegation.expired, delegation.revoked domain (the child) It ended; bindings depending on it left service
claim.state_changed domain A claim moved into or out of verified. The affected bindings also get their own events.

data carries only public, allow-listed fields; fields that do not apply are omitted. Pending steps of claims and registrations are never published, and a production feed never names test bindings.

How to consume it

  1. Start without a cursor, or from the next_cursor you saved last time.
  2. Treat every event as "re-resolve this subject". Delivery is at least once, so deduplicate by event id.
  3. Save next_cursor only after the page is processed. Keep going while has_more is true; an empty page returns the same cursor position, so poll again later from there.
  4. A page can hold fewer events than limit even when has_more is true: the cursor advances past rows that are not public.

Events come in commit order. Paging by cursor never skips or repeats an event, even while new ones are being written.

Cursors

A cursor is opaque to you, but for the record it is the unpadded base64url encoding of e<epoch>.s<seq>:

  • <epoch> is the 16-hex-digit identity of the feed's database. Restoring the database from a backup generates a new epoch.
  • <seq> is the position of the last event read.

Only the exact canonical value the feed returned is accepted (decoding and re-encoding must reproduce it, at most 96 characters). Anything else is 400. Do not build or edit cursors.

How fast a withdrawal takes effect Timeline. At t equals 0 the withdrawal commits. From then on, resolve answers withdrawn with no assertion, and a public binding.withdrawn event appears in the change feed so pollers drop the trust domain. An assertion read just before the commit still verifies until its own expiry: at most 300 seconds, plus 30 seconds of verifier clock skew, so nothing verifies after 330 seconds. Resolve answers withdrawn, no assertion, from the next read Change feed binding.withdrawn published; pollers drop the domain t = 0: commit 300 s 330 s Old assertions an assertion read just before t = 0 still verifies until its exp none left Worst case: 300 s assertion lifetime + 30 s verifier clock skew = 330 s after the commit. Re-resolve after exp; never cache longer.
The change feed tells you to resolve again; the 330-second bound holds even if you never poll.

The 410 resync

The feed answers 410 with code: "cursor_expired" when your cursor:

  • is older than the 90-day retention window,
  • comes from another database epoch (the directory was restored from a backup), or
  • points past the current head (a restore to an older state, or a forged cursor).
json
{
  "code": "cursor_expired",
  "message": "This cursor is older than the 90-day retention of the change feed.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
  "resync": "Re-resolve every trust domain you depend on, then resume from next_cursor",
  "next_cursor": "ZTNmYTg1ZjY0NTcxNzQ1NjIuczQ4OTA"
}

Do what resync says: re-resolve every trust domain you depend on, then continue from the next_cursor in the 410 body, which is the current head. The 410 is never cached.

A minimal poller

ts
let cursor = loadSavedCursor(); // undefined on first run
const seen = new Set<string>(); // persist this alongside the cursor in a real consumer
for (;;) {
  const url = new URL("https://svid.io/v1/changes");
  if (cursor) url.searchParams.set("cursor", cursor);
  url.searchParams.set("limit", "500");
  const res = await fetch(url);
  if (res.status === 410) {
    const body = await res.json();
    await reResolveEverything();
    cursor = body.next_cursor;
    saveCursor(cursor);
    continue;
  }
  if (res.status === 429) { await sleep(60_000); continue; }
  if (!res.ok) { await sleep(10_000); continue; }
  const page = await res.json();
  for (const e of page.events) {
    if (seen.has(e.id)) continue;
    seen.add(e.id);
    await reResolve(e.subject.trust_domain ?? e.subject.domain);
  }
  cursor = page.next_cursor;
  saveCursor(cursor); // only after the page is processed
  if (!page.has_more) await sleep(10_000);
}

For a domain subject, re-resolve the trust domains you depend on under that domain: a claim or delegation change can affect several bindings.