Error codes
Every error response from Driftstack is RFC 9457
application/problem+json. The stable identifier is
the type URI; dispatch your client logic on
type, not on the HTTP status or the human-readable
title / detail.
Response shape
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/problem+json
{
"type": "https://errors.driftstack.dev/rate-limited",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit for \"sessions:create\" exceeded for tier \"starter_v3\".",
"retry_after_seconds": 12
}
Required fields: type, title,
status. Optional: detail (human
message), instance (request id when present), and
problem-specific extensions (flat keys on the top-level, per RFC
9457 §3.2). Always check type, not the HTTP status
alone — the same status can correspond to multiple types
(e.g. 429 covers both rate-limited and
concurrency-limit; 401 covers five distinct types).
Reference
The full URI is https://errors.driftstack.dev/<slug>
where <slug> is the value in the first column.
Slugs are stable; they will not be renamed without a deprecation
window.
| Type slug | HTTP | When | Action |
|---|---|---|---|
bad-request | 400 | The request broke a basic rule (e.g. capacity ≤ 0, malformed URL). | Read `detail`; the field at fault is named. |
validation-failed | 400 | Request body / params failed validation. | Fix the field listed in the response detail. |
unauthorized | 401 | Authorization header missing or malformed. | Send `Authorization: Bearer ds_live_…` on every request. |
invalid-key | 401 | API key is malformed (wrong prefix) or unknown. | Confirm the key in your secret manager matches the dashboard. |
revoked-key | 401 | API key was revoked (manually or via rotation grace expiry). | Mint a new key from the dashboard. |
expired-key | 401 | API key passed its `expires_at` (set by rotation). | Roll to the post-rotation key; old key auto-expires after the 24h grace window. |
invalid-auth-token | 400 | Signup-verification / password-reset / magic-link token was malformed, consumed, or expired. | Request a fresh token via the dashboard. |
email-not-verified | 403 | Login attempted before the user verified their signup email. | Click the verify link in the welcome email, or use Resend verification email on /verify-email. |
invalid-credentials | 401 | Login failed (wrong password) OR account does not exist — both surface the same shape on purpose (no enumeration). | Verify credentials; do not retry-loop. |
forbidden | 403 | Key is valid but lacks the required scope for this endpoint. With `code: "device_not_on_plan"`: a session was started from a profile set up on a device the account’s plan does not include (on the Trial, any device but the iPhone 13 and iPhone 13 mini). | Create a key with the right scope in the Driftstack dashboard. For `device_not_on_plan`, choose a plan that includes the device, or use a profile on an included device. |
dashboard-only | 403 | The request was made with an API key for something a person does in the dashboard: checkout, the billing portal, creating or changing an order, creating an API key, accepting a team invite or the terms, or signing other dashboard sessions out. | Do it in the Driftstack dashboard. |
mfa-step-up-required | 403 | Endpoint requires a fresh MFA step-up confirmation. Extension `requires_mfa_step_up: true`. | Confirm your two-factor code again in the Driftstack dashboard, then retry. |
not-found | 404 | Resource id does not exist OR you do not have access (both surface as 404; no enumeration). | Verify the id; cross-account lookups return 404 by design. |
conflict | 409 | State conflict (duplicate, a disabled or deleted resource that cannot be brought back, idempotency-key replay with different body). | Read `detail` — often "mint a fresh resource instead". |
legal-acceptance-required | 409 | The account owner has not accepted the current Terms of Service, Privacy Policy, Acceptable Use Policy and Data Processing Agreement, so the request (for example creating an API key) was refused. Extension `pending_acceptances` lists each document and version. A sign-up without the terms box gets the same type with `code: "terms_not_accepted"`. `POST /v1/oauth/token` refuses the same way (409) when the account that approved the app has not accepted them: the body also carries `error: "invalid_grant"`, the code stays unused, and the same code can be exchanged once they accept, while it is still valid (codes last five minutes; after that, start the authorization again). | The owner accepts the documents in the banner at the top of the Driftstack dashboard, then retry. |
concurrency-limit | 429 | Account has hit its tier-bound concurrent-session cap. Extensions `current_sessions` + `limit`. On an AI agent-session message with `ai_tasks_in_flight: true` it is a different limit: 3 AI tasks are already running on the account (this limit answers this way or as `rate-limited`, depending on how the account’s AI is billed), and no step ran. | At create: wait for a running session to finish, or upgrade the tier. See https://docs.driftstack.io/guides/concurrency/. With `ai_tasks_in_flight: true`: wait for a task to finish, then send the message again (the same `Idempotency-Key` is fine); do not close sessions, and a higher tier does not raise this limit. |
tier-limit | 429 | A plan limit was reached: a saved-resource cap (e.g. profile count, API keys per account), a profile outside the set the plan can launch (`code: "over_plan_limit"`), or the end of the free trial’s 2 hours of session time (`code: "trial_session_time_used"`). | Upgrade the plan or delete unused resources. For `trial_session_time_used`, choose a plan in the dashboard. |
storage-quota-exceeded | 409 | A profile-backed session-launch was refused because your profiles’ total stored size reached your tier’s storage cap. Extensions `used_bytes` + `cap_bytes` + `tier`. Sessions without a profile are never affected; Enterprise is soft-only (never blocks). | Delete or trim a profile, or upgrade the tier, then launch again. |
profile-in-use | 409 | A session-create carried a `profile_id` that already has a live session — two sessions on the same profile would overwrite each other’s saved cookies/logins. Extension `active_session_id` names the live session. Sessions without a profile are never affected. | End the named session if you started it (or wait for it to finish), then launch again. |
device-unavailable | 409 | A launch named a profile whose device is not offered right now — on hold until the evidence behind it exists, or still in development. Nothing was started. Extensions `archetype` (the profile’s device) + `held_reason` (the reason for the hold in one plain sentence, when there is one). | Launch a profile on a device listed by `GET /v1/archetypes`. |
session-not-ready | 409 | A message reached an agent session whose browser is still starting: it has not yet reported ready (the session’s `ready` is still false). The message waited for it (up to 45 seconds on a streamed response, 20 seconds on a JSON one), and nothing ran. Extensions `code` (`session_not_ready`), `retryable` (`true`) + `retry_after_seconds`; the same wait as the `Retry-After` header. | Send the same message again after `retry_after_seconds` (the same `Idempotency-Key` is fine). Better, wait for the session to read `ready: true` before the first message; a VPN session can take longer to become ready. |
payload-too-large | 413 | The request is too large for where it has to go, and nothing was sent: a body over the endpoint’s size limit, or a file or cookie jar larger than the session takes at once. When the session is the limit, extensions `limit_bytes` + `size_bytes` carry the two sizes. | Send less: a smaller file or cookie jar. For uploads, check a file against the session’s `upload_max_file_bytes` first. |
proxy-validation-failed | 422 | The proxy on a launch failed a live launch-time connectivity test — the server connected through the proxy and tried to reach the internet, which failed, so the launch was blocked (no session was created). Extension `reason`: `unreachable` / `auth_failed` / `timeout` / `egress_blocked`. | Fix the proxy per the `reason` (host/port/online, credentials, or its internet access), then launch again. |
proxy-required | 422 | A session was asked for without one of your saved proxies, so nothing was created (no session, no charge, nothing recorded under the Idempotency-Key). Every agent session runs through a proxy you chose. Extension `code`: `proxy_required`. On hosted Driftstack `POST /v1/sessions` and `POST /v1/profiles/{id}/launch` need one too: they take the same `proxy_id`, and a launch from a profile bound to a proxy uses that proxy instead. | Pass the id of one of your saved proxies as `proxy_id` on `POST /v1/agent-sessions` (with `profile_id` to launch a profile), or on the direct session call (on hosted Driftstack that session's step calls then answer `503 driver-not-integrated`, so drive pages from an agent session). `GET /v1/account/me/proxies` lists them, with a key that has the `account_owner` scope. |
upgrade-required | 426 | A write or a launch came from a desktop sign-in whose app build is older than the server’s minimum, or that sent no build at all. Reads still work, and SDK and API keys are never refused this way. Extension `min_desktop_build` names the build to update to; `code` is `desktop_update_required`. | Update the desktop app to at least `min_desktop_build`, then try again. |
rate-limited | 429 | A rate-limit bucket is empty — or, on an AI agent-session message, the account already has as many AI turns running at once as it may (across its sessions, or on Driftstack’s included AI), in which case no step ran. Extension `retry_after_seconds`; same value as `Retry-After` header. | Wait `retry_after_seconds`, then send the same request again — an AI message may reuse the same `Idempotency-Key`, because nothing ran. The SDKs retry most calls for you; an agent-session message and the transcript stream are yours to retry. See /docs/rate-limits. |
session-destroyed | 410 | Tried to operate on a session that's already been destroyed. | Treat the session as gone; create a new one. |
session-timeout | 504 | A blocking session operation (e.g. wait-for-selector) ran past its timeout. | Increase the timeout if legitimate, or address slowness on the target site. |
internal | 500 | Driftstack hit an unexpected error processing the request. | Retry with exponential backoff. If persistent, email support with X-Request-Id. |
driver-error | 502 | The active browser runtime returned a structured failure. | Do not automatically retry; inspect `detail` and include X-Request-Id when contacting support. |
driver-not-integrated | 503 | The selected operation is not supported by the active browser runtime. | Not retryable; choose a supported operation or contact support with X-Request-Id. |
feature-unavailable | 503 | A capability required by this operation is disabled or unavailable on the deployment. The most common one on a session create is different: `code: "dispatch_no_live_node"` means no machine could take a new session just then (`node_updating: true` while the machines are being updated), and nothing was created. On a stop, `stop_unconfirmed: true` means the stop could not be confirmed just now. On an AI message, `code: "ai_provider_unavailable"` means the AI that comes with the plan is temporarily unavailable: the message was not run and no step ran (a turn on your own key never gets it). | With `code: "dispatch_no_live_node"` (`retryable: true`): wait the `Retry-After` seconds (30, or 60 with `node_updating: true`) and send the same request with the same Idempotency-Key. With `stop_unconfirmed: true`: call stop again. With `code: "ai_provider_unavailable"`: wait `retry_after_seconds` (60; also the `Retry-After` header unless the message was streamed) and send the message again, with a new Idempotency-Key if you sent one; steps sent to POST /v1/agent-sessions/{id}/steps still work. Otherwise not retryable until the capability is available; contact the deployment operator with X-Request-Id. |
maintenance | 503 | A session start (`POST /v1/agent-sessions`, `POST /v1/sessions` or `POST /v1/profiles/{id}/launch`) was refused because every machine that could run the session is in a booked maintenance window. Nothing was created, and sessions already running are not affected. Extensions `code` (`maintenance`), `retryable` (`true`), `until` (when the window ends, ISO 8601 in UTC) + `retry_after_seconds` (the seconds until then; the same wait as the `Retry-After` header). | Send the same request again after `until` (the same `Idempotency-Key` is fine, because nothing was created). |
byok-anthropic-required | 502 | The agent-session turn had no Anthropic key to run on — none sent, none stored, and Driftstack’s included AI not available to it — or, with `key_rejected: true`, Anthropic refused the key it did have (`key_source`, `key_rejected_reason`). No step ran either way. | Without key_rejected: PUT /v1/account/me/byok-anthropic-key to set a stored key, supply x-byok-anthropic-api-key on each request, or opt in to the included AI where the plan offers it — some plans run AI only on your own key and cannot opt in. With key_rejected: replace the key (invalid_or_unauthorized), settle billing with Anthropic (billing), or resolve the disabled organization with Anthropic or use a key from another one (organization_disabled); retrying unchanged fails the same way. |
bundled-llm-budget-exhausted | 402 | Bundled-LLM monthly soft-cap reached on this account (default $20; you set it in the dashboard’s Settings page). Resets at calendar month start. | Raise the cap in the dashboard if it is below the $100 maximum (the settings route answers 403 to any API key), wait until next month, or supply a BYOK Anthropic key (BYOK is always used when present). |
bundled-llm-consent-required | 402 | The deployment offers bundled LLM but you have not yet opted in. Distinct from byok-anthropic-required so dashboards can show a one-click "enable bundled-LLM" button. | Opt in from the dashboard’s Settings page; the settings route (PATCH /v1/account/me/bundled-llm-settings, field `consent`) answers 403 to any API key. Or bring your own Anthropic key: send x-byok-anthropic-api-key on each request, or store one with PUT /v1/account/me/byok-anthropic-key (an account_owner key). |
ai-credits-exhausted | 402 | An AI turn could not run on the account’s AI credits, and nothing ran. Extension `reason`: `balance` (the credits the plan includes are used up; `available_credits` and `required_credits` say how far short, and `resets_at`, when present, is when the next credits arrive — or, with the detail “Your plan has ended…” and `code: "plan_ended"`, the account’s paid plan has ended, and no AI runs on it, your own key included; or, with the detail “Your last payment didn’t go through…” and `code: "payment_failed"`, a card renewal failed while the card is still being retried, and once the last retry fails the plan has ended; a teammate in someone else’s workspace gets `code: "workspace_payment_failed"` or `code: "workspace_plan_ended"` instead), `debt` (AI is paused on the account until it is settled; `debt_reason` says why) or `task_too_large` (this one request is larger than credits can run). | For `balance` with no `code`, wait for `resets_at`, choose a larger plan, or run the turn on your own Anthropic key; with `code: "plan_ended"`, choose a plan; with `code: "payment_failed"`, pay the open invoice or update the card (or, once the last retry has failed, choose a plan); with a `workspace_` code, ask the workspace owner. Waiting or your own key does not help with any `code`. For `task_too_large`, shorten the request or start a new chat. For `debt`, contact support. |
trial-ended | 402 | The account’s free trial has ended — or, with `code: "plan_ended"`, its paid plan has — so a new session was refused. With `code: "payment_failed"`, the last renewal payment didn’t go through: sessions and AI stop at once, with no grace period, while the card is still being retried; once the last retry fails, the plan has ended (`code: "plan_ended"`). Every other call keeps working. After a free trial we keep the account’s profiles, proxies and settings for 30 days; a paid plan that has ended or whose payment failed keeps them. | Choose a plan in the Driftstack dashboard, then start the session again. For `payment_failed`, pay the open invoice or update the card; everything works again as soon as the payment goes through (or, once the last retry has failed, choose a plan). |
pair-mode-conflict | 409 | Another participant took over the pair-mode session first. Extension `winner_client_id` names who now has control, so your dashboard can show "user X is taking over". | Re-fetch the session; do not retry the takeover unless the winner releases the lock. |
pair-mode-invalid-transition | 409 | Pair-mode state machine refused the requested transition (e.g. a handback request before any takeover-grant). Extensions `from` + `transition`. | Re-fetch the pair-mode state; do not retry the transition without resolving the state mismatch. |
email-already-registered | 409 | Signup attempted for an email that already has an account. | Direct the user to /login or to the password-reset flow. |
Stability
The type URI slugs listed above are part of the API
contract — they will not be renamed or removed without a
deprecation window. New types may be added; defensive clients
should treat unknown types as internal
(i.e. retry with backoff and surface detail to the
operator).
OAuth token endpoint
Errors from the OAuth token endpoint follow the standard problem shape:
invalid_client/unauthorized_client→typeisunauthorized.invalid_request/invalid_scope/invalid_grant/access_denied→typeisbad-request, except a 409invalid_grantfrom the token exchange, whosetypeislegal-acceptance-required(the approving account owes acceptance of a legal document;pending_acceptanceslists them).
Status outages
For platform-level outages (everyone hits 5xx), check status.driftstack.io. Subscribe to email notifications from the same page if you'd like to be told before your dashboard refreshes.
Support
Persistent or surprising errors:
[email protected].
Include the X-Request-Id header from the response —
every Driftstack response carries one, and we use it to find the
corresponding server logs.