Preview — not for production reliance
Documentation

Docs API reference

Recovery

Organization recovery with a fresh DNS recheck and two independent reviewers.

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

Recovery requests for an organization (owners)

GET/v1/organizations/{id}/recovery-requests

Authentication
Session (passkey-verified)
Operation ID
listOrganizationRecoveryRequests

Owner only. Who has asked to recover this organization, newest first (at most 50). Never the requester's private reason or contact details.

Parameters

NameInRequiredTypeDescription
idpathYesOrganizationIdOrganization id.

Responses

StatusDescriptionBody
200The requests.application/json object
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

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

Ask to recover an organization

POST/v1/organizations/{id}/recovery-requests

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

For someone who is not an owner: for example the owner account's passkey is lost, or a new representative takes over. Signed in, passkey-verified, recent step-up, human, not platform staff. Rate-limited per IP and to 3 requests per person per 24 hours; one open request per person and organization (409). The organization must have a verified domain (422).

Every active owner is notified at once (in-app, GET /v1/notifications, and by email when configured) and again on every reviewer approval. At most 3 pending requests per organization (429).

The request grants nothing by itself. A changed email never transfers anything either: accounts are keyed on the identity provider's subject, not the email. Recovery completes only when (1) you pass a fresh DNS challenge on one of the organization's verified domains (POST /v1/recovery-requests/{id}/dns-challenge, then verify-dns) and (2) two different internal reviewers approve after each contacting the organization through an independently sourced channel (three if an owner objected to any recovery of the organization in the last 90 days, or you are a former member or former owner; any contact that denied you blocks approval), and not before the owners' notice period ends: always 7 days after filing. The DNS proof must cover every domain backing an active production issuer (every verified domain when there is none). Only then do you become an organization_owner; the reviewers decide whether the existing owners are kept, demoted to viewer or removed (demoted or removed owners lose all sessions and the service tokens they created). Your next action after approval needs a fresh passkey assertion. Undecided requests expire after 30 days. The organization's owners can see and withdraw the request.

reason (1-2000 characters) and contact_details (1-1000) are shown only to you and to reviewers. Reviewers never use your contact details to verify you.

Parameters

NameInRequiredTypeDescription
idpathYesOrganizationIdOrganization 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: object

FieldTypeRequiredDescription
reasonstringYes1 to 2000 characters
contact_detailsstringYes1 to 1000 characters

Example

json
{
  "reason": "Our only owner left and took the passkey with her laptop.",
  "contact_details": "I am the new head of PKI; call the main switchboard."
}

Responses

StatusDescriptionBody
201The pending request.application/json object
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

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

Your recovery request

GET/v1/recovery-requests/{id}

Authentication
Session (passkey-verified)
Operation ID
getRecoveryRequest

The requester only (anyone else gets 404). Shows the state, the verified domains you can recheck, the live DNS challenge (never its value), approvals so far and what to do next.

Parameters

NameInRequiredTypeDescription
idpathYesstringRecovery request id.
pattern ^rcv_[0-9a-z]{26,32}$
example rcv_5f8j1m4p7s0v3y6b9e2h5k8n1q

Responses

StatusDescriptionBody
200The request.application/json object
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

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

Get a fresh DNS challenge for recovery

POST/v1/recovery-requests/{id}/dns-challenge

Authentication
Session (passkey-verified)
Operation ID
createRecoveryDnsChallenge

The requester only, while the request is pending. A new single-use challenge (256-bit value, shown once; 24 h; 5 checks) at _workload-directory.<domain> for one of the organization's verified domains (domain, default the first one; any other name is 422). It supersedes a pending one. At most 5 per request (429). An idempotent replay returns the record without the value.

Parameters

NameInRequiredTypeDescription
idpathYesstringRecovery request id.
pattern ^rcv_[0-9a-z]{26,32}$
example rcv_5f8j1m4p7s0v3y6b9e2h5k8n1q
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: object

FieldTypeRequiredDescription
domainstringNo

Responses

StatusDescriptionBody
201The challenge, with its value (shown once).application/json object
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
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

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

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

Check the recovery DNS challenge now

POST/v1/recovery-requests/{id}/verify-dns

Authentication
Session (passkey-verified)
Operation ID
verifyRecoveryDns

The requester only. Looks the TXT record up at both public DoH resolvers (Cloudflare and Google); both must see the exact value at exactly that name (never through a CNAME). 200 with outcome: verified records the fresh domain-control recheck on the request (it then counts for 7 days and resets any approvals given to an older version of the request); 200 with outcome: no_match uses one of the 5 attempts. A resolver outage is 503 and uses no attempt.

Parameters

NameInRequiredTypeDescription
idpathYesstringRecovery request id.
pattern ^rcv_[0-9a-z]{26,32}$
example rcv_5f8j1m4p7s0v3y6b9e2h5k8n1q
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
200The outcome.application/json object
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
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

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

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 a recovery request

POST/v1/recovery-requests/{id}/withdraw

Authentication
Session (passkey-verified)
Operation ID
withdrawRecoveryRequest

The requester withdraws their own request (terminal). An active owner calling this (with recent step-up) records an objection instead: the request stays pending but is paused, needs a third independent reviewer to overrule it, and the other owners are notified (200 with owner_objection: true). Anyone else gets 404.

Parameters

NameInRequiredTypeDescription
idpathYesstringRecovery request id.
pattern ^rcv_[0-9a-z]{26,32}$
example rcv_5f8j1m4p7s0v3y6b9e2h5k8n1q
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
200Withdrawn.application/json object
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
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

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

409 example

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

Your notifications

GET/v1/notifications

Authentication
Session (passkey-verified)
Operation ID
listNotifications

People only. The latest 50 notifications for the signed-in principal, newest first, and the unread count. A notification says what happened to which organization (a recovery request filed, an approval, an owner objection, a completed recovery); it never carries tokens or private requester input.

Responses

StatusDescriptionBody
200The notifications.application/json object
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

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

Mark all your notifications read

POST/v1/notifications/read-all

Authentication
Session (passkey-verified)
Operation ID
markAllNotificationsRead

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

Responses

StatusDescriptionBody
200How many were marked.application/json object
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

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

Mark one notification read

POST/v1/notifications/{id}/read

Authentication
Session (passkey-verified)
Operation ID
markNotificationRead

Only your own notifications (anyone else's is 404).

Parameters

NameInRequiredTypeDescription
idpathYesstring
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
200Marked.application/json object
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

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