Docs API reference
Auth
Sign-in, sessions and passkeys.
10 operations. See the API overview for authentication, errors, idempotency and caching rules that apply to all of them.
Sign-in methods this deployment offers
GET/auth/providers
For the console sign-in page. Never reveals configuration values. test_login is true only where the test identity provider
is mounted and enabled (never in production); on staging test_login_key_required is also true.
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Available sign-in methods. Headers: Cache-Control: no-store | application/json AuthProviders |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | application/json Error |
200 example
{
"github": true,
"google": true,
"test_login": false,
"test_login_key_required": false
}
404 example
{
"code": "not_found",
"message": "No such resource.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Start an OIDC sign-in
GET/auth/login/{provider}
Redirects the browser to the identity provider (authorization code with PKCE and a nonce). Sets the single-use,
HttpOnly browser-binding cookie __Host-art_oidc. Rate limited per client.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
provider | path | Yes | one of "github", "google" | |
return_to | query | No | string | A same-site absolute path to return to after sign-in (at most 512 printable ASCII characters, starting with a single /). Anything else is replaced by the default.at most 512 characters example /console/ |
Responses
| Status | Description | Body |
|---|---|---|
| 302 | Redirect to the identity provider. Headers: Location, Set-Cookie | None |
| 404 | not_found. No such resource, or one the caller may not see (the two are indistinguishable by design). | 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 |
404 example
{
"code": "not_found",
"message": "No such resource.",
"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"
}
OIDC redirect target
GET/auth/callback/{provider}
Where the identity provider returns the browser. Exchanges the code, upserts the principal, revokes any older session the
browser held and sets the session cookie __Host-art_session (HttpOnly, Secure, SameSite=Lax, 12 hours), then
redirects to the original return_to. The browser-binding cookie is cleared on every outcome. A session created here has
no MFA yet: complete a passkey ceremony before using the API. Not for direct use by API clients.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
provider | path | Yes | one of "github", "google" | |
code | query | No | string | |
state | query | No | string | |
error | query | No | string | Set by the provider when the user cancelled. |
Responses
| Status | Description | Body |
|---|---|---|
| 302 | Signed in; redirect to the return path. Headers: Location, Set-Cookie | None |
| 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 |
| 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 |
| 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"
}
]
}
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"
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Test identity provider (non-production)
POST/auth/test-login
Exists only in dev, test and staging deployments with the test IdP enabled; it is not mounted in production.
Creates a session for a synthetic principal, without MFA. On staging the header x-art-test-idp-key must equal the
deployment's tester key; a wrong or missing key (like a deployment without the route) answers 404, not 401.
Sets the session cookie.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
x-art-test-idp-key | header | No | string | Tester key (staging only). |
Request body
application/json, required: TestLoginRequest
| Field | Type | Required | Description |
|---|---|---|---|
subject | string | Yes | pattern ^[A-Za-z0-9._@-]{1,128}$ |
email | string (email) | Yes | at most 254 characters |
Example
{
"subject": "tester-1",
"email": "tester-1@example.com"
}
Responses
| Status | Description | Body |
|---|---|---|
| 201 | Session created. Headers: Set-Cookie, Cache-Control: no-store | application/json TestLoginResponse |
| 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 |
| 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 |
| 429 | rate_limited. A per-client or per-record limit was reached; retry later. | application/json Error |
201 example
{
"principal_id": "prn_6d9g2j5m8q1t4w7z0c3f6h9k2n",
"has_passkey": false,
"mfa": false
}
400 example
{
"code": "invalid_request",
"message": "Invalid organization.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w",
"field_errors": [
{
"field": "legal_name",
"message": "must be 1-200 characters"
}
]
}
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"
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Current session
GET/auth/session
The signed-in principal, whether the session has completed a passkey assertion (mfa), the CSRF token to send as
x-art-csrf on mutations, and the principal's roles. Works for a session that has not completed MFA yet.
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Session details. Headers: Cache-Control: no-store | application/json SessionInfo |
| 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 |
| 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
{
"principal": {
"id": "prn_6d9g2j5m8q1t4w7z0c3f6h9k2n",
"idp": "github",
"email": "ada@example.com",
"display_name": "Ada Lovelace"
},
"mfa": true,
"mfa_at": "2026-10-05T11:55:00.000Z",
"has_passkey": true,
"csrf_token": "Kk3jVq0bYw2p0mT1aZc9sD8eF4gH6iJ7nL5oP2rQ1uA",
"roles": {
"platform": [],
"organizations": [
{
"organization_id": "org_7k3m9q2x8d5f1h4j6n0p2r4t8v",
"role": "organization_owner"
}
]
},
"expires_at": "2026-10-05T23:50:00.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"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Sign out
POST/auth/logout
Revokes the current session and clears the cookie.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
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 |
|---|---|---|
| 204 | Signed out. Headers: Set-Cookie | None |
| 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 |
| 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"
}
403 example
{
"code": "forbidden",
"message": "Your role does not permit this action.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Begin passkey registration
POST/auth/passkeys/registration/options
WebAuthn registration options (PublicKeyCredentialCreationOptionsJSON). Registering the first passkey needs only a
signed-in session. Registering an additional passkey needs a passkey-verified session and a fresh step-up. Rate limited.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
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 | Registration options for navigator.credentials.create().Headers: Cache-Control: no-store | application/json WebAuthnOptions |
| 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 |
| 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
{
"rp": {
"name": "Agentic Root Trust",
"id": "svid.io"
},
"user": {
"id": "cHJuXzZkOWcyajVtOHExdDR3N3owYzNmNmg5azJu",
"name": "ada@example.com",
"displayName": "Ada Lovelace"
},
"challenge": "m3Qx0Zk8Vb1Nn7Yp2Tt6Ra4Ew9Hs5Dc",
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
}
],
"timeout": 60000,
"attestation": "none"
}
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"
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Finish passkey registration
POST/auth/passkeys/registration/verify
Verifies the attestation, stores the credential and marks the session as passkey-verified (MFA).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
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: PasskeyCeremonyRequest
| Field | Type | Required | Description |
|---|---|---|---|
response | object | Yes | The RegistrationResponseJSON or AuthenticationResponseJSON from the browser. |
Example
{
"response": {
"id": "AQIDBAUGBwgJ",
"rawId": "AQIDBAUGBwgJ",
"type": "public-key",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0",
"attestationObject": "o2NmbXRkbm9uZQ"
},
"clientExtensionResults": {}
}
}
Responses
| Status | Description | Body |
|---|---|---|
| 201 | Credential registered; the session now has MFA. | application/json PasskeyRegistered |
| 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 |
| 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
{
"credential_id": "AQIDBAUGBwgJ",
"mfa": true,
"mfa_at": "2026-10-05T11:55:00.000Z"
}
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"
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Begin passkey assertion (MFA or step-up)
POST/auth/passkeys/authentication/options
WebAuthn assertion options (PublicKeyCredentialRequestOptionsJSON). Use after sign-in to complete MFA, and again to
refresh the step-up window before a sensitive action.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
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 | Assertion options for navigator.credentials.get().Headers: Cache-Control: no-store | application/json WebAuthnOptions |
| 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 |
| 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
{
"challenge": "Pq7Lw2Hn5Bv8Xc1Zd4Ke6Ry9Tj3Um0Fs",
"rpId": "svid.io",
"allowCredentials": [
{
"type": "public-key",
"id": "AQIDBAUGBwgJ"
}
],
"userVerification": "required",
"timeout": 60000
}
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"
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
Finish passkey assertion
POST/auth/passkeys/authentication/verify
Verifies the assertion and sets mfa_at on the session. Step-up is satisfied for the next 300 seconds.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
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: PasskeyCeremonyRequest
| Field | Type | Required | Description |
|---|---|---|---|
response | object | Yes | The RegistrationResponseJSON or AuthenticationResponseJSON from the browser. |
Example
{
"response": {
"id": "AQIDBAUGBwgJ",
"rawId": "AQIDBAUGBwgJ",
"type": "public-key",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0In0",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2M",
"signature": "MEUCIQDexample",
"userHandle": "cHJuXzZkOWcyajVtOHExdDR3N3owYzNmNmg5azJu"
},
"clientExtensionResults": {}
}
}
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Assertion verified. | application/json PasskeyAuthenticated |
| 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 |
| 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
{
"mfa": true,
"mfa_at": "2026-10-05T12:00:00.000Z"
}
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"
}
429 example
{
"code": "rate_limited",
"message": "Too many requests.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}
503 example
{
"code": "unavailable",
"message": "Temporarily unavailable.",
"request_id": "req_3m6p9s2v5y8b1e4h7k0n3q6t9w"
}