Preview — not for production reliance
Documentation

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

Authentication
Session with step-up (passkey within 300 s)
Operation ID
createIssuerBinding

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

NameInRequiredTypeDescription
Idempotency-KeyheaderYesstringUnique 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-csrfheaderYesstringThe csrf_token from GET /auth/session. Required on every session-authenticated mutation.
example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA

Request body

application/json, required: CreateIssuerBindingRequest

FieldTypeRequiredDescription
claim_idClaimIdYes
trust_domainTrustDomainYes
profile"spiffe-x509"YesThe only supported profile. Needs at least one x509-svid key.
bundle_endpointstring (uri)Yeshttps, port 443, DNS host (no IP literal), no credentials or fragment. Must be at or under the verified domain.
at most 2048 characters
bundlestringYesThe 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_scopeWorkloadScopeYes
environmentone of "production", "test"NoProduction deployments accept only production.
default "production"
mappingone of "dns", "explicit"Nodns: 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

json
{
  "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

StatusDescriptionBody
201The draft binding and the ceremony instructions.application/json BindingWithCeremony
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
401unauthenticated (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
403forbidden. 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
404not_found. No such resource, or one the caller may not see (the two are indistinguishable by design).application/json Error
409conflict. 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
422semantic_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
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

201 example

json
{
  "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

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

401 unauthenticated

json
{
  "code": "unauthenticated",
  "message": "Sign in required.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

401 step_up_required

json
{
  "code": "step_up_required",
  "message": "Recent passkey verification required for this action.",
  "request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}

403 example

json
{
  "code": "forbidden",
  "message": "Your role does not permit this action.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

404 example

json
{
  "code": "not_found",
  "message": "No such resource.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

409 example

json
{
  "code": "conflict",
  "message": "The record changed since you read it; reload and retry with the current version.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

422 example

json
{
  "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

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

503 example

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

Get an issuer binding

GET/v1/issuer-bindings/{id}

Authentication
Session (passkey-verified) or Service token
Operation ID
getIssuerBinding

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

NameInRequiredTypeDescription
idpathYesBindingIdIssuer binding id.

Responses

StatusDescriptionBody
200The binding.application/json BindingEnvelope
401unauthenticated (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
404not_found. No such resource, or one the caller may not see (the two are indistinguishable by design).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
{
  "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

json
{
  "code": "unauthenticated",
  "message": "Sign in required.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

401 step_up_required

json
{
  "code": "step_up_required",
  "message": "Recent passkey verification required for this action.",
  "request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}

404 example

json
{
  "code": "not_found",
  "message": "No such resource.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

503 example

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

Status of the control challenge

GET/v1/issuer-bindings/{id}/control-challenge

Authentication
Session (passkey-verified)
Operation ID
getControlChallenge

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

NameInRequiredTypeDescription
idpathYesBindingIdIssuer binding id.

Responses

StatusDescriptionBody
200Challenge status.
Headers: Cache-Control: no-store
application/json ControlChallengeStatus
401unauthenticated (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
403forbidden. 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
404not_found. No such resource, or one the caller may not see (the two are indistinguishable by design).application/json Error
409conflict. 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
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
{
  "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

json
{
  "code": "unauthenticated",
  "message": "Sign in required.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

401 step_up_required

json
{
  "code": "step_up_required",
  "message": "Recent passkey verification required for this action.",
  "request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}

403 example

json
{
  "code": "forbidden",
  "message": "Your role does not permit this action.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

404 example

json
{
  "code": "not_found",
  "message": "No such resource.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

409 example

json
{
  "code": "conflict",
  "message": "The record changed since you read it; reload and retry with the current version.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

503 example

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

Issue the control challenge

POST/v1/issuer-bindings/{id}/control-challenge

Authentication
Session (passkey-verified)
Operation ID
issueControlChallenge

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

NameInRequiredTypeDescription
idpathYesBindingIdIssuer binding id.
Idempotency-KeyheaderYesstringUnique 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-csrfheaderYesstringThe csrf_token from GET /auth/session. Required on every session-authenticated mutation.
example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA

Responses

StatusDescriptionBody
201The challenge with its one-time nonce.
Headers: Cache-Control: no-store
application/json ControlChallengeIssued
401unauthenticated (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
403forbidden. 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
404not_found. No such resource, or one the caller may not see (the two are indistinguishable by design).application/json Error
409conflict. 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
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

201 example

json
{
  "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

json
{
  "code": "unauthenticated",
  "message": "Sign in required.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

401 step_up_required

json
{
  "code": "step_up_required",
  "message": "Recent passkey verification required for this action.",
  "request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}

403 example

json
{
  "code": "forbidden",
  "message": "Your role does not permit this action.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

404 example

json
{
  "code": "not_found",
  "message": "No such resource.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

409 example

json
{
  "code": "conflict",
  "message": "The record changed since you read it; reload and retry with the current version.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

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

Submit an issuer-control proof

POST/v1/issuer-bindings/{id}/control-proof

Authentication
Session with step-up (passkey within 300 s)
Operation ID
submitControlProof

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 for spiffe://<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

NameInRequiredTypeDescription
idpathYesBindingIdIssuer binding id.
Idempotency-KeyheaderYesstringUnique 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-csrfheaderYesstringThe csrf_token from GET /auth/session. Required on every session-authenticated mutation.
example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA

Request body

application/json, required: ControlProofRequest

FieldTypeRequiredDescription
methodone of "federation_endpoint", "x509_svid", "signed_statement"Yes
svidsarray of stringNoRequired for x509_svid and forbidden otherwise. One PEM chain (leaf first) per x509-svid CA in the bundle.
statementsarray of stringNoRequired for signed_statement and forbidden otherwise. One compact JWS per jwt-svid key (1 to 32).
1 to 32 items
expected_versionintegerNoIf sent, must equal the binding's version.

Example: Federation endpoint proof

json
{
  "method": "federation_endpoint"
}

Example: Challenge SVIDs

json
{
  "method": "x509_svid",
  "svids": [
    "-----BEGIN CERTIFICATE-----\nMIIBszCCAVmgAwIBAgIUExampleOnly\n-----END CERTIFICATE-----\n"
  ]
}

Responses

StatusDescriptionBody
200The component passed; ceremony_status says whether the ceremony is complete.application/json ControlProofResult
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
401unauthenticated (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
403forbidden. 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
404not_found. No such resource, or one the caller may not see (the two are indistinguishable by design).application/json Error
409conflict. 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
422semantic_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
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
{
  "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

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

401 unauthenticated

json
{
  "code": "unauthenticated",
  "message": "Sign in required.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

401 step_up_required

json
{
  "code": "step_up_required",
  "message": "Recent passkey verification required for this action.",
  "request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}

403 example

json
{
  "code": "forbidden",
  "message": "Your role does not permit this action.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

404 example

json
{
  "code": "not_found",
  "message": "No such resource.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

409 example

json
{
  "code": "conflict",
  "message": "The record changed since you read it; reload and retry with the current version.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

422 example

json
{
  "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

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

Propose a new version of a draft binding

POST/v1/issuer-bindings/{id}/versions

Authentication
Session with step-up (passkey within 300 s)
Operation ID
createBindingVersion

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

NameInRequiredTypeDescription
idpathYesBindingIdIssuer binding id.
Idempotency-KeyheaderYesstringUnique 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-csrfheaderYesstringThe csrf_token from GET /auth/session. Required on every session-authenticated mutation.
example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA

Request body

application/json, required: CreateBindingVersionRequest

FieldTypeRequiredDescription
expected_versionintegerYesThe binding's version.
bundlestringYesThe exact SPIFFE bundle JSON text.
at most 65536 characters
bundle_endpointstring (uri)NoDefaults to the previous version's endpoint.
at most 2048 characters

Example

json
{
  "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

StatusDescriptionBody
201The binding with the new proposed version, and its ceremony instructions.application/json BindingWithCeremony
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
401unauthenticated (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
403forbidden. 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
404not_found. No such resource, or one the caller may not see (the two are indistinguishable by design).application/json Error
409conflict. 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
422semantic_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
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

201 example

json
{
  "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

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

401 unauthenticated

json
{
  "code": "unauthenticated",
  "message": "Sign in required.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

401 step_up_required

json
{
  "code": "step_up_required",
  "message": "Recent passkey verification required for this action.",
  "request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}

403 example

json
{
  "code": "forbidden",
  "message": "Your role does not permit this action.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

404 example

json
{
  "code": "not_found",
  "message": "No such resource.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

409 example

json
{
  "code": "conflict",
  "message": "The record changed since you read it; reload and retry with the current version.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

422 example

json
{
  "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

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

503 example

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

Preview a rotation (dry run)

POST/v1/issuer-bindings/{id}/rotation-preview

Authentication
Session (passkey-verified)
Operation ID
previewIssuerBindingRotation

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

NameInRequiredTypeDescription
idpathYesBindingIdIssuer binding id.
x-art-csrfheaderYesstringThe 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.

FieldTypeRequiredDescription
expected_versionintegerNo
bundlestringYesat most 65536 characters
bundle_endpointstring (uri)Noat most 2048 characters
overlap_secondsintegerYes330 to 604800
emergencybooleanNodefault false
reasonstringNo1 to 2000 characters

Responses

StatusDescriptionBody
200The classification, or the refusal rotate would answer with.application/json RotationPreview
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
401unauthenticated (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
403forbidden. 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
404not_found. No such resource, or one the caller may not see (the two are indistinguishable by design).application/json Error
413invalid_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
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
{
  "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

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

401 unauthenticated

json
{
  "code": "unauthenticated",
  "message": "Sign in required.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

401 step_up_required

json
{
  "code": "step_up_required",
  "message": "Recent passkey verification required for this action.",
  "request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}

403 example

json
{
  "code": "forbidden",
  "message": "Your role does not permit this action.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

404 example

json
{
  "code": "not_found",
  "message": "No such resource.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

413 example

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

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

Propose a rotation of an active binding

POST/v1/issuer-bindings/{id}/rotate

Authentication
Session with step-up (passkey within 300 s)
Operation ID
rotateIssuerBinding

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

NameInRequiredTypeDescription
idpathYesBindingIdIssuer binding id.
Idempotency-KeyheaderYesstringUnique 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-csrfheaderYesstringThe csrf_token from GET /auth/session. Required on every session-authenticated mutation.
example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA

Request body

application/json, required: RotateBindingRequest

FieldTypeRequiredDescription
expected_versionintegerYesThe binding's version.
bundlestringYesThe exact SPIFFE bundle JSON text of the new version.
at most 65536 characters
bundle_endpointstring (uri)NoDefaults to the served version's endpoint.
at most 2048 characters
overlap_secondsintegerYesThe 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
emergencybooleanNoDeclare an emergency rotation (compromise response): replaces or drops authorities at once, always 2 approvals. Needs reason.
default false
reasonstringNoRequired with emergency: true, refused otherwise. Newlines allowed, no other control characters.
1 to 2000 characters

Example

json
{
  "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

StatusDescriptionBody
201The binding, the rotation summary and the ceremony instructions.application/json RotationProposed
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
401unauthenticated (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
403forbidden. 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
404not_found. No such resource, or one the caller may not see (the two are indistinguishable by design).application/json Error
409conflict. 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
422semantic_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
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

201 example

json
{
  "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

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

401 unauthenticated

json
{
  "code": "unauthenticated",
  "message": "Sign in required.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

401 step_up_required

json
{
  "code": "step_up_required",
  "message": "Recent passkey verification required for this action.",
  "request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}

403 example

json
{
  "code": "forbidden",
  "message": "Your role does not permit this action.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

404 example

json
{
  "code": "not_found",
  "message": "No such resource.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

409 example

json
{
  "code": "conflict",
  "message": "The record changed since you read it; reload and retry with the current version.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

422 example

json
{
  "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

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

503 example

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

Withdraw an issuer binding

POST/v1/issuer-bindings/{id}/withdraw

Authentication
Session with step-up (passkey within 300 s)
Operation ID
withdrawIssuerBinding

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

NameInRequiredTypeDescription
idpathYesBindingIdIssuer binding id.
Idempotency-KeyheaderYesstringUnique 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-csrfheaderYesstringThe csrf_token from GET /auth/session. Required on every session-authenticated mutation.
example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA

Request body

application/json, required: WithdrawBindingRequest

FieldTypeRequiredDescription
expected_versionintegerYesThe version you last read; a stale value is 409.
reasonstringYesRequired. Free text (newlines allowed, no other control characters); audit log only, never public.
1 to 2000 characters

Example

json
{
  "expected_version": 6,
  "reason": "Decommissioning the payments agents."
}

Responses

StatusDescriptionBody
200Withdrawn.application/json BindingWithdrawn
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
401unauthenticated (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
403forbidden. 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
404not_found. No such resource, or one the caller may not see (the two are indistinguishable by design).application/json Error
409conflict. 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
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
{
  "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

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

401 unauthenticated

json
{
  "code": "unauthenticated",
  "message": "Sign in required.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

401 step_up_required

json
{
  "code": "step_up_required",
  "message": "Recent passkey verification required for this action.",
  "request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}

403 example

json
{
  "code": "forbidden",
  "message": "Your role does not permit this action.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

404 example

json
{
  "code": "not_found",
  "message": "No such resource.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

409 example

json
{
  "code": "conflict",
  "message": "The record changed since you read it; reload and retry with the current version.",
  "request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}

503 example

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