Skip to main content

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:

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.