Drıftstack docs
Docs

Agent sessions: the session resource and errors

Part of the Agent sessions reference, which lists every call.

Resource shape

{
  "id": "agt_<uuid>",
  "account_id": "<uuid>",
  "driftstack_session_id": "ses_<uuid> | null",
  "status": "provisioning | active | paused | closed",
  "closed_reason": "<string> | null",
  "closed_reason_detail": "<string> | null",
  "customer_close_reason": "<string> | null",
  "close_conditions": ["only_if_stale"],
  "provisioning_detail": null,
  "ready": false,
  "ready_at": "<ISO-8601> | null",
  "timings": {
    "requested_at": "<ISO-8601>",
    "allocated_at": "<ISO-8601> | null",
    "device_ready_at": "<ISO-8601> | null",
    "allocation_ms": "1840 | null",
    "ready_ms": "9120 | null"
  },
  "closed_at": "<ISO-8601> | null",
  "token_budget_total": 100000,
  "token_budget_remaining": 99500,
  "transcript_length": 12,
  "history": {
    "entries_used": 12,
    "entries_max": 256,
    "entries_left": 244,
    "nearly_full": false,
    "entries_dropped": 0
  },
  "created_by_user_id": "<user-uuid> | null",
  "mode": "ai | manual | pair",
  "model": "claude-sonnet-5-5 | claude-haiku-5-5 | claude-fable-5-1 | claude-opus-5-5 | claude-opus-5 | claude-sonnet-5 | claude-opus-4-8 | claude-opus-4-7 | claude-sonnet-4-6 | claude-haiku-4-5",
  "stop_on_exit_ip_change": false,
  "proxy_id": "<uuid> | null",
  "profile_id": "prof_<uuid> | null",
  "archetype": "iphone15_ios18_7_safari26_5 | null",
  "archetype_source": "explicit | profile | random | null",
  "resumed_from": "agt_<uuid> | null",
  "created_at": "<ISO-8601>",
  "updated_at": "<ISO-8601>",
  "livekit": {
    "ws_url": "wss://<livekit-host>",
    "room": "agt_<uuid>",
    "token": "<HS256 JWT>",
    "participant_identity": "customer-<account-uuid>",
    "expires_at": "<ISO-8601>"
  },
  "error_event": {
    "timestamp": "<ISO-8601>",
    "code": "launch_timeout",
    "severity": "info | warn | error | fatal",
    "summary": "The browser did not become ready in time.",
    "detail": "<string> | null",
    "customer_actionable": false,
    "retryable": true
  }
}

The status field reads active from the moment the session exists, before its browser has finished starting, so status alone does not tell you the browser is ready; ready below does. provisioning is kept in the list for a session whose setup step is reported in provisioning_detail; today a session reads active while it starts, VPN sessions included. If you do see provisioning, treat it as running but not ready: do not start work against the session, and do not treat it as finished. It is already consuming one of your concurrent-session slots, so a provisioning session counts against your cap exactly as an active one does.

ready turns true once the session’s browser has finished starting, and ready_at says when; status reads active before that. Wait for ready: true before sending the first message — a session on a VPN can take longer to become ready — and read a session that closes while ready is still false as one that failed to start, with closed_reason saying why. An older deployment does not send ready; read a missing ready as ready.

ready comes from the session’s own report, and some sessions cannot send that report again once it is lost (for example across a reconnect), so a running session can keep reading ready: false. A message is held for a session that is not ready only within 125 seconds of its creation, long enough for a VPN session to finish starting: if ready is still false after that while status stays active, send the message anyway — it is no longer held.

timings says where a session’s start time went, so you can tell the time on your side (your request and network) from the time on ours. Every value comes from times the server records on its own clock:

  • requested_at — the server accepted the create request (equals created_at).
  • allocated_at — the device the session was placed on picked it up and began starting the browser. Time spent waiting for a device counts before this. null until then, and on a session no device picked up.
  • device_ready_at — the browser was ready (the same instant as ready_at). null until then, and for ever on a session that failed to start.
  • allocation_ms and ready_ms — the milliseconds from requested_at to allocated_at and to device_ready_at; null while that time is null. ready_ms - allocation_ms is the time the device spent starting the browser.

Poll GET /v1/agent-sessions/{id} until ready is true to see all of them. An older deployment does not send timings.

