Preview — not for production reliance
Documentation

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: the csrf_token from GET /auth/session;
  • Origin: exactly the directory origin, https://svid.io;
  • Content-Type: application/json (any other declared type on a mutation is 415), 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).

json
{"name": "ci-claims", "scopes": ["domain_administrator"], "expires_in_days": 30}
  • scopes is a subset of issuer_administrator, domain_administrator and viewer. A service token is never an owner.
  • expires_in_days is 1 to 90 (default 30).
  • The secret (art_st_…) is returned once; only its hash is stored. An idempotent replay returns the record with token: null and token_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.