Preview — not for production reliance
Documentation

Docs API reference

Changes

Public lifecycle feed for relying parties.

1 operation. See the API overview for authentication, errors, idempotency and caching rules that apply to all of them.

Public lifecycle change feed

GET/v1/changes

Authentication
Public, no credentials
Operation ID
listChanges

The ordered feed of public lifecycle events: issuer binding activation, rotation, resumption, suspension, withdrawal and expiry; delegation activation and ending; and claims moving into or out of verified. Pending steps are never published.

The feed is a hint, never a credential. Treat every event as "re-resolve this subject" and use GET /v1/resolve as the authority. Delivery is at-least-once: deduplicate by event id, and persist next_cursor only after the page is processed. Poll with the returned next_cursor (an empty page returns the same cursor position). Events are retained for 90 days; a cursor older than the retention watermark answers 410 with the current head cursor so the consumer can re-resolve every trust domain it depends on and resume.

cursor and limit may each be given at most once.

Cursor. Opaque to clients: base64url (unpadded) of e<epoch>.s<seq>, where <epoch> is the 16-hex-digit identity of the feed's database (regenerated by a restore) and <seq> the last event position read. Only the canonical encoding is accepted (decoding and re-encoding must reproduce the exact input, at most 96 characters); anything else is 400. A cursor answers 410 (with the current head cursor) when it is older than the retention watermark, comes from another database epoch, or points past the current head (a restore to an older state, or a forged cursor).

Scope. Binding events are only published for bindings of an environment this deployment publishes (a production feed never names test bindings). Claim events appear only when a claim moves into or out of verified.

Rate limit. 60 requests per minute per client IP, sliding window, on every response of this route; over the limit is 429 rate_limited.

Parameters

NameInRequiredTypeDescription
cursorqueryNostringThe next_cursor of the previous page. Omit (or leave empty) to start from the oldest retained event.
at most 96 characters; pattern ^[A-Za-z0-9_-]*$
limitqueryNointegerMaximum events per page.
1 to 500; default 100

Responses

StatusDescriptionBody
200One page of events in feed order.
Headers: Cache-Control: public, max-age=10
application/json ChangeFeedPage
400invalid_request. Malformed JSON, an invalid or unknown field (see field_errors), a missing or malformed Idempotency-Key, or a bad query. Also used with status 413 (body over 64 KiB) and 415 (wrong Content-Type).application/json Error
410The cursor is older than the 90-day retention, belongs to another database epoch, or is ahead of the feed's head. Re-resolve every trust domain you depend on, then resume from next_cursor (the current head). Never cached.
Headers: Cache-Control: no-store
application/json CursorExpired
429rate_limited. A per-client or per-record limit was reached; retry later.application/json Error
503unavailable (temporarily unavailable; also a feature that is not configured in this deployment) or provider_unavailable (an identity provider is not configured). Both are 503. During maintenance (for example a database recovery) every mutating request answers unavailable "The directory is under maintenance; try again later." Reads keep working.application/json Error

200 example

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",
        "effective_at": "2026-10-05T11:58:12.000Z",
        "binding_version": 1,
        "bundle_sha256": "q7Yd3nXk0aV2pLw9sT4uHc8BzE1mR6fJ5gN0oIxK2tA",
        "expires_at": "2027-10-05T11:58:12.000Z",
        "profile": "spiffe-x509",
        "workload_scope": [
          "/agent/checkout"
        ]
      }
    }
  ],
  "next_cursor": "ZTNmYTg1ZjY0NTcxNzQ1NjIuczQyMQ",
  "has_more": false
}

400 example

json
{
  "code": "invalid_request",
  "message": "Invalid organization.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
  "field_errors": [
    {
      "field": "legal_name",
      "message": "must be 1-200 characters"
    }
  ]
}

410 example

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"
}

429 example

json
{
  "code": "rate_limited",
  "message": "Too many requests.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

503 example

json
{
  "code": "unavailable",
  "message": "Temporarily unavailable.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}