provisioning_detail names the step it is on (a snake_case token such as vpn_egress_bringing_up or vpn_egress_active), and is null once the session is active or closed. It may be absent entirely on older deployments, so read it defensively rather than assuming the key exists.

history says how much room the session’s live history has. The live history is what the agent works from: it holds at most 256 entries (about 256 steps calls, or roughly 85 to 127 AI messages) and 1 MiB. A steps call needs 1 free entry, a note in manual mode 1, and an AI message 3 (your message, the plan with its results, and the answer). When a call does not fit, the session does not close: the oldest entries are dropped from the live history and the session goes on. The drop keeps the newest entries that fit in half of each limit (128 entries and 512 KiB), from the start of a turn, and always keeps your first task and the newest two entries, so it happens about once every 125 steps calls, not on every call. Dropped entries are not deleted: the transcript stream still returns them under their own numbers, until the deployment’s retention empties a closed session’s transcript, dropped entries included. The agent no longer works from them, and a recipe built from the session and the profile’s activity leave them out too.

transcript_length counts every entry the session has written, dropped ones included, and never goes down; the newest entry is number transcript_length - 1. entries_used counts the entries in the live history, so it is the same number as transcript_length only until the first drop. entries_max is 256, and entries_left is the room before the next drop. entries_dropped says how many entries have been dropped so far (0 until the first drop); it is absent on older deployments, which close a full session instead. nearly_full turns true when fewer than 32 entries are left, or when the history is close to the 1 MiB limit, which a session with long entries can reach first with many entries still left. It means a drop is near; you need do nothing.

A session is still closed with closed_reason: "transcript-limit" in two cases: one call alone needs more room than the history has after a drop (a single call that adds several hundred KiB of text), or the session has already dropped the most it can (4,096 entries or 16 MiB). Then start a new session, with continue_from_agent_session_id to carry the recent conversation over. On the session of a message or steps answer the numbers already count that call’s own entries, and the same answer carries a warning while nearly_full is true. history is null on a closed session, in each row of List (read one session with GET /v1/agent-sessions/{id} to get it), and when the server cannot count the history; it is absent on older deployments. It never carries a size or any of the history itself.

closed_reason says why a closed session ended. It is an open string, not a fixed list: customer-closed (you closed it), budget-exhausted (its token budget ran out), transcript-limit (its history could not take a call; see history above), exit_ip_changed (see stop_on_exit_ip_change under Create), trial_session_limit (a free-trial session reached its 20 minutes), trial_time_used (the free trial’s 2 hours of session time ran out during the session), payment_failed (the account’s renewal payment didn’t go through, so its paid plan stopped), plan_ended (the account’s paid plan ended), browser_crashed (the session’s browser crashed — including when it reported that its network process crashed, after which Driftstack ends the session for you; see “When the browser’s network process crashes” under Message), and values such as idle_timeout or max_duration when the browser behind the session ended it. Read any other value as “the session could not start, or Driftstack ended it”. worker-disconnected means the Mac running the session went offline, and worker-restarted that it restarted (for example, to install an update); the session ended with it. Nothing in your requests caused either; start a new session. node_resource_pressure means the Mac running the session was briefly short of system resources on our side, so the session was stopped. Its error_event, when it has one, reports retryable: true. Nothing in your requests caused it; start a new session. session-errored means the session stopped on the Mac running it without a more specific reason: the browser reported an error, or the Mac stopped listing the session (it lost it) without saying how it ended, so Driftstack closed it rather than let it hold one of your concurrent-session slots. Nothing in your request caused it; start a new session. provisioning_interrupted means the Mac running the session picked it up and then stopped reporting it before it was ready, so Driftstack closed it rather than let it hold one of your concurrent-session slots. It closes with ready still false, and its error_event carries the same code, the summary “The session could not start on our side. Start it again.” and retryable: true. Nothing in your request caused it; start a new session. dispatch_no_live_node means the device the session was sent to went out of service just as it started, so the session closed moments after the create returned (rarely, it is already closed in the create response). Nothing in the request caused it; create the session again shortly with a new Idempotency-Key (the same key replays the closed session). When no device is free at the moment of the create, the create itself is refused with 503 and nothing is created — see No device free. archetype_config_failed means the device’s own configuration, on the Mac running the session, could not be loaded, either as the session started or later while it ran; launch_config_failed means that Mac could not set up the environment the device needs to start. Both are on our side, not in your request: the error_event carries the same code, customer_actionable: false, detail: null and the summary “This device’s configuration couldn’t be loaded on our side, so the session didn’t start. Choose a different device, or contact support if it keeps happening.” (or “…so this session was stopped. …” when it had been ready). retryable is false: until that Mac is fixed, a new session with the same device is likely to be sent to it and fail the same way, so choose a different device, or contact support if it keeps happening. upstream_proxy_not_public means the proxy saved for the session points at an address that is not on the public internet (a private or local one), so the Mac that would have run the session did not start it. Nothing ran. The error_event carries the same code, customer_actionable: true, detail: null and the summary “This session’s proxy address is not a public address, so the session was not started. Change the proxy and start again.” retryable is false: the same proxy is refused the same way each time, so change the proxy’s address first.

