When a request fails, we tell you why in the error's code and message, plus a hint and details when there is something to add. The code says what kind of failure it is, the message explains what happened, and the hint suggests what to do next.
Branch on the code in your program, never on the message: we may reword a message, but the code stays. The tables below give the cause of every code and how to recover from it.
Shell
curl-s-X POST "$ACCOUNTS_URL/v1/silicons/login"-H'Content-Type: application/json'\-d'{"id":"si:scout","stk":"stk-000000000000"}'
JSON
{"error":{"code":"invalid_credentials","message":"Sign-in failed: no Silicon has this si:id, or the STK is wrong. Both cases get this same answer, so ids can't be probed.","hint":"Check the si:id (use the current one; ids can change) and the STK (stk- followed by the hex characters shown once at creation or rotation). 10 wrong STKs in a row lock sign-in for 1 minute. A lost STK can be replaced by the Silicon's custodian (`silicon-accounts silicon rotate-stk`)."}}
423 and 429 also set the Retry-After header. 401 responses are Cache-Control: no-store. 5xx
bodies never describe our internals.
POST /v1/oauth/token, /v1/oauth/revoke and /v1/oauth/introspect answer with RFC 6749
bodies, {"error": "invalid_grant", "error_description": "…"}, because that's what OAuth
libraries expect (OAuth errors).
Always log the X-Request-Id response header with an error. It identifies the request in our
logs, and silicon-accounts report and POST /v1/reports take it in the message.
a body was sent without Content-Type: application/json (or, for imports, text/csv)
invalid_body
400
the body couldn't be read (connection broken mid-upload)
invalid_query
400
a query parameter is missing, has the wrong type or an unknown value (named)
invalid_path
400
a path parameter is malformed
invalid_cursor
400
cursor isn't a next_cursor from this list; pass it unchanged or omit it
invalid_request
400
the request is missing something it needs (the message names it)
validation_failed
422
fields are missing, of the wrong type, invalid, or unknown: details.fields maps each path (branding.radius, scopes[3]) to its problem; every problem is reported at once
payload_too_large
413
the body is over the route's limit (details.limit_bytes: 64 KB by default)
unsupported_media_type
415
a photo upload's Content-Type isn't PNG, JPEG, WebP or GIF
route_not_found
404
no endpoint has this path
method_not_allowed
405
the path exists with other methods (the Allow header lists them)
Idempotency-Key isn't 1 to 200 visible ASCII characters (no spaces)
idempotency_key_reused
409
the key was used for a different body on this endpoint; use a new key for a new request
idempotency_in_progress
409
a request with this key is still running; retry in a few seconds
idempotency_result_unavailable
409
the stored secret-bearing result can no longer be decrypted, so it isn't run again; check the current state (e.g. list your Silicons) before retrying with a new key
idempotency_key_required
400
reserved for endpoints that require a key (none currently do)
no credentials; sign in (silicon-accounts login) or send the app's Basic credentials
account_auth_required
401
app credentials (Basic) were sent to an endpoint that acts for an account
invalid_authorization
401
the Authorization header is unreadable or uses an unsupported scheme
invalid_token
401
not an access token, a bad signature, or expired (access tokens last 30 minutes: refresh)
token_wrong_audience
401
an app's token was used where a first-party (aud = silicon-accounts) token is needed, or a developer-platform token (aud = developer, details.aud) outside the routes it may use (GET /v1/me, GET /v1/session, GET /v1/me/owned-apps and the owner routes under /v1/apps/{app_id}/…); the message names the method and route
identity_token_not_accepted
401
an identity token (token_use: identity, made for AWS, Google Cloud or Entra) was sent as a bearer token; send the access token
token_revoked
401
the sign-in behind the token ended (signed out, STK rotated, account deleted, refresh token reuse, a removed key or trust); the message says when and why; sign in again
session_expired
401
the session cookie was signed out, revoked or expired
account_deleted
401 / 403 / 404 / 409
the account was deleted: 401 for its own tokens, 403 at Silicon sign-in, 404 at lookups, 409 when it happened during the request
origin_not_allowed
403
a cookie-authenticated POST/PUT/PATCH/DELETE came without the account site's Origin; use a Bearer token instead of the cookie
carbon_only
403
a Silicon called an endpoint for Carbons (emails, phones, custodian side…)
silicon_only
403
a Carbon called an endpoint for Silicons (/v1/me/webhook…: its own webhook, deliveries and replay)
account_not_active
403
the account isn't active (pending custodian, unfinished import)
app_credentials_required
401
an app endpoint got no credentials
invalid_app_credentials
401
unknown app_id, wrong secret, or malformed Basic header
app_disabled
403 (400 in /v1/flows, 401 at userinfo)
the app is disabled
app_mismatch
403
app credentials were used on another app's /v1/apps/{app_id} URL
not_app_owner
403
an account that isn't one of the app's authors (its owner or an accepted co-author) tried to manage it
unknown_app
404 (400 in /v1/flows)
no app has this app_id
request_token_required
401
GET /v1/silicons/requests/{id} without Bearer sarq_…
invalid_request_token
401
the sarq_ token doesn't belong to this request
internal_token_required
401
/v1/internal/* without the internal token (Silicon Apps only)
invalid_internal_token
401
the internal token is wrong
internal_api_disabled
403
the server has no internal token configured
access_removed
401
at userinfo: the account removed your app's access
membership_inactive
401 at userinfo, 403 for proofs
the account has no active membership with your app
over a limit (Limits); wait Retry-After / details.retry_after_seconds. The message names the limit ("the limit is 120 per minute"); id changes add details.limit, window_seconds, retry_at; imports add row budget details
verification_locked
423
10 wrong codes in a row for this address; every code to it waits 60 seconds (details.locked_until)
login_locked
423
10 wrong STKs in a row for this Silicon; sign-in waits 60 seconds
imports_busy
503
the server is already parsing its maximum of imports; retry after Retry-After (15 s)
wrong code (details.remaining_attempts for the address), or not 6 digits (not counted). The 10th wrong one in a row has remaining_attempts: 0, details.locked_until and Retry-After
code_expired
410
older than 10 minutes, or replaced by a resend; send a new one
code_already_used
409
this code was already accepted
challenge_not_found
404
unknown challenge_id (or one of another flow)
no_code_sent
409
a resend (or a requirement verify) before any code was sent in this flow
the redirect_uri isn't registered exactly; never redirected to
invalid_scope
400
an unknown scope (with details.redirect_to)
unsupported_response_type
400
response_type other than code
method_not_enabled
400 / 403
the app didn't enable that method (or no managed credentials exist)
flow_not_found
404
unknown flow id
flow_not_bound
403
the request lacks this flow's sa_flow cookie: continue in the browser that started it
flow_expired
410
flows last 60 minutes; start again from the app
invalid_step
409
the flow is at another step (the message names the allowed ones)
flow_completed / flow_failed
409
the flow ended; GET /v1/flows/{id} returns its redirect_to
flow_changed
409
the flow moved on in another tab while this request ran, or the app changed its flow and the details page is gone: GET /v1/flows/{id} shows where it is now
account_changed
409
the browser is now signed in as a different account than the flow's
account_unavailable
409
the address belongs to an account that can't sign in
session_required
401
"continue as" without a browser session
continue_not_allowed
403
the app turned off remember_browser
reauthentication_required
403
the app asked for prompt=login
email_domain_not_allowed
403
the app accepts only some email domains
signup_not_allowed
403
the app takes no new accounts (allow_signup: false)
signup_not_bound
403
the sign-up belongs to another browser
signup_expired
410
sign-ups last 48 hours; verify the address again
signup_already_completed
409
this sign-up already created an account; sign in instead
detail_not_on_page
409
details/add for a detail that isn't on the page on screen
no_previous_page
409
details/back on the first page
requirements_missing
409
a required email or phone of the page isn't on the account yet (details.missing): add it with details/add + details/verify
invalid_state
400
a provider callback with a malformed state
Some codes come in flow.error (and in ?error= on your redirect URI) rather than as HTTP
errors: login_required, consent_required, interaction_required (prompt=none), access_denied
(cancelled on a details or review page), provider_cancelled, provider_error, provider_token_invalid,
provider_unavailable, provider_config_changed, provider_answer_elsewhere,
provider_email_invalid, email_not_verified, hosted_domain_mismatch (a Google account
outside the app's google.hosted_domain), signup_expired, session_changed (linking),
identity_in_use, email_in_use, email_limit_reached (linking, as ?link_error=).
at sign-in: not stk- + 8 to 32 hex characters (creating a Silicon or rotating its STK reports a bad chosen STK as validation_failed on stk)
invalid_credentials
401
unknown si:id or wrong STK (one answer for both)
custodian_pending
403
the custodian hasn't accepted yet (details.custodian, request_id, expires_at)
custodian_declined
403
the custodian declined; the account was released
custodian_expired
403
nobody accepted within 14 days; the account was released
custodian_request_not_found
404
no such request (or not addressed to you)
custodian_request_not_pending
409
already accepted, declined, expired or cancelled (details.status)
custodian_request_expired
410
the 14 days ran out
custodian_request_pending
409
the Silicon already has a pending request
silicon_not_pending
409
accepting an initial request for a Silicon that is no longer waiting
silicon_not_active
409
a transfer of a Silicon that isn't active
already_custodian
409
you already are its custodian
transfer_pending
409
one transfer at a time (details.request_id): cancel it first
transfer_not_found
404
no pending transfer to cancel
transfer_to_self
422
a transfer must go to another Carbon
transfer_stale
409
the custodian changed after the transfer was requested
webhook_not_set
409
a test ping, secret rotation or replay without a webhook URL: set one first
invalid_assertion
401
a key sign-in's assertion is malformed, expired, for another aud, not signed by a live key of that Silicon, or was used before: sign a fresh one
key_exists
409
the Silicon already has this key (details.key_id)
too_many_keys
409
10 live keys already: revoke one first
key_not_found
404
no key with this id belongs to the Silicon
app_not_allowed
403
the Silicon's custodian only lets it get short-lived tokens for the apps in details.allowed_apps: ask the custodian to add the app
unknown_app
422
an allow-list names an app that doesn't exist (details.unknown)
federation_exists
409
the Silicon already trusts these tokens (details.federation_id)
too_many_federations
409
20 live trusts already: remove one first
federation_not_found
404
no trust with this id belongs to the Silicon
issuer_unreachable
422
a new trust's issuer has no discovery document we can read over https from a public address, it names another issuer, or its jwks_uri isn't public https (details.issuer)
federated_session
403
a sign-in that came from a trusted outside token tried to add a key or a trust: do it from the STK or a key, or as the custodian
custodian_only
403
only the Silicon's custodian chooses its identity token audiences
audience_not_allowed
403
the Silicon's custodian hasn't allowed this audience for identity tokens (details.allowed_audiences)
the app already has a subscription with this delivery (details.subscription_id); an app has one webhook and one stream subscription at most: change that one instead
subscription_not_found
404
no subscription with this id belongs to the app
invalid_updates
422
updates names something that isn't an update (details.allowed lists them)
invalid_webhook_events
422
PUT /v1/apps/{app_id}/webhookevents names something that isn't an update
stream_subscription_required
409
an app opened GET /v1/events/stream without a stream subscription: create one with POST /v1/apps/{app_id}/subscriptions{"delivery":"stream"}
unknown_event_id
400
the stream's Last-Event-ID or after isn't an event of this feed: resume with the last id this stream sent you, or connect without one
too_many_streams
429
5 streams are already open for this app or account: close one (one stream carries every event of the feed), then retry after Retry-After
stream_capacity_reached
503
this server is full or restarting: reconnect after Retry-After with Last-Event-ID
When we end a stream on purpose, we send event: stream.closed with a reason first. See
Event stream.
the Accounts-Version header names a version this deployment doesn't serve (details.supported, details.current): send a supported one or leave the header out
capabilities_missing
422
GET /v1/capabilities?require=… named a capability that is unknown or unsupported (details.missing, details.supported, details.available)
the subject token isn't a live access token (details.reason: not_an_access_token, invalid, expired, revoked)
subject_token_wrong_app
403
the subject token belongs to another app (details.token_app)
app_verification_single_app
422
an app verification request named apps in audiences: an app verification proof is for exactly one app; send {"receiving_app": "…"} once per app (details.field, details.apps)
unknown_receiving_app
400
the receiving app doesn't exist (details.app_ids)
invalid_receiving_app
400
the issuer itself, or Silicon Accounts itself (silicon-accounts, developer)
receiving_app_disabled
403
the receiving app is disabled
invalid_proof_refresh_token
400
not a sapr_ token, or unknown (mistyped, another environment, or its proof ended over 30 days ago)
not_issuing_app
403
only the issuing app refreshes or revokes a proof
proof_refresh_token_reused
400
a used refresh token was presented: the proof is now revoked
proof_revoked
410
the proof was revoked, or its User verification sign-in ended (details.reason, revoked_at)
proof_expired
410
past the proof's lifetime
invalid_proof_id
400
not a UUID
proof_not_found
404
not a proof you can see
A proof that doesn't verify is never an error: POST /v1/proofs/verify answers 200 with
{"valid": false, "expires_at": null}.
A fault on our side. Retry later. If it continues, report the details.request_id so we can find the request.
database_unavailable
503
the database is unreachable; nothing was changed; retry in a few seconds
request_timeout
503
the request ran past its time budget (30 s, 60 s for uploads, 5 min for imports)
web_not_built
503
(static hosting only) the account site build is incomplete
dev_outbox_disabled
404
the development outbox is off
When a request is refused by a framework layer rather than a handler, the code comes from the status:
forbidden 403, conflict 409, gone 410, locked 423, not_acceptable 406,
length_required 411, uri_too_long 414, range_not_satisfiable 416, not_implemented 501,
bad_gateway 502, unavailable 503, gateway_timeout 504, request_failed (other 4xx).
These come from /v1/oauth/token, /revoke and /introspect as {"error", "error_description"},
with Cache-Control: no-store.
error
Status
Cause
invalid_request
400 (413 for a body over 64 KB)
a parameter is missing, repeated or malformed; the client authenticated twice
invalid_client
401
unknown app, wrong secret, disabled app, or no credentials; WWW-Authenticate: Basic realm="Silicon Accounts"
invalid_grant
400
the code, refresh token, SLT or device code is unknown, expired, already used (a reused refresh token or code also revokes its sign-in), revoked, another app's, or its account is deleted or removed the app's access; a redirect_uri or PKCE mismatch; a token exchange's outside token refused, with the reason and its code in brackets: invalid_federated_token (malformed, unsafe algorithm, bad signature, unknown key, expired, not yet valid, used before), no_matching_trust (no trust of the Silicon accepts its issuer, audience and claims), issuer_unavailable (the issuer's keys couldn't be read)
unauthorized_client
400
a public client (client_id without a secret) used a grant that needs the secret, an app without device_flow used the device-code grant, or an app asked for a token exchange (it signs a Silicon into Silicon Accounts itself)
unsupported_grant_type
400
the grant isn't supported (the description names the alternative)
invalid_scope
400
a refresh asked for more scopes than were granted
authorization_pending
400
device sign-in not approved yet; keep polling
slow_down
400
polled within 5 seconds of the last poll; add 5 seconds
access_denied
400
the Carbon denied the device sign-in
expired_token
400
the device code expired (10 minutes)
rate_limited
429
more than 60 token exchanges per minute from one address; wait Retry-After seconds
server_error
500
a fault on our side (request id in the description)
a non-Silicon-Accounts error body (a proxy or load balancer answered)
Webhook verification (WebhookError) has EmptySecret, MissingHeader, InvalidTimestamp,
TimestampOutOfTolerance (more than 5 minutes off), InvalidSignatureFormat,
SignatureMismatch and InvalidBody. Refuse the delivery (answer 400 or 401) in every case.
Details: Rust client.
The silicon-accounts CLI prints these same codes (with --json, as
{"error":{"code","message","hint","exit_code","status","request_id","details"}}) and maps them
to exit codes (Exit codes). It also has codes of its own, for problems it
finds without asking us (not_signed_in, wrong_account_kind, invalid_arguments,
corrupt_state_file, unknown_topic…), and CLI error codes lists
them with their exit codes.