Docs API reference
Claims
Domain claims, DNS challenges, evidence, appeals and jobs.
10 operations. See the API overview for authentication, errors, idempotency and caching rules that apply to all of them.
File a domain claim
POST/v1/claims
Claims an exact domain on behalf of an organization and issues the first DNS challenge. Owner or domain administrator
(session or service token). The domain must already be in the directory: an unknown name is 422 semantic_check_failed
with field_errors[0].message = domain_not_in_directory; add it first with POST /v1/domains, which checks live DNS and a
publicly trusted certificate for the exact name. An organization may have only one open claim per domain (409) and at most
10 claims per rolling hour (429).
The challenge value is shown once. Publish it as a TXT record at challenge.dns_name, then call
POST /v1/claims/{id}/verify.
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 | No | string | Required when authenticating with a session cookie; not used with a service token. example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA |
Request body
application/json, required: CreateClaimRequest
| Field | Type | Required | Description |
|---|---|---|---|
organization_id | OrganizationId | Yes | |
domain | string | Yes | An exact hostname. It is canonicalized; a name that is not a valid registrable-domain hostname is a field error carrying the reason. |
Example
{
"organization_id": "org_7k3m9q2x8d5f1h4j6n0p2r4t8v",
"domain": "payments.example.com"
}
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The claim and its first challenge. Headers: Cache-Control: no-store | application/json ClaimCreated |
| 400 | invalid_request. Malformed JSON, an invalid or unknown field (see field_errors), a missing or malformed
Idempotency-Key, or a bad query. Also used with status 413 (body over 64 KiB) and 415 (wrong Content-Type). | application/json Error |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 409 | conflict. The record changed since you read it (stale expected_version), the action is not valid in the record's
current state, the Idempotency-Key was reused with a different request, or the same request is still in flight. | application/json Error |
| 422 | semantic_check_failed. The request is well formed but a policy or proof check failed (for example an issuer-control proof
did not verify, a challenge expired, or the organization is not yet verified). field_errors[].message is often a stable
reason code. | application/json Error |
| 429 | rate_limited. A per-client or per-record limit was reached; retry later. | application/json Error |
| 503 | unavailable (temporarily unavailable; also a feature that is not configured in this deployment) or
provider_unavailable (an identity provider is not configured). Both are 503. During maintenance (for example a database
recovery) every mutating request answers unavailable "The directory is under maintenance; try again later." Reads keep working. | application/json Error |
201 example
{
"claim": {
"id": "clm_4c8e2g6k0m3q7s1w5y9a2d6f8h",
"organization_id": "org_7k3m9q2x8d5f1h4j6n0p2r4t8v",
"domain": "payments.example.com",
"state": "pending_domain",
"resume_state": null,
"version": 1,
"claimant_id": "prn_6d9g2j5m8q1t4w7z0c3f6h9k2n",
"created_at": "2026-10-05T11:50:00.000Z",
"updated_at": "2026-10-05T11:50:00.000Z",
"expires_at": null,
"domain_verified_at": null,
"identity_verified_at": null,
"authority_verified_at": null,
"authority_expires_at": null,
"permitted_actions": []
},
"challenge": {
"id": "chl_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"dns_name": "_workload-directory.payments.example.com",
"record_type": "TXT",
"expires_at": "2026-10-06T11:50:00.000Z",
"max_attempts": 5,
"value": "art-verify=Vd2Qm9Xk4Lw7Zp0Nb3Ts6Ra1Ey8Hc5Jf"
},
"instructions": "Publish a TXT record at _workload-directory.payments.example.com whose value is exactly the challenge value, then POST /v1/claims/{id}/verify. The value is shown only once. Both Cloudflare and Google public DNS must see it. Request a new challenge if it expires (24 h) or after 5 failed checks."
}
400 example
{
"code": "invalid_request",
"message": "Invalid organization.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "legal_name",
"message": "must be 1-200 characters"
}
]
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
409 example
{
"code": "conflict",
"message": "The record changed since you read it; reload and retry with the current version.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
422 example
{
"code": "semantic_check_failed",
"message": "Issuer-control proof failed (endpoint_http_404). 4 attempt(s) left on this challenge.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "method",
"message": "endpoint_http_404"
}
]
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Get a claim
GET/v1/claims/{id}
Any member. Returns the claim, the three check results, the five most recent challenges (status only, never secrets), the
organization-visible evidence (never storage keys, reviewer identity or reviewer notes), failed checks, suggested
next_actions and, when a 90-day domain recheck is open, domain_recheck.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | ClaimId | Claim id. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The claim detail. | application/json ClaimDetail |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 503 | unavailable (temporarily unavailable; also a feature that is not configured in this deployment) or
provider_unavailable (an identity provider is not configured). Both are 503. During maintenance (for example a database
recovery) every mutating request answers unavailable "The directory is under maintenance; try again later." Reads keep working. | application/json Error |
200 example
{
"claim": {
"id": "clm_4c8e2g6k0m3q7s1w5y9a2d6f8h",
"organization_id": "org_7k3m9q2x8d5f1h4j6n0p2r4t8v",
"domain": "payments.example.com",
"state": "pending_identity",
"resume_state": null,
"version": 3,
"claimant_id": "prn_6d9g2j5m8q1t4w7z0c3f6h9k2n",
"created_at": "2026-10-05T11:50:00.000Z",
"updated_at": "2026-10-05T12:02:00.000Z",
"expires_at": null,
"domain_verified_at": "2026-10-05T12:02:00.000Z",
"identity_verified_at": null,
"authority_verified_at": null,
"authority_expires_at": null,
"permitted_actions": []
},
"information_request": null,
"checks": {
"domain_control": "passed",
"organization_identity": "pending",
"representative_authority": "pending"
},
"challenges": [
{
"id": "chl_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"purpose": "claim",
"principal_id": "prn_6d9g2j5m8q1t4w7z0c3f6h9k2n",
"dns_name": "_workload-directory.payments.example.com",
"expires_at": "2026-10-06T11:50:00.000Z",
"consumed_at": "2026-10-05T12:02:00.000Z",
"attempts": 1,
"status": "consumed",
"last_failure": null,
"last_checked_at": "2026-10-05T12:02:00.000Z",
"created_at": "2026-10-05T11:50:00.000Z"
}
],
"evidence": [],
"failed_checks": [],
"next_actions": [
"submit_organization_identity_evidence"
],
"domain_recheck": null
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Replace the DNS challenge
POST/v1/claims/{id}/challenges
Issues a new challenge (lost value, expiry, attempts exhausted) and supersedes the live one. Owner or domain administrator.
Only while domain control is the pending step (409 otherwise). At most 5 challenges per claim, ever (429).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | ClaimId | Claim 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 | No | string | Required when authenticating with a session cookie; not used with a service token. example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The new challenge. The value is shown once. Headers: Cache-Control: no-store | application/json ChallengeCreated |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 409 | conflict. The record changed since you read it (stale expected_version), the action is not valid in the record's
current state, the Idempotency-Key was reused with a different request, or the same request is still in flight. | application/json Error |
| 429 | rate_limited. A per-client or per-record limit was reached; retry later. | application/json Error |
| 503 | unavailable (temporarily unavailable; also a feature that is not configured in this deployment) or
provider_unavailable (an identity provider is not configured). Both are 503. During maintenance (for example a database
recovery) every mutating request answers unavailable "The directory is under maintenance; try again later." Reads keep working. | application/json Error |
201 example
{
"challenge": {
"id": "chl_5a8c1f4h7k0m3p6s9v2x5z8b1d",
"dns_name": "_workload-directory.payments.example.com",
"record_type": "TXT",
"expires_at": "2026-10-06T12:10:00.000Z",
"max_attempts": 5,
"value": "art-verify=Ne5Tw1Zk8Lb4Vq7Xc0Ma3Rs6Hy9Dp2Jf"
},
"instructions": "Publish a TXT record at _workload-directory.payments.example.com whose value is exactly the challenge value, then POST /v1/claims/{id}/verify. The value is shown only once. Both Cloudflare and Google public DNS must see it. Request a new challenge if it expires (24 h) or after 5 failed checks."
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
409 example
{
"code": "conflict",
"message": "The record changed since you read it; reload and retry with the current version.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Check the DNS challenge
POST/v1/claims/{id}/verify
Enqueues a DNS check of your own live challenge and returns 202 with a job to poll at GET /v1/jobs/{id}. For a
verified or suspended claim this verifies the 90-day recheck challenge instead. While a check of the same challenge
attempt is queued or running, the same job is returned. No live challenge for the caller's account, an expired one or an
exhausted one is 422 semantic_check_failed.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | ClaimId | Claim 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 | No | string | Required when authenticating with a session cookie; not used with a service token. example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA |
Responses
| Status | Description | Body |
|---|---|---|
| 202 | The check was queued. | application/json JobAccepted |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 409 | conflict. The record changed since you read it (stale expected_version), the action is not valid in the record's
current state, the Idempotency-Key was reused with a different request, or the same request is still in flight. | application/json Error |
| 422 | semantic_check_failed. The request is well formed but a policy or proof check failed (for example an issuer-control proof
did not verify, a challenge expired, or the organization is not yet verified). field_errors[].message is often a stable
reason code. | application/json Error |
| 503 | unavailable (temporarily unavailable; also a feature that is not configured in this deployment) or
provider_unavailable (an identity provider is not configured). Both are 503. During maintenance (for example a database
recovery) every mutating request answers unavailable "The directory is under maintenance; try again later." Reads keep working. | application/json Error |
202 example
{
"job_id": "job_2k5n8r1v4y7b0e3h6m9q2t5x8a",
"status": "queued",
"challenge_id": "chl_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
409 example
{
"code": "conflict",
"message": "The record changed since you read it; reload and retry with the current version.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
422 example
{
"code": "semantic_check_failed",
"message": "Issuer-control proof failed (endpoint_http_404). 4 attempt(s) left on this challenge.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "method",
"message": "endpoint_http_404"
}
]
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Reveal the 90-day recheck challenge
POST/v1/claims/{id}/recheck-challenge
Human only; owner or domain administrator. For a verified claim whose recheck is due, or a claim suspended because the
recheck lapsed. Each reveal rotates the token (the old value stops working) and is bound to the revealing account.
Reveals are capped per person and per claim in a 14-day window (429). Publish the value, then call
POST /v1/claims/{id}/verify.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | ClaimId | Claim id. |
Idempotency-Key | header | Yes | string | Unique key for this logical mutation (1 to 255 characters of A-Z a-z 0-9 . _ : -). Replays within 24 hours return the original outcome.1 to 255 characters; pattern ^[A-Za-z0-9._:-]+$example 7f3c1b52-9d44-4a0e-8f21-0a6e5b9d3c77 |
x-art-csrf | header | Yes | string | The csrf_token from GET /auth/session. Required on every session-authenticated mutation.example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The recheck challenge. The value is shown once. Headers: Cache-Control: no-store | application/json ChallengeCreated |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 409 | conflict. The record changed since you read it (stale expected_version), the action is not valid in the record's
current state, the Idempotency-Key was reused with a different request, or the same request is still in flight. | application/json Error |
| 422 | semantic_check_failed. The request is well formed but a policy or proof check failed (for example an issuer-control proof
did not verify, a challenge expired, or the organization is not yet verified). field_errors[].message is often a stable
reason code. | application/json Error |
| 429 | rate_limited. A per-client or per-record limit was reached; retry later. | application/json Error |
| 503 | unavailable (temporarily unavailable; also a feature that is not configured in this deployment) or
provider_unavailable (an identity provider is not configured). Both are 503. During maintenance (for example a database
recovery) every mutating request answers unavailable "The directory is under maintenance; try again later." Reads keep working. | application/json Error |
201 example
{
"challenge": {
"id": "chl_7c0e3g6j9m2p5s8v1x4z7b0d3f",
"purpose": "recheck",
"dns_name": "_workload-directory.payments.example.com",
"record_type": "TXT",
"expires_at": "2027-01-02T12:00:00.000Z",
"max_attempts": 5,
"attempts": 0,
"value": "art-verify=Rb8Xp3Zk0Nw6Lq2Vc5Ta9Hs1Ey4Dm7Jf"
},
"instructions": "Publish a TXT record at _workload-directory.payments.example.com whose value is exactly the challenge value, then POST /v1/claims/{id}/verify. The value is shown only once. Both Cloudflare and Google public DNS must see it. Request a new challenge if it expires (24 h) or after 5 failed checks."
}
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"
}
Appeal a rejected claim
POST/v1/claims/{id}/appeal
Owner, human, step-up. Moves a rejected claim to disputed for a reviewer to uphold or reject; the reason goes to the audit
trail for the reviewer and is never public. At most 3 appeals per claim (429). expected_version, when sent, must equal
the claim's current version. The organization must not hold another open claim on the domain (409).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | ClaimId | Claim 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: AppealRequest
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | Yes | Plain text, no control characters. Kept in the audit trail for the reviewer; never public. 1 to 2000 characters |
expected_version | integer | No | If sent, must equal the claim's current version. |
Example
{
"reason": "The registry record was updated on 2026-10-01 and now matches our legal name.",
"expected_version": 5
}
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The claim, now disputed. | application/json ClaimEnvelope |
| 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 |
| 429 | rate_limited. A per-client or per-record limit was reached; retry later. | application/json Error |
| 503 | unavailable (temporarily unavailable; also a feature that is not configured in this deployment) or
provider_unavailable (an identity provider is not configured). Both are 503. During maintenance (for example a database
recovery) every mutating request answers unavailable "The directory is under maintenance; try again later." Reads keep working. | application/json Error |
200 example
{
"claim": {
"id": "clm_4c8e2g6k0m3q7s1w5y9a2d6f8h",
"organization_id": "org_7k3m9q2x8d5f1h4j6n0p2r4t8v",
"domain": "payments.example.com",
"state": "disputed",
"resume_state": "pending_identity",
"version": 6,
"claimant_id": "prn_6d9g2j5m8q1t4w7z0c3f6h9k2n",
"created_at": "2026-10-05T11:50:00.000Z",
"updated_at": "2026-10-06T09:00:00.000Z",
"expires_at": null,
"domain_verified_at": "2026-10-05T12:02:00.000Z",
"identity_verified_at": null,
"authority_verified_at": null,
"authority_expires_at": null,
"permitted_actions": []
}
}
400 example
{
"code": "invalid_request",
"message": "Invalid organization.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "legal_name",
"message": "must be 1-200 characters"
}
]
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
409 example
{
"code": "conflict",
"message": "The record changed since you read it; reload and retry with the current version.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
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 claim
POST/v1/claims/{id}/withdraw
Owner, human, step-up. Terminal. Withdrawing a verified claim stops its issuer bindings from resolving, in the same commit.
Live challenges are cancelled and open conflicts are resolved. expected_version must equal the claim's current version
(409 otherwise).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | ClaimId | Claim 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: ExpectedVersionRequest
| Field | Type | Required | Description |
|---|---|---|---|
expected_version | integer | Yes | The version you last read; a stale value is 409.minimum 1 |
Example
{
"expected_version": 7
}
Responses
| Status | Description | Body |
|---|---|---|
| 200 | The claim, now withdrawn. | application/json ClaimEnvelope |
| 400 | invalid_request. Malformed JSON, an invalid or unknown field (see field_errors), a missing or malformed
Idempotency-Key, or a bad query. Also used with status 413 (body over 64 KiB) and 415 (wrong Content-Type). | application/json Error |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 409 | conflict. The record changed since you read it (stale expected_version), the action is not valid in the record's
current state, the Idempotency-Key was reused with a different request, or the same request is still in flight. | application/json Error |
| 503 | unavailable (temporarily unavailable; also a feature that is not configured in this deployment) or
provider_unavailable (an identity provider is not configured). Both are 503. During maintenance (for example a database
recovery) every mutating request answers unavailable "The directory is under maintenance; try again later." Reads keep working. | application/json Error |
200 example
{
"claim": {
"id": "clm_4c8e2g6k0m3q7s1w5y9a2d6f8h",
"organization_id": "org_7k3m9q2x8d5f1h4j6n0p2r4t8v",
"domain": "payments.example.com",
"state": "withdrawn",
"resume_state": null,
"version": 8,
"claimant_id": "prn_6d9g2j5m8q1t4w7z0c3f6h9k2n",
"created_at": "2026-10-05T11:50:00.000Z",
"updated_at": "2026-10-07T10:00:00.000Z",
"expires_at": null,
"domain_verified_at": "2026-10-05T12:02:00.000Z",
"identity_verified_at": "2026-10-05T14:00:00.000Z",
"authority_verified_at": "2026-10-05T14:30:00.000Z",
"authority_expires_at": "2027-10-05T14:30:00.000Z",
"permitted_actions": [
"authorize_issuer"
]
}
}
400 example
{
"code": "invalid_request",
"message": "Invalid organization.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "legal_name",
"message": "must be 1-200 characters"
}
]
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
409 example
{
"code": "conflict",
"message": "The record changed since you read it; reload and retry with the current version.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Begin an evidence submission
POST/v1/claims/{id}/evidence
Human only; owner or domain administrator. Records a submitted evidence row (only a reviewer can pass evidence) and
returns a one-time upload instruction for each declared file. At most 20 submissions per claim, 5 files each.
organization_identity is accepted while identity review is the pending step; representative_authority while identity
or authority is. details is a kind-specific structured object; unknown detail keys are rejected.
| kind | details.type |
other detail fields |
|---|---|---|
organization_identity |
ca_certificate |
certificate_reference (required, 500) |
registry_record |
registry_number (required, 100), registry_url (required, https, 500), registry_name (200) |
|
other |
at least one file; any of the above optional | |
representative_authority |
independent_contact |
contact_name (required, 200), contact_channel (required, 200), contact_role (200) |
delegated_authority_letter |
at least one file (the letter) | |
company_email |
company_email (required, 254) |
|
other |
at least one file |
Upload each file with PUT /v1/evidence-uploads within 1 hour.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | ClaimId | Claim 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: SubmitEvidenceRequest
| Field | Type | Required | Description |
|---|---|---|---|
kind | one of "organization_identity", "representative_authority" | Yes | |
description | string | Yes | Plain text for the reviewer. 1 to 2000 characters |
details | EvidenceDetails | Yes | |
files | array of DeclaredFile | No | at most 5 items |
Example
{
"kind": "organization_identity",
"description": "Delaware registry extract for the legal entity.",
"details": {
"type": "registry_record",
"registry_name": "Delaware Division of Corporations",
"registry_number": "5849201",
"registry_url": "https://icis.corp.delaware.gov/"
},
"files": [
{
"name": "registry-extract.pdf",
"content_type": "application/pdf",
"size": 182044
}
]
}
Responses
| Status | Description | Body |
|---|---|---|
| 201 | The evidence record and its upload instructions. Tokens are shown once. Headers: Cache-Control: no-store | application/json EvidenceSubmitted |
| 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 |
| 429 | rate_limited. A per-client or per-record limit was reached; retry later. | application/json Error |
| 503 | unavailable (temporarily unavailable; also a feature that is not configured in this deployment) or
provider_unavailable (an identity provider is not configured). Both are 503. During maintenance (for example a database
recovery) every mutating request answers unavailable "The directory is under maintenance; try again later." Reads keep working. | application/json Error |
201 example
{
"evidence": {
"id": "evi_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"claim_id": "clm_4c8e2g6k0m3q7s1w5y9a2d6f8h",
"kind": "organization_identity",
"result": "submitted",
"created_at": "2026-10-05T12:20:00.000Z",
"files": [
{
"id": "evf_5a8c1f4h7k0m3p6s9v2x5z8b1d",
"name": "registry-extract.pdf",
"content_type": "application/pdf",
"size": 182044,
"status": "awaiting_upload",
"upload_expires_at": "2026-10-05T13:20:00.000Z",
"upload": {
"method": "PUT",
"url": "https://svid.io/v1/evidence-uploads",
"headers": {
"x-art-upload-token": "Zr4Qm8Xk1Lw5Vp9Nb2Ts6Ra0Ey3Hc7Jf4Dg1Ma8Pq5"
}
}
}
]
}
}
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"
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Upload one evidence file
PUT/v1/evidence-uploads
Streams one declared file into private storage. The body is the raw file, not JSON; there is no Idempotency-Key
(the single-use token makes a retry after success a 409). Authorization is BOTH the unguessable one-time token in
x-art-upload-token (43 characters of base64url; a header so it never appears in URLs or logs) AND the uploader's own
passkey session (with CSRF and Origin). Human only.
Content-Type must equal the content_type declared at submission and Content-Length must equal the declared size
(415 / 400 or 413, code invalid_request). The bytes are re-validated against the declared type; a bad file burns
the token (submit again), a transient storage failure hands it back. An unknown, someone else's, expired or revoked token
is 404; an already-used one is 409.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
x-art-upload-token | header | Yes | string | The one-time upload token from the evidence submission. pattern ^[A-Za-z0-9_-]{43}$ |
x-art-csrf | header | Yes | string | The csrf_token from GET /auth/session. Required on every session-authenticated mutation.example Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA |
Request body
application/pdf, required: string (binary)
The file bytes.
image/png, required: string (binary)
The file bytes.
image/jpeg, required: string (binary)
The file bytes.
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Stored. | application/json EvidenceFileStored |
| 400 | invalid_request. Malformed JSON, an invalid or unknown field (see field_errors), a missing or malformed
Idempotency-Key, or a bad query. Also used with status 413 (body over 64 KiB) and 415 (wrong Content-Type). | application/json Error |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 403 | forbidden. Authenticated but not allowed: a role that does not permit the action, a service token on a human-only operation,
a missing or wrong CSRF token, or a request Origin that is not allowed. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 409 | conflict. The record changed since you read it (stale expected_version), the action is not valid in the record's
current state, the Idempotency-Key was reused with a different request, or the same request is still in flight. | application/json Error |
| 503 | unavailable (temporarily unavailable; also a feature that is not configured in this deployment) or
provider_unavailable (an identity provider is not configured). Both are 503. During maintenance (for example a database
recovery) every mutating request answers unavailable "The directory is under maintenance; try again later." Reads keep working. | application/json Error |
200 example
{
"id": "evf_5a8c1f4h7k0m3p6s9v2x5z8b1d",
"evidence_id": "evi_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"status": "stored",
"size": 182044,
"sha256": "9f2c4ab07d1e5b83c6a0f4d92e7b1a35c8d0e6f4a2b9713d5c8e0f1a2b3c4d5e"
}
400 example
{
"code": "invalid_request",
"message": "Invalid organization.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "legal_name",
"message": "must be 1-200 characters"
}
]
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
409 example
{
"code": "conflict",
"message": "The record changed since you read it; reload and retry with the current version.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Poll an asynchronous job
GET/v1/jobs/{id}
The job's owner only (the principal that started it); anything else is 404.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | string | example job_2k5n8r1v4y7b0e3h6m9q2t5x8a |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Job state. | application/json Job |
| 401 | unauthenticated (no or invalid credentials, or the session has not completed the passkey ceremony) or
step_up_required (the passkey assertion is too old; also returned to service tokens, which can never step up).
Both are 401; tell them apart by code. | application/json Error |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
| 503 | unavailable (temporarily unavailable; also a feature that is not configured in this deployment) or
provider_unavailable (an identity provider is not configured). Both are 503. During maintenance (for example a database
recovery) every mutating request answers unavailable "The directory is under maintenance; try again later." Reads keep working. | application/json Error |
200 example
{
"id": "job_2k5n8r1v4y7b0e3h6m9q2t5x8a",
"kind": "verify_dns",
"state": "done",
"attempts": 1,
"result": {
"outcome": "verified",
"claim_state": "pending_identity",
"evidence_id": "evi_6d9g2j5m8q1t4w7z0c3f6h9k2n"
},
"created_at": "2026-10-05T12:02:00.000Z",
"updated_at": "2026-10-05T12:02:05.000Z"
}
401 unauthenticated
{
"code": "unauthenticated",
"message": "Sign in required.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
401 step_up_required
{
"code": "step_up_required",
"message": "Recent passkey verification required for this action.",
"request_id": "req_5a8c1f4h7k0m3p6s9v2x5z8b1d"
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}