closed_reason_detail says why a closed session ended in plain words you can show to a person, and is null while the session has not ended. When the session’s error_event names the cause — an error or fatal event, or any event on a session that closed session-errored (for example because your proxy refused its sign-in) — it explains that cause rather than the close reason; a session you or Driftstack support stopped, or that reached a trial limit, says so whatever its error_event. Where there is something to do next, it ends with “Reopen the session to continue.” while the session can still be reopened (within 7 days of closed_at, for a session that ran through one of your proxies and whose profile, if its device came from one, still exists), and with “Start a new session to continue.” after that. A reason not in the table below gets “This session has ended. Contact support if you did not expect it and it keeps happening.” and the next step, never the raw value. The wording may change: branch on closed_reason and error_event.code, show closed_reason_detail. It is absent on older deployments.

The words for each reason (a closed_reason, or the error_event.code that explains it); “Next step: yes” means one of the two sentences above follows. transcript-limit and budget-exhausted are the exceptions: they end “To keep the recent conversation, start a new session that continues from this one.” (create it with continue_from_agent_session_id), and while the session can still be reopened they add “Reopening it starts a new session with the same setup and an empty history.” (budget-exhausted: “…with the same budget…”), because a reopen carries no conversation and keeps the budget:

Reason Words Next step
customer-closed You closed this session. no
session_ended This session ended normally. no
staff-stopped Driftstack support stopped this session. Contact support if you need to know why. no
idle_timeout This session was closed because nothing happened in it for longer than its idle limit. yes
max_duration This session reached the maximum running time set for it, so it was closed. yes
challenge_pause_timeout This session was paused for a verification check on the page, and it was not resumed within 5 minutes after the last activity, so it was closed. yes
orphaned-lifetime This session reached the longest time a session may stay open, counted from when it started, so it was closed automatically. yes
trial_session_limit This trial session reached its time limit. You can start another while your trial has session time left, or choose a plan for longer sessions. no
trial_time_used Your trial’s session time ran out, so this session was closed. Choose a plan to keep running sessions. Your profiles, proxies and settings stay saved for at least 30 days after your trial ends. no
payment_failed This workspace’s last payment didn’t go through, so this session was stopped. The account owner can pay the open invoice or update the card; the plan comes back as soon as the payment goes through. no
plan_ended This session was closed because this workspace’s plan has ended. The account owner can choose a plan (or pay for a new crypto term) to start sessions again; profiles and settings are kept. no
budget-exhausted This session used up its token budget, so it was closed. Give the next session a larger budget if you need one. yes
transcript-limit This session’s history reached its maximum length, so it was closed. yes
exit_ip_changed Your proxy’s exit IP address changed while the session ran, and this session was set to stop when that happens. Check the proxy’s settings if its address should stay the same. yes
intent_deadline_exceeded A step ran past the time allowed for it, so the session was stopped. yes
session_resource_overuse The page used more memory or processing power than a session allows, so the session was stopped. yes
browser_crashed The browser running this session stopped unexpectedly. yes
browser_closed The browser running this session was closed. yes
browser_exited The browser running this session shut down. yes
renderer_crashed The page in this session stopped running and could not be recovered. yes
launch_timeout This session did not start in time. Nothing in your requests caused it. yes
render_failed The browser for this session did not start. Nothing in your requests caused it. yes
webkit_spawn_failed The browser for this session did not start. Nothing in your requests caused it. yes
webdriver_connect_failed The browser for this session did not start. Nothing in your requests caused it. yes
worker-restarted The Mac running this session restarted. yes
worker-disconnected The Mac running this session went offline. Nothing in your requests caused it. yes
node_drain The Mac running this session was taken out of service for maintenance. Nothing in your requests caused it. yes
node_shutdown The Mac running this session was taken out of service for maintenance. Nothing in your requests caused it. yes
node_shutting_down The Mac running this session was taken out of service for maintenance. Nothing in your requests caused it. yes
control_plane_unreachable We lost contact with the Mac running this session, so it was stopped. Nothing in your requests caused it. yes
session_not_found This session had already stopped on the Mac running it. yes
node_resource_pressure The Mac running this session was briefly overloaded on our side, so the session was stopped. Nothing in your requests caused it. yes
dispatch_no_live_node No Mac was free to start this session just then. Nothing in your requests caused it. Wait a minute or two first. yes
node_shedding No Mac was free to start this session just then. Nothing in your requests caused it. Wait a minute or two first. yes
session_refused_at_capacity No Mac was free to start this session just then. Nothing in your requests caused it. Wait a minute or two first. yes
dispatch_failed This session could not be started, so nothing ran in it. Nothing in your requests caused it. yes
create_failed This session could not be started, so nothing ran in it. Nothing in your requests caused it. yes
dispatch_owner_claim_failed This session could not be started, so nothing ran in it. Nothing in your requests caused it. yes
provisioning_interrupted This session stopped before it finished starting. Nothing in your requests caused it. yes
reaped_during_provisioning This session stopped before it finished starting. Nothing in your requests caused it. yes
screen_capture_denied Live view was not available for this session, so it was stopped before it started. Wait a few minutes first. yes
live_view_capture_unavailable The live view of this session could not start, so the session was stopped. Nothing in your requests caused it. yes
archetype_lookup_failed The device this session’s profile uses is on hold or not available right now. Pick another device for the profile. yes
archetype_not_supported The device profile this session asked for is not available right now. Pick another device for the profile. yes
session_config_invalid This session could not start with its current settings. Check the profile and proxy it uses. yes
archetype_config_failed This device’s configuration couldn’t be loaded on our side, so the session didn’t start. Choose a different device, or contact support if it keeps happening. no
launch_config_failed This device’s configuration couldn’t be loaded on our side, so the session didn’t start. Choose a different device, or contact support if it keeps happening. no
session-errored This session stopped unexpectedly because of a problem on our side. yes
unknown_error This session stopped unexpectedly because of a problem on our side. yes
proxy_required This session needs one of your saved proxies and none was given, so it was not started. Choose a proxy for it. yes
no_proxy_configured This session needs one of your saved proxies and none was given, so it was not started. Choose a proxy for it. yes
upstream_proxy_not_public This session’s proxy address is not a public address, so the session was not started. Change the proxy and start again. no
exit_timezone_unresolved This session was not started because we could not confirm where your proxy’s connection comes out. If it keeps happening, test the proxy. yes
proxy_connection_failed This session could not connect through your proxy. Check the proxy’s settings, or test it. yes
proxy_udp_unsupported Your proxy cannot carry the UDP traffic this session needs for HTTP/3, so it was not started. Use a proxy that supports UDP. yes
egress_unreachable We could not confirm where your proxy’s connection comes out, so the session was stopped. Check the proxy’s settings, or test it. yes
egress_invariant_violation Some of this session’s traffic did not go through your proxy, so the session was stopped to keep it on the proxy. Check the proxy’s settings, or test it. yes
egress_bind_failed The VPN for this session could not be started. Check the proxy’s VPN settings and that its server is up. yes
egress_lost The part of the connection we run for this session stopped while it was running. Nothing is wrong at your end. yes
proxy_boot_failed The part of the connection we run for this session did not start. Nothing is wrong at your end. yes
network_shim_boot_failed The part of the connection we run for this session did not start. Nothing is wrong at your end. yes
default_egress_unavailable The connection this older session ran on, which we provided, stopped working. That was ours to fix, not yours. yes
proxy_auth_failed Your proxy refused the username or password saved for it. Check them in the proxy’s settings. yes
egress_verification_unavailable We could not confirm that this session’s traffic was going through your proxy, so we stopped it rather than risk it going out unprotected. This one is ours, not your proxy. yes
vpn_bringup_failed The VPN for this session could not be brought up, and there was not enough detail to say why. Check the proxy’s VPN settings and that its server is up. yes
vpn_bringup_no_active_state The VPN for this session could not be brought up, and there was not enough detail to say why. Check the proxy’s VPN settings and that its server is up. yes
remote_unresolved The address your proxy points at could not be found, so nothing could connect. Check the address in the proxy’s settings, or ask whoever supplies it. yes
remote_refused Your proxy’s server refused the connection. Check the address and port in the proxy’s settings, or ask whoever supplies it. yes
remote_unreachable Nothing answered at your proxy’s address. Check the address and port in the proxy’s settings, or ask whoever supplies it. yes
remote_closed_during_setup Your proxy’s server closed the connection before the VPN was up. Check the proxy’s VPN settings, or ask whoever supplies it. yes
tls_handshake_failed The secure handshake with your proxy’s server did not complete. A certificate or key in the proxy’s settings may no longer match the server. yes
config_rejected The VPN configuration for this proxy was refused. Open the proxy and paste its configuration again. yes
auth_failed Your proxy refused the username or password saved for it. Check them in the proxy’s settings. yes
tunnel_setup_timeout The VPN for this session did not finish connecting in time. Check the proxy’s VPN settings and that its server is up. yes
no_output The VPN for this session could not be started on our side. This one is ours, not yours. yes
egress_bypasses_tunnel The VPN came up but did not carry all of this session’s traffic, so the session was stopped. Set the proxy to send all traffic through the VPN. yes
tunnel_no_egress The VPN came up but no traffic came back through it, so the session could not browse. Check that the VPN server forwards traffic. yes
vpn_no_dns Your VPN configuration sets no DNS server, so this session was not started. Add a DNS = line naming a resolver inside your VPN (WireGuard), or have the server push dhcp-option DNS (OpenVPN). We don’t use a public resolver for you. yes
exit_ip_check_unavailable We couldn’t check this session’s exit address, so it was stopped. The connection itself may be fine. yes
egress_ip_lookup_failed We couldn’t check the connection on our side, so this session was stopped. Start the session again. yes

