Docs API reference
Issuer bindings
Issuer registration, the issuer-control ceremony, rotation and withdrawal.
9 operations. See the API overview for authentication, errors, idempotency and caching rules that apply to all of them.
Register an issuer binding
POST/v1/issuer-bindings
Registers an exact trust domain, its SPIFFE bundle endpoint, the initial bundle (the exact JSON text, as a string) and
the workload scope. Creates the binding as draft with version 1 proposed; the response carries the issuer-control
ceremony instructions. Owner or issuer administrator (a domain administrator cannot), human, step-up, against a
verified claim whose reviewed authority includes authorize_issuer.
The trust domain must be the claim's domain or a DNS name under it (mapping: dns), or an explicitly mapped single-label
name (mapping: explicit). Wildcards, spiffe:// prefixes, public suffixes and special-use names are refused. The bundle
endpoint must be https on port 443, a DNS host at or under the verified domain, at most 2048 characters. bundle is at
most 65536 bytes with at most 32 keys. Registration is never refused because another organization used a key (that would
be first-come-first-served); overlaps are recorded for review. A trust domain that already has a live binding answers the
deliberately uninformative 409 conflict "This trust domain is not available."
Production deployments accept only environment: production. At most 20 issuer registrations per organization in a rolling 30
days, counting every status (429 rate_limited).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
Idempotency-Key | header | Yes | string | Unique key for this logical mutation (1 to 255 characters of A-Z a-z 0-9 . _ : -). Replays within 24 hours return the original outcome.1 to 255 characters; pattern ^[A-Za-z0-9._:-]+$example 7f3c1b52-9d44-4a0e-8f21-0a6e5b9d3c77 |
x-art-csrf | header | Yes | string | The csrf_token from GET /auth/session. Required on every session-authenticated mutation.example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA |
Request body
application/json, required: CreateIssuerBindingRequest
| Field | Type | Required | Description |
|---|---|---|---|
claim_id | ClaimId | Yes | |
trust_domain | TrustDomain | Yes | |
profile | "spiffe-x509" | Yes | The only supported profile. Needs at least one x509-svid key. |
bundle_endpoint | string (uri) | Yes | https, port 443, DNS host (no IP literal), no credentials or fragment. Must be at or under the verified domain.at most 2048 characters |
bundle | string | Yes | The exact SPIFFE bundle JSON text (a string, not an object), UTF-8, no BOM, at most 64 KiB and 32 keys. The directory stores and hashes these exact bytes. at most 65536 characters |
workload_scope | WorkloadScope | Yes | |
environment | one of "production", "test" | No | Production deployments accept only production.default "production" |
mapping | one of "dns", "explicit" | No | dns: the trust domain is the claim's domain or a DNS name under it. explicit: a single-label trust domain (no dots) mapped by a reviewer.default "dns" |
Example
{
"claim_id": "clm_4c8e2g6k0m3q7s1w5y9a2d6f8h",
"trust_domain": "agents.payments.example.com",
"profile": "spiffe-x509",
"bundle_endpoint": "https://spire.payments.example.com/bundle",
"bundle": "{\"spiffe_sequence\":1,\"spiffe_refresh_hint\":300,\"keys\":[{\"use\":\"x509-svid\",\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU\",\"y\":\"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0\",\"x5c\":[\"MIIBszCCAVmgAwIBAgIUExampleOnlyNotARealCertificateExampleOnly=\"]}]}",
"workload_scope": [
"/agent/checkout",
"/agent/refunds"
],
"environment": "production",
"mapping": "dns"
}
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The draft binding and the ceremony instructions. | application/json BindingWithCeremony |
| 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 |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 409 | conflict. The record changed since you read it (stale expected_version), the action is not valid in the record's
current state, the Idempotency-Key was reused with a different request, or the same request is still in flight. | application/json Error |
| 422 | semantic_check_failed. The request is well formed but a policy or proof check failed (for example an issuer-control proof
did not verify, a challenge expired, or the organization is not yet verified). field_errors[].message is often a stable
reason code. | application/json Error |
| 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 |
201 example
{
"binding": {
"id": "bnd_9r2t5w8z1b4d7f0h3k6n9q2s5v",
"organization_id": "org_7k3m9q2x8d5f1h4j6n0p2r4t8v",
"claim_id": "clm_4c8e2g6k0m3q7s1w5y9a2d6f8h",
"delegation_id": null,
"trust_domain": "agents.payments.example.com",
"verified_domain": "payments.example.com",
"mapping": "dns",
"profile": "spiffe-x509",
"workload_scope": [
"/agent/checkout",
"/agent/refunds"
],
"environment": "production",
"required_approvals": 2,
"status": "draft",
"status_reason": null,
"version": 1,
"current_version": 0,
"expires_at": null,
"created_at": "2026-10-05T11:56:00.000Z",
"updated_at": "2026-10-05T11:56:00.000Z",
"endpoint_drift": false,
"endpoint_drift_at": null,
"rotation": null,
"versions": [
{
"version": 1,
"status": "proposed",
"endpoint": "https://spire.payments.example.com/bundle",
"bundle_sha256": "q7Yd3nXk0aV2pLw9sT4uHc8BzE1mR6fJ5gN0oIxK2tA",
"bundle_size": 412,
"keys": [
{
"use": "x509-svid",
"kty": "EC",
"crv": "P-256"
}
],
"created_at": "2026-10-05T11:56:00.000Z",
"approved_at": null,
"effective_at": null,
"endpoint_checked_at": null,
"overlap_until": null,
"superseded_by": null,
"rotation_of": null,
"overlap_seconds": null,
"rotation_kind": null,
"issuer_control": null,
"ceremony_components": []
}
],
"next_actions": [
"complete_issuer_control_ceremony"
]
},
"ceremony": {
"version": 1,
"bundle_sha256": "q7Yd3nXk0aV2pLw9sT4uHc8BzE1mR6fJ5gN0oIxK2tA",
"challenge": "POST https://svid.io/v1/issuer-bindings/bnd_9r2t5w8z1b4d7f0h3k6n9q2s5v/control-challenge issues a one-time nonce (24 h, shown once); GET shows its status. All proofs below use the SAME nonce.",
"required": [
"federation_endpoint",
"x509_svid",
"signed_statement (only if the bundle has jwt-svid keys)"
],
"methods": {
"federation_endpoint": "Publish at https://spire.payments.example.com/bundle the submitted bundle plus ONE extra JWK with kid art-challenge-<nonce>, then POST control-proof {\"method\":\"federation_endpoint\"}.",
"x509_svid": "For EVERY x509-svid CA in the bundle, issue an X.509-SVID for spiffe://agents.payments.example.com/art-challenge/<nonce>, then POST control-proof {\"method\":\"x509_svid\",\"svids\":[...]}.",
"signed_statement": "For EVERY jwt-svid key, sign {binding_id, version, bundle_sha256, nonce, iat}, then POST control-proof {\"method\":\"signed_statement\",\"statements\":[...]}."
},
"after": "When every required proof has passed, the binding moves to pending_review."
}
}
400 example
{
"code": "invalid_request",
"message": "Invalid organization.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "legal_name",
"message": "must be 1-200 characters"
}
]
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
409 example
{
"code": "conflict",
"message": "The record changed since you read it; reload and retry with the current version.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
422 example
{
"code": "semantic_check_failed",
"message": "Issuer-control proof failed (endpoint_http_404). 4 attempt(s) left on this challenge.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "method",
"message": "endpoint_http_404"
}
]
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Get an issuer binding
GET/v1/issuer-bindings/{id}
Any member (or a service token for this organization). The binding with its version history, the issuer-control evidence
summary of each version, any rotation in flight and suggested next_actions.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | BindingId | Issuer binding id. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The binding. | application/json BindingEnvelope |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | 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
{
"binding": {
"id": "bnd_9r2t5w8z1b4d7f0h3k6n9q2s5v",
"organization_id": "org_7k3m9q2x8d5f1h4j6n0p2r4t8v",
"claim_id": "clm_4c8e2g6k0m3q7s1w5y9a2d6f8h",
"delegation_id": null,
"trust_domain": "agents.payments.example.com",
"verified_domain": "payments.example.com",
"mapping": "dns",
"profile": "spiffe-x509",
"workload_scope": [
"/agent/checkout",
"/agent/refunds"
],
"environment": "production",
"required_approvals": 2,
"status": "active",
"status_reason": null,
"version": 6,
"current_version": 1,
"expires_at": "2027-10-05T11:58:12.000Z",
"created_at": "2026-10-05T11:56:00.000Z",
"updated_at": "2026-10-05T11:58:12.000Z",
"endpoint_drift": false,
"endpoint_drift_at": null,
"rotation": null,
"versions": [
{
"version": 1,
"status": "active",
"endpoint": "https://spire.payments.example.com/bundle",
"bundle_sha256": "q7Yd3nXk0aV2pLw9sT4uHc8BzE1mR6fJ5gN0oIxK2tA",
"bundle_size": 412,
"keys": [
{
"use": "x509-svid",
"kty": "EC",
"crv": "P-256"
}
],
"created_at": "2026-10-05T11:56:00.000Z",
"approved_at": "2026-10-05T11:58:12.000Z",
"effective_at": "2026-10-05T11:58:12.000Z",
"endpoint_checked_at": "2026-10-05T11:57:00.000Z",
"overlap_until": null,
"superseded_by": null,
"rotation_of": null,
"overlap_seconds": null,
"rotation_kind": null,
"issuer_control": {
"evidence_id": "evi_6d9g2j5m8q1t4w7z0c3f6h9k2n",
"result": "pass",
"observed_at": "2026-10-05T11:57:30.000Z",
"expires_at": "2026-11-04T11:57:30.000Z",
"method": "issuer_control_ceremony"
},
"ceremony_components": []
}
],
"next_actions": [
"rotate"
]
}
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Status of the control challenge
GET/v1/issuer-bindings/{id}/control-challenge
Human only; owner or issuer administrator. Side-effect free: shows the live challenge's status (never its nonce) and the
ceremony instructions. challenge is null when none is usable. The ceremony applies to a draft binding's newest proposed
version, or to a pending rotation of an active binding; otherwise 409.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | BindingId | Issuer binding id. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Challenge status. Headers: Cache-Control: no-store | application/json ControlChallengeStatus |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 409 | conflict. The record changed since you read it (stale expected_version), the action is not valid in the record's
current state, the Idempotency-Key was reused with a different request, or the same request is still in flight. | 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
{
"challenge": {
"id": "bch_8h1k4n7q0t3w6z9c2f5j8m1p4s",
"version": 1,
"bundle_sha256": "q7Yd3nXk0aV2pLw9sT4uHc8BzE1mR6fJ5gN0oIxK2tA",
"expires_at": "2026-10-06T11:57:00.000Z",
"attempts": 0,
"max_attempts": 5,
"nonce": null,
"value_redacted": true
},
"issue": "POST /v1/issuer-bindings/bnd_9r2t5w8z1b4d7f0h3k6n9q2s5v/control-challenge",
"ceremony": {
"version": 1,
"bundle_sha256": "q7Yd3nXk0aV2pLw9sT4uHc8BzE1mR6fJ5gN0oIxK2tA",
"challenge": "POST /control-challenge issues a one-time nonce.",
"required": [
"federation_endpoint",
"x509_svid"
],
"methods": {
"federation_endpoint": "Publish the bundle plus the challenge key at the endpoint.",
"x509_svid": "Issue a challenge X.509-SVID per CA."
},
"after": "When every required proof has passed, the binding moves to pending_review."
}
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
409 example
{
"code": "conflict",
"message": "The record changed since you read it; reload and retry with the current version.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Issue the control challenge
POST/v1/issuer-bindings/{id}/control-challenge
Human only; owner or issuer administrator. Issues (or replaces) the control challenge: a 256-bit nonce shown once
(24 hours, 5 attempts). The nonce is the same for every proof of the ceremony. At most 5 challenges per binding version (429).
Idempotent replays redact the nonce.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | BindingId | Issuer binding id. |
Idempotency-Key | header | Yes | string | Unique key for this logical mutation (1 to 255 characters of A-Z a-z 0-9 . _ : -). Replays within 24 hours return the original outcome.1 to 255 characters; pattern ^[A-Za-z0-9._:-]+$example 7f3c1b52-9d44-4a0e-8f21-0a6e5b9d3c77 |
x-art-csrf | header | Yes | string | The csrf_token from GET /auth/session. Required on every session-authenticated mutation.example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The challenge with its one-time nonce. Headers: Cache-Control: no-store | application/json ControlChallengeIssued |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 409 | conflict. The record changed since you read it (stale expected_version), the action is not valid in the record's
current state, the Idempotency-Key was reused with a different request, or the same request is still in flight. | application/json Error |
| 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 |
201 example
{
"challenge": {
"id": "bch_8h1k4n7q0t3w6z9c2f5j8m1p4s",
"version": 1,
"bundle_sha256": "q7Yd3nXk0aV2pLw9sT4uHc8BzE1mR6fJ5gN0oIxK2tA",
"expires_at": "2026-10-06T11:57:00.000Z",
"max_attempts": 5,
"nonce": "Hq4Zm9Xk2Lw6Vp0Nb3Ts7Ra1Ey5Dc8Jf",
"challenge_kid": "art-challenge-Hq4Zm9Xk2Lw6Vp0Nb3Ts7Ra1Ey5Dc8Jf"
},
"ceremony": {
"version": 1,
"bundle_sha256": "q7Yd3nXk0aV2pLw9sT4uHc8BzE1mR6fJ5gN0oIxK2tA",
"challenge": "POST /control-challenge issues a one-time nonce.",
"required": [
"federation_endpoint",
"x509_svid"
],
"methods": {
"federation_endpoint": "Publish the bundle plus the challenge key at the endpoint.",
"x509_svid": "Issue a challenge X.509-SVID per CA."
},
"after": "When every required proof has passed, the binding moves to pending_review."
}
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
409 example
{
"code": "conflict",
"message": "The record changed since you read it; reload and retry with the current version.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Submit an issuer-control proof
POST/v1/issuer-bindings/{id}/control-proof
Human only; owner or issuer administrator; step-up. Submits one component of the issuer-control ceremony for the exact bundle digest of the current version, under the live challenge nonce:
federation_endpoint: no extra fields. The directory fetches the endpoint through its scanner and compares the key set (the submitted bundle plus the challenge key).x509_svid:svids, an ordinary X.509-SVID chain forspiffe://<trust domain>/art-challenge/<nonce>for every x509-svid CA in the bundle (proof of possession without exporting a CA key).signed_statement:statements, one compact JWS per jwt-svid key over{binding_id, version, bundle_sha256, nonce, iat}. Required only if the bundle has jwt-svid keys.
A failing proof is 422 semantic_check_failed and burns one of 5 attempts. When every required component has passed, one
issuer_control evidence row is written, the nonce is consumed and the binding moves draft -> pending_review (for a
rotation the version awaits review while the binding keeps serving). A proof already recorded for the challenge is 409.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | BindingId | Issuer binding id. |
Idempotency-Key | header | Yes | string | Unique key for this logical mutation (1 to 255 characters of A-Z a-z 0-9 . _ : -). Replays within 24 hours return the original outcome.1 to 255 characters; pattern ^[A-Za-z0-9._:-]+$example 7f3c1b52-9d44-4a0e-8f21-0a6e5b9d3c77 |
x-art-csrf | header | Yes | string | The csrf_token from GET /auth/session. Required on every session-authenticated mutation.example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA |
Request body
application/json, required: ControlProofRequest
| Field | Type | Required | Description |
|---|---|---|---|
method | one of "federation_endpoint", "x509_svid", "signed_statement" | Yes | |
svids | array of string | No | Required for x509_svid and forbidden otherwise. One PEM chain (leaf first) per x509-svid CA in the bundle. |
statements | array of string | No | Required for signed_statement and forbidden otherwise. One compact JWS per jwt-svid key (1 to 32).1 to 32 items |
expected_version | integer | No | If sent, must equal the binding's version. |
Example: Federation endpoint proof
{
"method": "federation_endpoint"
}
Example: Challenge SVIDs
{
"method": "x509_svid",
"svids": [
"-----BEGIN CERTIFICATE-----\nMIIBszCCAVmgAwIBAgIUExampleOnly\n-----END CERTIFICATE-----\n"
]
}
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The component passed; ceremony_status says whether the ceremony is complete. | application/json ControlProofResult |
| 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 |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 409 | conflict. The record changed since you read it (stale expected_version), the action is not valid in the record's
current state, the Idempotency-Key was reused with a different request, or the same request is still in flight. | application/json Error |
| 422 | semantic_check_failed. The request is well formed but a policy or proof check failed (for example an issuer-control proof
did not verify, a challenge expired, or the organization is not yet verified). field_errors[].message is often a stable
reason code. | 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
{
"binding": {
"id": "bnd_9r2t5w8z1b4d7f0h3k6n9q2s5v",
"organization_id": "org_7k3m9q2x8d5f1h4j6n0p2r4t8v",
"claim_id": "clm_4c8e2g6k0m3q7s1w5y9a2d6f8h",
"delegation_id": null,
"trust_domain": "agents.payments.example.com",
"verified_domain": "payments.example.com",
"mapping": "dns",
"profile": "spiffe-x509",
"workload_scope": [
"/agent/checkout"
],
"environment": "production",
"required_approvals": 2,
"status": "pending_review",
"status_reason": null,
"version": 2,
"current_version": 0,
"expires_at": null,
"created_at": "2026-10-05T11:56:00.000Z",
"updated_at": "2026-10-05T11:57:30.000Z",
"endpoint_drift": false,
"endpoint_drift_at": null,
"rotation": null,
"versions": [],
"next_actions": [
"await_review"
]
},
"component": {
"id": "evi_6d9g2j5m8q1t4w7z0c3f6h9k2n",
"kind": "issuer_surface",
"result": "pass",
"method": "federation_endpoint",
"endpoint": "https://spire.payments.example.com/bundle",
"bundle_sha256": "q7Yd3nXk0aV2pLw9sT4uHc8BzE1mR6fJ5gN0oIxK2tA",
"version": 1
},
"ceremony_status": {
"challenge_id": "bch_8h1k4n7q0t3w6z9c2f5j8m1p4s",
"required": [
"issuer_surface",
"ca_possession"
],
"passed": [
"issuer_surface",
"ca_possession"
],
"complete": true
}
}
400 example
{
"code": "invalid_request",
"message": "Invalid organization.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "legal_name",
"message": "must be 1-200 characters"
}
]
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
409 example
{
"code": "conflict",
"message": "The record changed since you read it; reload and retry with the current version.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
422 example
{
"code": "semantic_check_failed",
"message": "Issuer-control proof failed (endpoint_http_404). 4 attempt(s) left on this challenge.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "method",
"message": "endpoint_http_404"
}
]
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Propose a new version of a draft binding
POST/v1/issuer-bindings/{id}/versions
Human only; owner or issuer administrator; step-up. For a draft binding only (after a rejection or quarantine, or to
replace a proposed bundle before the ceremony). Key material is never overwritten: new bytes get a new version. For an
active binding use POST /v1/issuer-bindings/{id}/rotate. expected_version must equal the binding's version.
bundle_endpoint defaults to the previous version's. At most 20 versions that never became active per binding per 90 days.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | BindingId | Issuer binding id. |
Idempotency-Key | header | Yes | string | Unique key for this logical mutation (1 to 255 characters of A-Z a-z 0-9 . _ : -). Replays within 24 hours return the original outcome.1 to 255 characters; pattern ^[A-Za-z0-9._:-]+$example 7f3c1b52-9d44-4a0e-8f21-0a6e5b9d3c77 |
x-art-csrf | header | Yes | string | The csrf_token from GET /auth/session. Required on every session-authenticated mutation.example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA |
Request body
application/json, required: CreateBindingVersionRequest
| Field | Type | Required | Description |
|---|---|---|---|
expected_version | integer | Yes | The binding's version. |
bundle | string | Yes | The exact SPIFFE bundle JSON text. at most 65536 characters |
bundle_endpoint | string (uri) | No | Defaults to the previous version's endpoint. at most 2048 characters |
Example
{
"expected_version": 2,
"bundle": "{\"spiffe_sequence\":2,\"keys\":[{\"use\":\"x509-svid\",\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU\",\"y\":\"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0\",\"x5c\":[\"MIIBszCCAVmgAwIBAgIUExampleOnlyNotARealCertificateExampleOnly=\"]}]}"
}
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The binding with the new proposed version, and its ceremony instructions. | application/json BindingWithCeremony |
| 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 |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 409 | conflict. The record changed since you read it (stale expected_version), the action is not valid in the record's
current state, the Idempotency-Key was reused with a different request, or the same request is still in flight. | application/json Error |
| 422 | semantic_check_failed. The request is well formed but a policy or proof check failed (for example an issuer-control proof
did not verify, a challenge expired, or the organization is not yet verified). field_errors[].message is often a stable
reason code. | application/json Error |
| 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 |
201 example
{
"binding": {
"id": "bnd_9r2t5w8z1b4d7f0h3k6n9q2s5v",
"organization_id": "org_7k3m9q2x8d5f1h4j6n0p2r4t8v",
"claim_id": "clm_4c8e2g6k0m3q7s1w5y9a2d6f8h",
"delegation_id": null,
"trust_domain": "agents.payments.example.com",
"verified_domain": "payments.example.com",
"mapping": "dns",
"profile": "spiffe-x509",
"workload_scope": [
"/agent/checkout"
],
"environment": "production",
"required_approvals": 2,
"status": "draft",
"status_reason": null,
"version": 3,
"current_version": 0,
"expires_at": null,
"created_at": "2026-10-05T11:56:00.000Z",
"updated_at": "2026-10-05T12:40:00.000Z",
"endpoint_drift": false,
"endpoint_drift_at": null,
"rotation": null,
"versions": [],
"next_actions": [
"complete_issuer_control_ceremony"
]
},
"ceremony": {
"version": 2,
"bundle_sha256": "Yt3Lw8Xk1Zq5Vp9Nb2Ms6Ra0Ey4Hc7Jf3Dg1Ta5PuKw",
"challenge": "POST /control-challenge issues a one-time nonce.",
"required": [
"federation_endpoint",
"x509_svid"
],
"methods": {
"federation_endpoint": "Publish the bundle plus the challenge key at the endpoint.",
"x509_svid": "Issue a challenge X.509-SVID per CA."
},
"after": "When every required proof has passed, the binding moves to pending_review."
}
}
400 example
{
"code": "invalid_request",
"message": "Invalid organization.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "legal_name",
"message": "must be 1-200 characters"
}
]
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
409 example
{
"code": "conflict",
"message": "The record changed since you read it; reload and retry with the current version.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
422 example
{
"code": "semantic_check_failed",
"message": "Issuer-control proof failed (endpoint_http_404). 4 attempt(s) left on this challenge.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "method",
"message": "endpoint_http_404"
}
]
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Preview a rotation (dry run)
POST/v1/issuer-bindings/{id}/rotation-preview
A dry run of POST /v1/issuer-bindings/{id}/rotate with the same body: it runs every check rotate runs, in the same
order, and writes nothing (no version, no stored bundle, no audit row, no event, no idempotency record). So it needs a session
and CSRF, but no step-up and no Idempotency-Key. Same roles as rotate (owner or issuer administrator, a person).
Bodies over 64 KB are 413; at most 30 previews per minute per person (429).
expected_version is optional here; when given it is checked.
Malformed input is 400 exactly as for rotate. Anything rotate would refuse (no shared authority, adding and removing at
once, a remove step before the overlap ends, an expired or inactive binding, lapsed representative authority, a stale
expected_version, ...) is reported in the body with 200: allowed: false and refusal carrying the code, HTTP status,
message and field errors rotate would answer with. When allowed, rotation gives the classification
(add / remove / renewal / emergency), the shared, added and removed authorities and the approvals required.
overlap_rules gives the overlap bounds in force for this deployment and the earliest time authorities can be removed.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | BindingId | Issuer binding id. |
x-art-csrf | header | Yes | string | The csrf_token from GET /auth/session. Required on every session-authenticated mutation.example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA |
Request body
application/json, required: RotationPreviewRequest
The body of RotateBindingRequest, with expected_version optional.
| Field | Type | Required | Description |
|---|---|---|---|
expected_version | integer | No | |
bundle | string | Yes | at most 65536 characters |
bundle_endpoint | string (uri) | No | at most 2048 characters |
overlap_seconds | integer | Yes | 330 to 604800 |
emergency | boolean | No | default false |
reason | string | No | 1 to 2000 characters |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The classification, or the refusal rotate would answer with. | application/json RotationPreview |
| 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 |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 413 | 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 |
| 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
{
"allowed": true,
"dry_run": true,
"refusal": null,
"rotation": {
"version": 7,
"rotation_of": 6,
"kind": "add",
"overlap_seconds": 86400,
"continuity": true,
"renewal": false,
"emergency": false,
"shared_authorities": [
"hQ2b9r7kM1x0vT4yP8sL3nC6wZ5dF2gJ9eA1uK7oI3E"
],
"added_authorities": [
"Zt5Kp2W8nX1cV7bM4qR9sD3fG6hJ0kL2aP5oI8uY1eT"
],
"removed_authorities": [],
"required_approvals": 1,
"bundle_sha256": "q3Jm0f4vX9n2bR7cT1yW8kP5sD6hL0aZ4eU2iO9gN1M",
"bundle_endpoint": "https://spiffe.payments.example.com/bundle",
"replaces_pending_version": null,
"overlap_until_if_activated_now": "2026-10-06T12:00:00.000Z"
},
"overlap_rules": {
"min_overlap_seconds": 330,
"max_overlap_seconds": 604800,
"min_add_overlap_seconds": 86400,
"served_overlap_until": null,
"earliest_remove_at": "2026-10-05T12:00:00.000Z",
"assertion_bound_seconds": 330
},
"next_steps": [
"submit POST /rotate with the same body and expected_version",
"complete the issuer-control ceremony for the new version",
"1 independent reviewer approval"
]
}
400 example
{
"code": "invalid_request",
"message": "Invalid organization.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "legal_name",
"message": "must be 1-200 characters"
}
]
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
413 example
{
"code": "invalid_request",
"message": "Invalid organization.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "legal_name",
"message": "must be 1-200 characters"
}
]
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Propose a rotation of an active binding
POST/v1/issuer-bindings/{id}/rotate
Human only; owner or issuer administrator; step-up. Proposes version N+1 for an active, unexpired binding (an expired
binding is never revived: 409, register it again). The binding keeps serving version N while the new version goes through
the same validation as registration and its own full issuer-control ceremony (a challenge SVID for every x509 authority
of the new bundle).
The assertion pins exactly one bundle digest, so there is no overlap between versions: the overlap lives inside the bundle, and rotating a CA is a two-step process. The server classifies the rotation by comparing the new bundle's x509-svid authorities (the CA certificates' public keys; jwt keys never count) with the served version's:
kind |
The new bundle | Approvals |
|---|---|---|
renewal |
Byte-identical to the served bundle. Re-proves control and recomputes expires_at. |
1 |
add |
Keeps every authority of the served version and adds new ones. Records overlap_until = activation + overlap_seconds on the new version. |
2 in a production deployment when it adds an x509-svid authority; otherwise 1 |
remove |
A strict subset of the served authorities (at least one kept), allowed only after the overlap in force has passed. The approving reviewer must not have approved the add step(s) it completes. | 1 |
emergency |
emergency: true with a reason: anything else (replace or drop authorities) takes effect at once. |
2 |
In a production deployment, any step whose new bundle contains an x509-svid authority that the served version lacks needs
two distinct reviewers (rotation.required_approvals is 2), so a new CA never becomes trusted on a single approval. Remove steps
and renewals stay at 1, and the remove step's reviewer must not have approved the add step it completes. Staging and dev need 1.
Refusals are 422 semantic_check_failed with the reason code in field_errors[0].message (field bundle):
no_shared_authority (nothing in common with the served version: add the new CA alongside the old first, or declare an
emergency), replace_requires_two_steps (adds and removes authorities at once), overlap_not_elapsed (a remove step before
the served version's overlap_until).
Overlap. overlap_seconds is required: 330 s to 7 days (330 s is the assertion lifetime plus verifier clock skew, so every
assertion that named the old bundle has expired before an authority can be removed). In a production deployment an add
step needs at least 86400 s (24 h), otherwise 400 on overlap_seconds; staging and dev keep the 330 s minimum. The overlap
is sticky: a renewal carries it forward and a second add step can only extend it, never shorten it, so a remove step is
allowed only after the longest overlap since the last remove step has ended. A remove step completes (clears) the overlap.
emergency must be a boolean; reason (1 to 2000 characters, no control characters except newlines) is required with
emergency: true and refused otherwise. A proposal not yet activated may be replaced by another rotate call.
expected_version must equal the binding's version. At most 20 versions that never became active per binding per 90 days.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | BindingId | Issuer binding id. |
Idempotency-Key | header | Yes | string | Unique key for this logical mutation (1 to 255 characters of A-Z a-z 0-9 . _ : -). Replays within 24 hours return the original outcome.1 to 255 characters; pattern ^[A-Za-z0-9._:-]+$example 7f3c1b52-9d44-4a0e-8f21-0a6e5b9d3c77 |
x-art-csrf | header | Yes | string | The csrf_token from GET /auth/session. Required on every session-authenticated mutation.example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA |
Request body
application/json, required: RotateBindingRequest
| Field | Type | Required | Description |
|---|---|---|---|
expected_version | integer | Yes | The binding's version. |
bundle | string | Yes | The exact SPIFFE bundle JSON text of the new version. at most 65536 characters |
bundle_endpoint | string (uri) | No | Defaults to the served version's endpoint. at most 2048 characters |
overlap_seconds | integer | Yes | The overlap recorded on an add step: the earliest a later remove step may drop the old authorities (activation + this). 330 s to 7 days; at least 86400 s for an add step in production. Required for every kind.330 to 604800 |
emergency | boolean | No | Declare an emergency rotation (compromise response): replaces or drops authorities at once, always 2 approvals. Needs reason.default false |
reason | string | No | Required with emergency: true, refused otherwise. Newlines allowed, no other control characters.1 to 2000 characters |
Example
{
"expected_version": 6,
"overlap_seconds": 86400,
"bundle": "{\"spiffe_sequence\":2,\"keys\":[{\"use\":\"x509-svid\",\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU\",\"y\":\"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0\",\"x5c\":[\"MIIBszCCAVmgAwIBAgIUExampleOnlyNotARealCertificateExampleOnly=\"]}]}"
}
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The binding, the rotation summary and the ceremony instructions. | application/json RotationProposed |
| 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 |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 409 | conflict. The record changed since you read it (stale expected_version), the action is not valid in the record's
current state, the Idempotency-Key was reused with a different request, or the same request is still in flight. | application/json Error |
| 422 | semantic_check_failed. The request is well formed but a policy or proof check failed (for example an issuer-control proof
did not verify, a challenge expired, or the organization is not yet verified). field_errors[].message is often a stable
reason code. | application/json Error |
| 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 |
201 example
{
"binding": {
"id": "bnd_9r2t5w8z1b4d7f0h3k6n9q2s5v",
"organization_id": "org_7k3m9q2x8d5f1h4j6n0p2r4t8v",
"claim_id": "clm_4c8e2g6k0m3q7s1w5y9a2d6f8h",
"delegation_id": null,
"trust_domain": "agents.payments.example.com",
"verified_domain": "payments.example.com",
"mapping": "dns",
"profile": "spiffe-x509",
"workload_scope": [
"/agent/checkout"
],
"environment": "production",
"required_approvals": 2,
"status": "active",
"status_reason": null,
"version": 7,
"current_version": 1,
"expires_at": "2027-10-05T11:58:12.000Z",
"created_at": "2026-10-05T11:56:00.000Z",
"updated_at": "2026-10-20T09:00:00.000Z",
"endpoint_drift": false,
"endpoint_drift_at": null,
"rotation": {
"version": 2,
"rotation_of": 1,
"kind": "add",
"overlap_seconds": 86400,
"continuity": true,
"required_approvals": 1,
"ceremony_complete": false
},
"versions": [],
"next_actions": [
"complete_issuer_control_ceremony"
]
},
"rotation": {
"version": 2,
"rotation_of": 1,
"kind": "add",
"overlap_seconds": 86400,
"continuity": true,
"renewal": false,
"emergency": false,
"shared_authorities": [
"9f2c4ab07d1e5b83c6a0f4d92e7b1a35c8d0e6f4a2b9713d5c8e0f1a2b3c4d5e"
],
"added_authorities": [
"1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809"
],
"removed_authorities": [],
"required_approvals": 1
},
"ceremony": {
"version": 2,
"bundle_sha256": "Yt3Lw8Xk1Zq5Vp9Nb2Ms6Ra0Ey4Hc7Jf3Dg1Ta5PuKw",
"challenge": "POST /control-challenge issues a one-time nonce.",
"required": [
"federation_endpoint",
"x509_svid"
],
"methods": {
"federation_endpoint": "Publish the bundle plus the challenge key at the endpoint.",
"x509_svid": "Issue a challenge X.509-SVID per CA."
},
"after": "A non-emergency rotation needs one approval; an emergency rotation needs two."
}
}
400 example
{
"code": "invalid_request",
"message": "Invalid organization.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "legal_name",
"message": "must be 1-200 characters"
}
]
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
409 example
{
"code": "conflict",
"message": "The record changed since you read it; reload and retry with the current version.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
422 example
{
"code": "semantic_check_failed",
"message": "Issuer-control proof failed (endpoint_http_404). 4 attempt(s) left on this challenge.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "method",
"message": "endpoint_http_404"
}
]
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Withdraw an issuer binding
POST/v1/issuer-bindings/{id}/withdraw
Human only; owner or issuer administrator; step-up. Valid in any live state (draft, pending_review, active,
suspended). A published binding becomes withdrawn (terminal) with a public binding.withdrawn event; the served version's
bundle stays fetchable. The response is sent only after the commit. An assertion signed from a read taken before the commit
stays acceptable until its own expiry, so assertion_bound_seconds (330 = 300 s lifetime + 30 s verifier clock skew) is the
worst case after committed_at. Withdrawal stops the directory's endorsement; it does not revoke SVIDs already issued.
expected_version must equal the binding's version. reason is required (kept in the audit log only, never public).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | BindingId | Issuer binding id. |
Idempotency-Key | header | Yes | string | Unique key for this logical mutation (1 to 255 characters of A-Z a-z 0-9 . _ : -). Replays within 24 hours return the original outcome.1 to 255 characters; pattern ^[A-Za-z0-9._:-]+$example 7f3c1b52-9d44-4a0e-8f21-0a6e5b9d3c77 |
x-art-csrf | header | Yes | string | The csrf_token from GET /auth/session. Required on every session-authenticated mutation.example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA |
Request body
application/json, required: WithdrawBindingRequest
| Field | Type | Required | Description |
|---|---|---|---|
expected_version | integer | Yes | The version you last read; a stale value is 409. |
reason | string | Yes | Required. Free text (newlines allowed, no other control characters); audit log only, never public. 1 to 2000 characters |
Example
{
"expected_version": 6,
"reason": "Decommissioning the payments agents."
}
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Withdrawn. | application/json BindingWithdrawn |
| 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 |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 409 | conflict. The record changed since you read it (stale expected_version), the action is not valid in the record's
current state, the Idempotency-Key was reused with a different request, or the same request is still in flight. | 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
{
"status": "withdrawn",
"committed_at": "2026-10-21T10:00:00.000Z",
"assertion_bound_seconds": 330,
"binding": {
"id": "bnd_9r2t5w8z1b4d7f0h3k6n9q2s5v",
"organization_id": "org_7k3m9q2x8d5f1h4j6n0p2r4t8v",
"claim_id": "clm_4c8e2g6k0m3q7s1w5y9a2d6f8h",
"delegation_id": null,
"trust_domain": "agents.payments.example.com",
"verified_domain": "payments.example.com",
"mapping": "dns",
"profile": "spiffe-x509",
"workload_scope": [
"/agent/checkout"
],
"environment": "production",
"required_approvals": 2,
"status": "withdrawn",
"status_reason": "organization_withdrew",
"version": 8,
"current_version": 1,
"expires_at": "2027-10-05T11:58:12.000Z",
"created_at": "2026-10-05T11:56:00.000Z",
"updated_at": "2026-10-21T10:00:00.000Z",
"endpoint_drift": false,
"endpoint_drift_at": null,
"rotation": null,
"versions": [],
"next_actions": []
}
}
400 example
{
"code": "invalid_request",
"message": "Invalid organization.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "legal_name",
"message": "must be 1-200 characters"
}
]
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
409 example
{
"code": "conflict",
"message": "The record changed since you read it; reload and retry with the current version.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}