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
Owner only. Who has asked to recover this organization, newest first (at most 50). Never the requester's private reason or contact details.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | OrganizationId | Organization id. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The requests. | application/json object |
| 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 |
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"
}
Ask to recover an organization
POST/v1/organizations/{id}/recovery-requests
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | OrganizationId | Organization 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: object
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | Yes | 1 to 2000 characters |
contact_details | string | Yes | 1 to 1000 characters |
Example
{
"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
| Status | Description | Body |
|---|---|---|
| 201 | The pending request. | application/json object |
| 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 |
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"
}
Your recovery request
GET/v1/recovery-requests/{id}
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | string | Recovery request id. pattern ^rcv_[0-9a-z]{26,32}$example rcv_5f8j1m4p7s0v3y6b9e2h5k8n1q |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The request. | application/json object |
| 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 |
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"
}
Get a fresh DNS challenge for recovery
POST/v1/recovery-requests/{id}/dns-challenge
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | string | Recovery request id. pattern ^rcv_[0-9a-z]{26,32}$example rcv_5f8j1m4p7s0v3y6b9e2h5k8n1q |
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: object
| Field | Type | Required | Description |
|---|---|---|---|
domain | string | No |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The challenge, with its value (shown once). | application/json object |
| 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 |
| 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 |
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"
}
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"
}
Check the recovery DNS challenge now
POST/v1/recovery-requests/{id}/verify-dns
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | string | Recovery request id. pattern ^rcv_[0-9a-z]{26,32}$example rcv_5f8j1m4p7s0v3y6b9e2h5k8n1q |
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 |
|---|---|---|
| 200 | The outcome. | application/json object |
| 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 |
| 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 |
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"
}
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 a recovery request
POST/v1/recovery-requests/{id}/withdraw
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
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | string | Recovery request id. pattern ^rcv_[0-9a-z]{26,32}$example rcv_5f8j1m4p7s0v3y6b9e2h5k8n1q |
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 |
|---|---|---|
| 200 | Withdrawn. | application/json object |
| 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 |
| 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 |
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"
}
409 example
{
"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
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
| Status | Description | Body |
|---|---|---|
| 200 | The notifications. | application/json object |
| 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 |
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"
}
Mark all your notifications read
POST/v1/notifications/read-all
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 |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | How many were marked. | application/json object |
| 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 |
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"
}
Mark one notification read
POST/v1/notifications/{id}/read
Only your own notifications (anyone else's is 404).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | string | |
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 |
|---|---|---|
| 200 | Marked. | application/json object |
| 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 |
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"
}