stop_on_exit_ip_change is always present and echoes the create-time setting.

network_detail is on GET /v1/agent-sessions/{id} and on the create answer (never on a list row or another call’s answer): requested is true when the create sent network_detail: true, served says whether request detail is being recorded for the session, and reason is null when it is, otherwise not_requested, mode_off (the deployment is not recording request detail for this session) or not_recorded (asked, but the browser has sent no request detail yet).

proxy_id is the proxy the session’s traffic goes out through, one of your account’s proxies: the proxy_id you passed at create. It is set from the create’s response onwards, and routing new sessions somewhere else does not change it. Every agent session created since proxy_id became required has one; null appears only on an older session that was created without a proxy of yours. It is absent on older deployments, so read a missing key as “not reported”, not as null.

archetype is the device profile the session runs (an id from GET /v1/archetypes), fixed for the session’s lifetime, and archetype_source says where it came from: explicit (the create’s archetype), profile (the device of the profile it runs) or random (the create named neither, so the session runs as a fresh visitor on a random current iPhone your plan includes — see Create). Both are null on a session created before the server recorded them, and absent on older deployments.

resumed_from is the id of the ended session this one was reopened from, and null on a session that was created. It is only a link: the reopened session has the ended one’s configuration and none of its history. Absent on older deployments.

