Docs For organizations
API access
Sessions, CSRF and origin checks, service tokens for automation, idempotency keys on every mutation, and passkey step-up.
The console is a client of the public API, so everything it does you can automate. This page covers the rules every authenticated call follows. The API reference lists the authentication each operation accepts.
Sessions
A browser session is the __Host-art_session cookie, valid for 12 hours, for someone who has signed in with GitHub or Google and completed a passkey assertion. On every mutation a session also needs:
x-art-csrf: thecsrf_tokenfromGET /auth/session;Origin: exactly the directory origin,https://svid.io;Content-Type: application/json(any other declared type on a mutation is415), with a body of at most 64 KiB.
Service tokens
For server-to-server automation, an owner can create a service token: POST /v1/organizations/{id}/service-tokens (human, step-up).
{"name": "ci-claims", "scopes": ["domain_administrator"], "expires_in_days": 30}
scopesis a subset ofissuer_administrator,domain_administratorandviewer. A service token is never an owner.expires_in_daysis 1 to 90 (default 30).- The secret (
art_st_…) is returned once; only its hash is stored. An idempotent replay returns the record withtoken: nullandtoken_redacted: true.
Send it as Authorization: Bearer art_st_…. A bearer request without an Origin header skips the origin check; one that sends Origin must still match. An Authorization header that is present but invalid is 401, never a fall-back to a cookie.
Service tokens can do what their scopes allow except human-only operations, which answer 403, and they can never step up. In practice they suit reading organization records, filing claims and running DNS checks; registering issuers, rotation, withdrawal, delegations and evidence uploads need a person.
Owners list tokens with GET /v1/organizations/{id}/service-tokens (no secrets) and revoke one with DELETE /v1/organizations/{id}/service-tokens/{tokenId}. Revocation deliberately needs no step-up, so it stays fast during an incident. Removing or demoting a member revokes the tokens they created for that organization.
Idempotency keys
Every /v1 mutation except the binary evidence upload requires an Idempotency-Key header: 1 to 255 characters of A-Z a-z 0-9 . _ : -. Use a new key for each logical action and reuse it only to retry that same action.
| You send | You get |
|---|---|
| The same key and the same request | The stored status and body, with idempotency-replayed: true |
| The same key and a different request | 409 conflict |
| The same key while the first request is still running | 409 conflict |
Keys are kept for 24 hours per principal. Only 2xx responses and the deterministic client errors 400, 404, 409 and 422 are stored, so a 401 step_up_required, 403, 429 or 5xx can be retried with the same key. One-time secrets (DNS challenge values, control-challenge nonces, service-token secrets, upload tokens) are shown once and redacted in a replay.
Step-up
Sensitive actions need a passkey assertion that is at most 300 seconds old:
- registering, rotating or withdrawing an issuer, and every issuer-control proof;
- creating, accepting or withdrawing a delegation;
- changing or removing members, inviting people and accepting an invitation;
- creating service tokens and registering an additional passkey;
- withdrawing or appealing a claim.
Without a fresh assertion the API answers 401 step_up_required. Run the passkey assertion again (POST /auth/passkeys/authentication/options, then /verify), which sets mfa_at on the session and satisfies step-up for the next 300 seconds, then retry with the same Idempotency-Key.
Optimistic concurrency
Records that change carry a version. Updates send it back as expected_version; if someone else changed the record first, the answer is 409 conflict and you should reload and retry.
Errors
Every failure is { "code", "message", "request_id", "field_errors"? }. Branch on code, never on message. Every response carries x-request-id; quote it when you report a problem.
code |
Status |
|---|---|
invalid_request |
400 (413 for an oversized body, 415 for the wrong content type) |
unauthenticated, step_up_required |
401 |
forbidden |
403 |
not_found |
404 |
conflict |
409 |
semantic_check_failed |
422 |
rate_limited |
429 |
provider_unavailable, unavailable |
503 |
Unknown, other-tenant and invisible records all answer the same 404, so tenants cannot be enumerated.