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
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
cursor | query | No | string | The 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_-]*$ |
limit | query | No | integer | Maximum events per page. 1 to 500; default 100 |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | One page of events in feed order. Headers: Cache-Control: public, max-age=10 | application/json ChangeFeedPage |
| 400 | invalid_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 |
| 410 | The 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 |
| 429 | rate_limited. A per-client or per-record limit was reached; retry later. | application/json Error |
| 503 | unavailable (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
{
"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
{
"code": "invalid_request",
"message": "Invalid organization.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "legal_name",
"message": "must be 1-200 characters"
}
]
}
410 example
{
"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
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}