Two more fields are optional and read-only: liveness ({ state, fresh } — the browser’s latest reported state, active, provisioning, idle or terminating, and whether that report is recent) and capability_report (the browser’s latest report of what the session can do — for example its exit IP, country and timezone, and whether live video is streaming). Both are absent until the browser has reported; treat an absent field as “not known yet”, never as “not running”.

capability_report.egress_state says whether the session’s traffic is getting out: live; dead_proxy — your proxy stopped carrying traffic while the session ran, so check it before starting another; or default_connection_down — only on a session created before proxy_id became required, without a proxy of its own: the connection Driftstack provided for it then stopped carrying traffic. That one was on our side. No agent session created since reports it, because every one runs through one of your proxies. null means it has not been reported. Read a value you do not recognise as null.

capability_report.os_fingerprint is the last OS reading Driftstack took of the session’s proxy — the same reading as the proxy’s own os_fingerprint (see Proxies): os, confidence, at (when it was taken; it can be any age), and how it was taken — observed_via, single_host_vantage and web_port_vantage, with direct_reading and website_like_reading as the customer names of the last two. Read a missing or false value on either pair as “this reading does not describe the path a website sees”. null means not measured, never “no OS”.

capability_report.exit_tcp_stack says whether the network stack of the session’s proxy looks like an iPhone’s. Your proxy opens its own connection to every site, so the first settings of that connection come from the proxy’s own operating system, not from the Mac running the session, and a site that checks them can notice when they differ. It is measured once, when the session starts: verdict is match, mismatch or unknown (it could not be decided), measured_at is when Driftstack took the reading, and proxy_id is the proxy it describes. A mismatch is a warning: nothing is blocked and the session keeps running. null means it was not measured: the check is off, the session has no proxy of its own, the reading could not be timed, the Mac running the session was too busy to run the check, or the session has moved to another proxy since the reading. Read a verdict you do not recognise as unknown.

upload_max_file_bytes is the largest file, in bytes, that one upload to this session can carry right now. Each session takes a file up to its own size, so this is often smaller than the 64 MiB per-file maximum, and it can change while the session runs. It is returned by GET /v1/agent-sessions/{id} while the session is running and has reported it, and absent otherwise — absent means “not known”, not “no limit”. Check a file against it before reading or encoding the file.

viewport is the phone’s page viewport for the session, in the phone’s CSS pixels (for example { "width": 402, "height": 714 }): window.innerWidth x window.innerHeight of a page in the session. It is the coordinate space taps and clicks on the live screen are given in, and it is fixed for the session’s lifetime by its device. Each side is a whole number from 1 to 4096, the largest the server accepts. It is returned only by GET /v1/agent-sessions/{id}; it is null until the device has reported it (normally as the session starts) and after the session has ended, and absent from the list and create answers and from older servers.

The error_event field is optional and nullable — it carries the most recent launch or runtime failure recorded for the session, and is absent or null when none has been reported. Branch on its two booleans rather than on the prose: customer_actionable says whether a human can do anything about the failure, and retryable says whether repeating the same call is worth trying. detail is null when the server has nothing to add beyond summary, and severity is one of info, warn, error, fatal. An error_event does not by itself close the session — read status for that.

Two codes concern a session started without a proxy of your own. default_egress_unavailable appears only on a session created before proxy_id became required, without one: the connection Driftstack provided for it then failed; customer_actionable is false, because it was ours to fix. The same value can appear as closed_reason. proxy_required means the machine running the session refused a session that reached it with no proxy; sessions run only through a proxy you have added, so start it again with one. (A create without proxy_id never gets that far — it is refused with 422 proxy-required; see Create.)

session_not_found means a request reached the Mac running the session after the session had already stopped there. Its summary is “This session had already stopped on the Mac running it, so the last request sent to it could not be carried out.” and detail is null. It is not recorded on a session that has already closed for a reason of its own, so it does not hide why that session stopped.

archetype_lookup_failed means the session could not start on its profile’s device: that device is on hold or still in development (the create refuses these up front with 409 device-unavailable, so this appears only when the catalog changed in between), or it cannot run right now for another reason. Its summary says so in words you can show — for a device on hold, the hold sentence with its reason in plain words, such as “This device profile is on hold while we confirm it matches a real iPhone. Pick another device.”; otherwise “This profile’s device isn’t available right now.” — and detail is null. It is fatal, customer_actionable and not retryable: the same profile fails the same way. The session closes with it; start a new one on a profile with another device.

The livekit field is optional — auto-populated on the session-create response when live video is available on the deployment, and absent otherwise. It is also absent when the session in the response is already closed, since nothing will ever stream into it. Clients that need a token for a running session in the absent case use the explicit endpoint at Live video (LiveKit) below.

ID-format note. The agent-sessions resource emits account_id as a bare UUID (no acc_ prefix), unlike GET /v1/whoami, which emits acc_<uuid>, and the GET /v1/sessions/:id resource which emits prefixed ses_/acc_/ key_ IDs. Customer code comparing agentSession.account_id against whoami.account_id must strip the acc_ prefix from the latter first. (id and driftstack_session_id are prefixed — agt_<uuid> and ses_<uuid>. Only account_id is bare here.)

Errors

Status Type When
400 validation-failed body fails schema (missing user_message, a user_message over 8,000 characters, etc.)
400 bad-request create’s proxy_id names an HTTP proxy
403 forbidden create with mode: ai on a plan without the AI agent (the free trial, Free, Personal)
403 forbidden a model that runs only on your own key, at create or on a message (requires_own_key: true, with model naming it)
403 forbidden a message on bundled billing when the account’s current plan no longer includes it (add your own key)
404 not-found session id you cannot access (not your own, and not a team you hold admin on)
409 conflict mode mismatch, or ai_control_unavailable: true when control of the session changes while a message is running; the latter includes phase and can include consumed tokens_consumed, usage, and redacted partial_results that must not be replayed automatically
409 conflict turn_in_progress: true — another message is still running on this session; wait for it or stop it
409 conflict session_status — the session was not active ("closed", with closed_reason beside it when the session records one and closed_reason_detail, the same reason as a sentence, or "paused"); start a new session, optionally with continue_from_agent_session_id, or resume the paused one
409 conflict idempotency_status: "in_progress" — the first request with this Idempotency-Key is still running (retry later with the same key); "mismatch" — the key was used for a different request (use a new key)
409 session-not-ready a message to a session whose browser is still starting (ready is still false), after waiting up to 45 seconds (20 seconds on a JSON response) for it; nothing ran — send the same message again after retry_after_seconds (carries code: "session_not_ready", retryable: true)
409 profile-in-use create’s profile_id already has a live session (carries active_session_id)
409 device-unavailable create’s profile_id is on a device that is not offered right now (carries archetype, and held_reason when there is one)
409 storage-quota-exceeded a profile-backed create when your plan’s profile storage is full
409 conflict reopen of a session still running (code: "session_not_ended"), or of one whose configuration cannot start a session today (code: "reopen_configuration_unavailable", with fields)
409 conflict top-up of a session that has ended (code: "session_not_running"), or past 10,000,000 tokens (code: "token_budget_cap_exceeded", with max_add_tokens)
410 session-destroyed reopen of a session that ended more than 7 days ago (code: "reopen_window_passed", reopen_window_days: 7); create a new session instead
402 bundled-llm-budget-exhausted bundled-LLM monthly cap reached
402 bundled-llm-consent-required no key of your own and no opt-in to bundled billing; on a plan without bundled billing opting in is refused, so add your own key instead
422 proxy-validation-failed create’s live proxy test failed (carries reason)
422 proxy-required create named no proxy_id; nothing was created (carries code: "proxy_required")
429 rate-limited the agent_sessions:message request rate for your plan (agent_sessions:steps for a list of steps), or your account already has as many AI turns running at once as it may — across your sessions, or on bundled billing (retry_after_seconds)
429 concurrency-limit at create, your account already has as many open agent sessions as your plan allows; on a message, with ai_tasks_in_flight: true, 3 AI tasks are already running (depending on how your account’s AI is billed, that limit answers this way or as rate-limited, above): nothing ran, wait and send the message again
429 tier-limit at create only: the free trial’s 2 hours of session time are used up (code: "trial_session_time_used"), or the profile or proxy is outside the ones your plan can launch (code: "over_plan_limit")
429 rate-limited at create and reopen: the account’s last sessions could not connect through their proxy (code: "proxy_failure_backoff"), or it is at its limit: at most as many sessions starting at once as your plan runs at once, and never more than 10 (this number can change; the 429 tells you how long to wait) (code: "provisioning_limit"); retryable: true, with Retry-After; nothing was created — see Create errors. The limit is per account for now
500 internal something went wrong on our side, except a refusal from Anthropic we do not recognise yet (for example a spend limit set in the Anthropic Console): if it repeats on your own key, check that account first; a refusal we recognise, of your own Anthropic key or of the account behind it, is the 502 below
502 byok-anthropic-required no Anthropic key could be found for the turn: none sent, none stored, and bundled billing is not available — or, with key_rejected: true, Anthropic refused yours, or the account behind it (no credit left, or the organization disabled), on the first planning call (key_source, key_rejected_reason)
503 feature-unavailable on a message, an Idempotency-Key was sent but this deployment cannot record one, so the turn did not run — the same key will fail the same way, and the same request without the header runs the turn (Idempotency); that answer has no code (with code: "ai_provider_unavailable", see the next row: keep the header and use a new key); on Stop, stop_unconfirmed: true means the stop could not be confirmed just now (try again)
503 feature-unavailable on a message, code: "ai_provider_unavailable" (retry_after_seconds; Retry-After except when streamed): the AI service is temporarily unavailable, so nothing ran; steps sent to /steps still work. A turn on your own key never gets it
503 maintenance on a create or a reopen, every machine that could run the session is in a booked maintenance window (code: "maintenance", retryable: true, until, retry_after_seconds and Retry-After); nothing was created — send the same request again after until, with the same Idempotency-Key if you sent one