Drıftstack docs
Docs

Agent sessions: create

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

Create

POST /v1/agent-sessions

Request body (proxy_id is required; every other field is optional):

{
  "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",
  "driftstack_session_id": "ses_<uuid>",
  "token_budget": 100000,
  "archetype": "iphone15_ios18_7_safari26_5",
  "profile_id": "prof_<uuid>",
  "proxy_id": "a1b2c3d4-...",
  "initial_url": "https://example.com/",
  "geolocation": { "latitude": 48.8566, "longitude": 2.3522, "accuracy": 20 },
  "continue_from_agent_session_id": "agt_<uuid>",
  "stop_on_exit_ip_change": false,
  "skip_proxy_probe": false,
  "network_detail": false
}

Headers:

  • Idempotency-Key: <string> (optional) — retries with the same key replay the original 201 instead of minting a duplicate row. This endpoint is one of those that honour the header; see Idempotency keys for the full list and the endpoints that ignore it.
  • x-byok-anthropic-api-key: sk-ant-... (optional) — your own Anthropic key. At create it only decides whether an Opus or Fable model may be chosen without a stored key (see below); the key is not stored, so send it on every message too.

Response 201 Created returns the resource above.

Plan availability. AI-driven sessions (mode: "ai", the default when mode is omitted) require a plan with the AI agent: every PAID plan on offer, and the earlier Team, Agency and API plans. On the free trial, and on the earlier Free and Personal plans, the create is refused with a 403 forbidden tier error — on the trial the detail reads “AI isn’t included on this account’s current plan (Trial).” and names the plans that do include the agent. mode: "manual" is not gated and works on every plan, the trial included. Team-scoped creates (X-Driftstack-Account) gate on the owner account’s tier — the account the session runs and bills against.

Session time on the free trial. Every trial session — manual or code-driven, from the desktop app, the API or the SDKs — runs for at most 20 minutes, and the trial includes 2 hours of session time in total, counted from the day it starts. A new session runs for 20 minutes or for what is left of the 2 hours, whichever is shorter: the session is told the limit when it starts, and Driftstack also closes a session that is still running within about two minutes of the mark (closed_reason: "trial_session_limit", or "trial_time_used" when the 2 hours ran out). Once none is left, the create is refused with 429 (tier-limit, code: "trial_session_time_used"). Paid plans have no limit of 20 minutes a session and no total of session hours. A paid session still ends on its own: after a time without activity, at 4 hours if it was created in manual mode, and in every case about 12 hours after it was created (see Turn and session limits).

If mode is omitted the server defaults to ai: the AI drives the browser and the session refuses taps and typing from the desktop app (409, “input-event requires mode=‘manual’ or ‘pair’”) until someone switches it to Manual in the session window. Send "mode": "manual" when a person will drive the session themselves in the desktop app: it takes taps from the app’s session window from the start, and a message to it is recorded without running the AI (kind: "logged-manual"). If model is omitted it defaults to claude-sonnet-5-5 (every earlier id stays accepted for back-compat, and a session keeps the model it was created with) — the model selects which Claude model the AI agent runs, and applies in ai and pair mode. token_budget is the most model tokens the session may use (see Sizing the token budget). The browser starts when the session is created (ready turns true once it has started). The optional driftstack_session_id links an existing browser session you created with /v1/sessions, for reference; it does not change which browser the AI drives.

A shorter idle timeout

A create that does not send idle_timeout_seconds gets the default idle timeout for its mode, and "idle_timeout_seconds": null is the same as leaving it out.

idle_timeout_seconds (a whole number of seconds) closes the session (closed_reason: "idle_timeout") after that long with nothing reaching its browser, instead of the default for its mode: about 5 minutes for ai and pair, 30 minutes for manual. It can only shorten that default: the value must be from 120 up to the mode’s default (300 for ai and pair, 1800 for manual); any other value is 400 validation-failed naming idle_timeout_seconds. The session reads it back as idle_timeout_seconds (absent on a session that uses the default), and a reopen keeps it while it is still within that range for the session’s mode (a session moved from manual to ai with a value over 300 reopens with the ai default). A retry with the same Idempotency-Key returns the first session with its first value, even if the retry sends a different value in the range. The value is checked before the key is looked up, so a retry whose value is outside the range is the 400 above; the first session is not affected by it.

What counts as activity is the same with or without the field:

  • Counts: a step being sent to the browser, and that step ending — each step of POST /v1/agent-sessions/{id}/steps and each step an AI turn runs; a paused session resuming; and live-view or manual input (a touch, a key, a URL typed in the address bar; at most once a second, and keep-alive pings do not count).
  • Does not count: reading the session — GET /v1/agent-sessions/{id}, the list, page-state, the network log, the transcript stream — because those are answered without reaching the browser; and an AI turn’s planning between two steps.
  • Never closed as idle: a session still starting, a paused one, and one running a step. A paused session’s clock starts again when it resumes.
  • The clock starts when the session is created, not when it is ready. A start can take up to about two minutes (a VPN proxy is the slow case), so send the first step soon after ready turns true. For an ai session through a VPN proxy, choose 180 or more.

Sizing the token budget

token_budget is set per session, when you create it, and can be at most 10,000,000 tokens. If you leave it out, the session gets:

  • 500,000 tokens when it runs on your own Anthropic key: stored in the dashboard, or sent in the x-byok-anthropic-api-key header on the create. That header is not stored, so send it on every message too;
  • 100,000 tokens when it runs on Driftstack’s AI: your plan’s AI credits or the included AI.

A value you send always wins over the default.

To size it, count about 25,000 tokens per turn on a complex page (a long form, a page with many controls). A 10-page form wizard therefore needs roughly 750,000: send "token_budget": 750000. A task that reads page after page spends more than one that taps and types, because the text it reads goes back to the model, so set a larger budget at create for a long task rather than rely on the default. What a turn actually took from the budget is its usage.turn_budget_debited_tokens (see Message for usage); usage.anthropic_input_tokens is the size of one call’s prompt, not the turn’s cost.

If a long job needs more, add tokens to the running session with POST /v1/agent-sessions/{id}/budget before it runs out; the session carries on where it is. When a turn stops with notice_reason: "budget_low", top up first, then send “continue”: a message sent before the top-up is not run — it is refused (the session stays open, and nothing is charged) with how many tokens to add, and that refusal carries notice_reason: "budget_low". The agent does not start planning a message with less than 6,000 tokens left, and the number it asks for also covers what that turn’s first planning call can use, so adding that much lets the next message run. A turn that starts with only a little more than it needs can still use the last token and close the session. A session created with a token_budget under 6,000 can never plan a message: give it at least that much (the default is 500,000 on your own model key, 100,000 on the included model).

A session with no budget left is closed at its next turn; it may show active with 0 remaining until then. Once it closes (closed_reason: "budget-exhausted"), its page, cookies and transcript go with it. The way back is to reopen it within 7 days: the new session starts with the ended one’s total, top-ups included (but not its page, cookies or transcript).

Claude Opus models (claude-opus-5-5, claude-opus-5, claude-opus-4-8, claude-opus-4-7) and Claude Fable 5.1 (claude-fable-5-1) are available with your own Anthropic key only. On an account that would run the session on bundled billing — no key of its own, sent on this request or stored — creating an ai or pair session with one of these models returns 403 with requires_own_key: true and the refused model. The same check runs on every message, because a session moves to bundled billing if its key is removed or expires.

Which models your plan runs

GET /v1/agent-models

Lists every model an agent session accepts and, for each, how your plan may run it. It answers for the plan and AI credits of the account the request acts for (with X-Driftstack-Account, the team owner’s). It needs the read scope and takes no body.

Response (200):

{
  "ai_included": true,
  "credits_fund_turns": true,
  "models": [
    {
      "id": "claude-sonnet-5",
      "label": "Claude Sonnet 5",
      "on_credits": true,
      "included_ai": true,
      "own_key": true,
      "automatic_with_key": true
    },
    {
      "id": "claude-haiku-5-5",
      "label": "Claude Haiku 5.5",
      "on_credits": false,
      "included_ai": true,
      "own_key": true,
      "automatic_with_key": true
    }
  ]
}

Each field says one thing:

  • ai_included: whether the plan includes the AI agent at all. When it is false, no model runs on any key, and every other field is false.
  • credits_fund_turns: whether this account’s turns may be paid from its AI credits: true when they are, and also when that cannot be read. Filter by your AI source first. When it is false, they run on the plan’s included AI or on your own key.
  • on_credits: whether the model runs if a turn is on AI credits. It does not say that your turns are; a turn on credits that names a model with on_credits: false is refused with 403 and not_on_credits: true.
  • included_ai: whether the model runs on the plan’s included AI (bundled billing) when turns are not on credits. Consent and the monthly cap still apply.
  • own_key: whether the plan permits running the model on your own Anthropic key. It does not say that a key is saved: check your AI source and stored key in the account’s AI settings.
  • automatic_with_key: whether the model runs on AI source automatic with a usable key saved, when turns are on AI credits: it runs on credits, or on your key for a model automatic never asks credits for. A model with own_key: true and automatic_with_key: false is refused there with 403 and not_on_credits: true.

To offer only models the next turn will run, filter by where the turn’s AI comes from:

  • AI source credits: models with on_credits;
  • AI source your own key: models with own_key, when a usable key is saved (or sent in the header);
  • automatic: when credits_fund_turns is true, models with automatic_with_key if a usable key is saved, else models with on_credits; when it is false, models with included_ai, plus models with own_key when a usable key is saved (or one is sent in the header).

The list keeps the same order between calls.

Which iPhone the session is. A session created without a profile or archetype runs as a fresh visitor on a random current iPhone; pass archetype to pin one, or profile_id to keep an identity between runs. The random device is drawn from among the devices your plan includes that are open for new profiles (see GET /v1/archetypes), weighted toward the newest iOS the way real traffic skews; it is chosen once, when the session is created, and never changes while the session runs. Sessions created without a device used to all share one default, which made many fresh sessions from one account look alike; now each is its own visitor. The response says what you got: archetype names the device and archetype_source is random, explicit (you passed archetype) or profile (the profile’s device; an explicit archetype wins over the profile’s). An archetype that is not on the list is a 400; one your plan does not include is a 403 (code: "device_not_on_plan").

The optional profile_id attaches one of your saved profiles (a persistent browser identity — cookies, localStorage, etc.) to the session, so the run resumes that profile’s stored state and saves changes back when it ends. Pass the prof_<uuid> id from the profiles API (a bare uuid is also accepted). It must reference a profile your account owns; an unknown or not-owned id returns 404. Omit it for a stateless (fresh) session.

A profile can have only one live session at a time. If the profile already has a non-terminal session, the create is refused with 409 profile-in-use (the body’s active_session_id names the live session). This prevents two sessions on the same profile from overwriting each other’s saved cookies and logins. End the named session if you created it — or wait for it to finish — before launching another. Sessions without a profile_id are never affected.

A profile whose device is not offered right now — on hold until the evidence behind it exists, or still in development (the device picker shows it but does not offer it, and GET /v1/archetypes does not list it) — cannot start a session. The create is refused with 409 device-unavailable before any session exists, so nothing starts and no concurrent-session slot is used. The body’s detail is a sentence you can show, such as “This device profile is on hold while we check it against a real iPhone. Pick another device.”; its archetype is the profile’s device id, and held_reason is the reason for the hold in one plain sentence, such as “We’re still checking it against a real iPhone.”, when there is one. A retry with the same Idempotency-Key is refused the same way; a create that already succeeded under that key still replays its 201. Launch a profile on a device from GET /v1/archetypes instead.

The required proxy_id is the saved account proxy the session’s traffic goes out through; pass the bare proxy uuid. Every agent session runs through a proxy you chose — Driftstack has no connection of its own to run it on. To find the id, see Find a proxy’s id: GET /v1/account/me/proxies lists them with a key that has the account_owner scope. It must reference a proxy your account owns — an unknown or not-owned id returns 404 with resource: "proxy". In a team workspace (X-Driftstack-Account), an admin may use a proxy the owner saved or one saved on the admin’s own account — where the desktop app saves every proxy it uses; the session is the owner’s, the proxy stays the admin’s, and the owner cannot list it or start a session through it. The session’s created_by_user_id then names that admin. A create without it (or with proxy_id set to null or "") is refused with 422 proxy-required (code: "proxy_required", and a detail naming the proxy_id field) before anything else happens: no session is created, nothing is recorded under your Idempotency-Key (a retry is refused the same way, and the same key works once you add a proxy_id), and no session limit or rate-limit allowance beyond the request itself is used. The one exception is a retry of a create that succeeded before proxy_id became required: under the same Idempotency-Key it returns that create’s original response, as every retry of a successful create does.

Every session needs a proxy, so drive pages here. Hosted Driftstack runs every session through a proxy of yours — sessions created with POST /v1/sessions too — and the step-by-step operations of /v1/sessions (navigate, interact, wait and the rest) answer 503 driver-not-integrated on a session that runs through a proxy. So drive pages through an agent session: POST /message for an AI turn, POST /steps for steps you write yourself.

Most tasks leave initial_url out and name the page in the message. A message is what tells the agent what to do, so write the address in it either way. Set initial_url when the browser should already be on that page when the session is ready: for a person watching the live view, or for a first message about “this page”. Without it, a session starts on Driftstack’s own home page, which the agent treats as no page at all.

The optional initial_url sets the start URL the remote browser opens on launch, overriding the default start URL. It must be an absolute http(s) URL; file:, javascript:, and data: schemes are rejected (400). An address a navigate step may not open is refused the same way (400, naming initial_url): this computer, a private network, a name only a private network answers (such as http://intranet/ or one ending .local), or a user name and password. Omit it to use the default. For a session that runs a saved profile, initial_url, when given, is where the session starts; the profile’s last active tab is used only when no initial_url is passed.

The first message to a session created with initial_url can arrive while that page is still loading. Until 12 seconds after the session became ready (ready_at), the agent does not read inside the page’s embedded frames, or plan from a page that shows little yet — no heading and fewer than 200 characters of text outside its navigation, header and footer, which a short sign-in form that has fully loaded can also be: it waits out the rest of those 12 seconds without touching the page, then reads it again. So on that first message a page with frames, or one with that little on it, can take up to that long to be read. Within those 12 seconds, a wait for the page to load that you ask for on that page is judged by what the page shows; after them it is the usual wait. A session created without initial_url, and any later page, is not affected.

The optional geolocation explicitly overrides the location reported by the session’s navigator.geolocation (explicit geolocation is not applied yet; the session uses the proxy exit location). By default you should not set this — when omitted, the device’s location is derived from the proxy exit IP, so the reported location is automatically coherent with the session’s apparent network location. Supply explicit coordinates only when you know the proxy’s true physical location better than IP geolocation does; coordinates that diverge from the proxy’s exit country make the session’s fingerprint internally inconsistent (a detectable signal). latitude is -90..90, longitude is -180..180, and the optional accuracy is in meters (omit for the device default). Out-of-range values are rejected (400). Explicit geolocation is not applied yet; the session uses the proxy exit location.

The optional continue_from_agent_session_id is the id of one of your closed agent sessions. The new session starts with that session’s recent conversation, so the agent remembers it — use it to carry on after a session closed with budget-exhausted or transcript-limit. A history that fits in half of the session history limit (128 entries and 512 KiB) carries over whole. A longer one carries your first message and its most recent part, up to that half: the entries between them are left out, so the new session has room to keep going. What it carries is what Driftstack stores: your messages and the AI’s replies (with card numbers, saved credentials and the passwords and codes of four or more characters a step typed into a recognised password or code field, and secret parameters in web addresses, masked) and each step with its site, the step keeping no typed value and no page text (a step held for approval keeps the words of the control it would press). A closed session’s transcript can be emptied by the deployment’s retention, and a session continued from an emptied one carries nothing. An unknown id, or one your account does not own, returns 404; a session that is not closed yet returns 409 conflict.

Set stop_on_exit_ip_change: true to end the session (closed_reason: "exit_ip_changed") if its proxy’s exit IP changes while it runs, so a proxy that silently rotates cannot carry on from a new apparent location. It defaults to false.

Set network_detail: true to ask for this session’s network request detail. It defaults to false. Where the deployment does not offer request detail, true is refused with 400 bad-request (code: "network_detail_not_offered") and nothing is created; it is never silently ignored.

The create normally tests the proxy you named live first and refuses with 422 proxy-validation-failed (its reason says why — see Account proxies) if it does not work, before any session exists. If the test cannot tell either way — nothing answered Driftstack’s servers at all, or the test failed on our side — the create is not refused, because the session connects from a different network than the test (see When the check cannot tell). skip_proxy_probe: true skips that test for this launch only.

No device free

Driftstack picks the machine for each new session, preferring one with room for it and, when your proxy’s exit country is known, one near that exit that has room. There is nothing to configure, and it is not a promise about where a session runs.

When none of the machines that run sessions can take a new one — none is connected, each one is in maintenance, or none can run the session’s device profile — the create is refused with 503 feature-unavailable (code: "dispatch_no_live_node", retryable: true, retry_after_seconds) and a Retry-After header, and nothing is created: no session, no session time counted on your plan, and nothing stored under your Idempotency-Key. Wait the Retry-After seconds and send the same request again; the same key is fine, and starts the session once a device is free. A create that would be refused for another reason — your plan, an ended trial, used-up trial time, storage, or your concurrent-session limit — gets that refusal instead, since waiting for a device would not change it. POST /v1/sessions answers the same way.

On a free trial, the same 503 can also carry trial_capacity: true, with Retry-After: 60 and the detail “Free-trial sessions are at their limit right now…”: the free-trial sessions running across the whole service are at their limit just then. Nothing is created and nothing is stored under your Idempotency-Key, so send the same request again after the wait, with the same key if you sent one. Paid accounts never get it. The reopen (POST /v1/agent-sessions/{id}/reopen), POST /v1/sessions and POST /v1/profiles/{id}/launch answer the same way; on the last two, a trial create that loses a race for the last place can show in your session list as errored, and no session starts.

Sent again while the first one starts. A create sent again with the same Idempotency-Key while the first one is still placing the session’s browser on its Mac waits for that, up to about 20 seconds, and then answers the session with its livekit live view, for the Mac that runs it. If the session is still starting after that, the answer is 503 feature-unavailable (code: "session_starting", retryable: true) with a Retry-After of 5 seconds. That session exists, so send the same request again with the same key; it answers the session once it is placed. A session whose first create stopped before placing it is closed instead, and the answer is that closed session.

When the only reason is that the machines are being updated (each connected one is in maintenance), the create does not refuse at once: it waits up to 25 seconds for one to come back, and starts the session as normal if one does. If none does, the answer is the same 503 with node_updating: true, a Retry-After of 60 seconds and the detail “The device is being updated…” — and nothing is created, so the same request with the same key is fine. Closing the connection while the create waits ends the wait; nothing is created. POST /v1/sessions and POST /v1/profiles/{id}/launch wait the same way for a session started through a proxy.

A machine can also be booked for maintenance. When every machine that could run the session is in such a window, the create is refused with 503 maintenance (code: "maintenance", retryable: true) and a Retry-After header, and nothing is created: no session, no session time counted on your plan, and nothing stored under your Idempotency-Key. until is when the window ends (ISO 8601, UTC) and retry_after_seconds the seconds until then, the same as Retry-After. The detail reads “All machines that can run this session are in maintenance until the time given; it may end early.” A window can end before until, so the same request can be sent again sooner; after until it is no longer refused for this window. The same key is fine. Sessions already running are not affected. POST /v1/sessions and POST /v1/profiles/{id}/launch answer the same way.

Only POST /v1/agent-sessions (and its reopen) tells the device apart; the two doors above still answer maintenance. When a machine that runs the session’s device profile is in a maintenance window with a known end, the answer is maintenance; until is the earliest end of any maintenance window on the service, so it can come before the window of the machine you need, and Retry-After is at most an hour. When no machine runs the device, or every one that does is out of service with no known end, the create is refused with 503 feature-unavailable (code: "device_not_served", retryable: false, archetype: the device id; no until, no retry_after_seconds and no Retry-After header) and nothing is created. The detail reads “This phone model isn’t available right now. Try again later, or make a profile with another model.” The SDKs raise it as FeatureUnavailableError, whose isRetryable() is false.

Launching through the profile’s proxy

A create that names a profile_id and no proxy_id uses the proxy the profile is bound to (default_proxy_id), so the same profile leaves through the same exit on every computer. A detached profile — its proxy was deleted — is refused with 409 (resource: "profile", code: "proxy_detached", This profile’s proxy is no longer available. Choose a proxy.) and nothing is created: it is never given another proxy. An unset profile, or no profile, with no proxy_id is the 422 proxy-required above. An explicit proxy_id always wins over the profile’s. The profile’s geolocation and stop_on_exit_ip_change default onto the session when the body omits them. Explicit geolocation is not applied yet; the session uses the proxy exit location.

Every session carries profile_id beside proxy_id, so a second computer can show a profile as running. Creating a session stamps the profile’s and the proxy’s last_used_at.

Over your plan. After a downgrade, or on the trial (which keeps one saved profile), an account may hold more saved profiles or proxies than its plan allows. Nothing is deleted: the most recently used, up to the plan’s number, keep launching — on the trial, only the most recently used profile; the rest are refused with 429 (tier-limit, code: "over_plan_limit", resource: "profile" or "proxy") until you delete some or upgrade. The same rule applies to POST /v1/sessions with a profile_id and to POST /v1/profiles/:id/launch.

Create errors

Status Type When
400 validation-failed a field fails validation (for example a non-http(s) initial_url or an out-of-range geolocation)
400 bad-request proxy_id names an HTTP proxy; browser sessions need a SOCKS5, OpenVPN or WireGuard proxy
400 bad-request network_detail: true where the deployment does not offer request detail (code: "network_detail_not_offered"); nothing was created
402 trial-ended the free trial or the plan has ended, or the subscription is paused; nothing was created. code says which (for example plan_ended, payment_failed, subscription_paused or trial_already_used); a free trial that simply ended carries none
403 forbidden ai or pair on a plan without the AI agent; an Opus or Fable model without your own key (requires_own_key: true, model); a team member without the admin role (required_role: "admin"); a VPN proxy your plan does not include (feature: "vpnEgress"); an archetype, or a profile_id’s device, your plan does not include (code: "device_not_on_plan")
404 not-found profile_id, proxy_id (resource: "proxy"), driftstack_session_id or continue_from_agent_session_id is unknown or not yours
409 profile-in-use the profile already has a live session (carries active_session_id)
409 device-unavailable the profile’s device is not offered right now — on hold or still in development (carries archetype, and held_reason when there is one); nothing was started
409 conflict continue_from_agent_session_id names a session that is not closed yet
409 conflict the proxy is an OpenVPN or WireGuard proxy and no machine that could run the session has VPN egress yet (code: "vpn_egress_not_available_yet", resource: "proxy", retryable: false); nothing was created and a wait does not change it: use a SOCKS5 proxy for now
409 storage-quota-exceeded your plan’s profile storage is full (profile-backed creates only)
422 proxy-validation-failed the live proxy test failed (carries reason)
422 proxy-required the body names no proxy_id; nothing was created (carries code: "proxy_required")
429 concurrency-limit your account already has as many open agent sessions as your plan allows (carries current_sessions, limit, and open_session_ids — see below); a provisioning session counts
429 tier-limit the free trial’s 2 hours of session time are used up (code: "trial_session_time_used"); or profile_id / proxy_id is outside the ones your plan can launch (code: "over_plan_limit")
429 rate-limited your last sessions could not connect through their proxy, so the next one waits (code: "proxy_failure_backoff", retryable: true, retry_after_seconds and Retry-After; proxy_id when you started the failing session). detail reads: “We could not connect through your proxy “<label>”. Check its address and login (Proxies → Test); if it works elsewhere, write to support, as the problem may be on our side. So that the machines stay free for everyone, the next session can start in <N> s.” (<label> is the name you gave the proxy, <N> the seconds of Retry-After), or, when that proxy cannot be named to you (deleted since, or a teammate’s), “We could not connect through your proxy. Check its address and login (Proxies → Test); if it works elsewhere, write to support, as the problem may be on our side. So that the machines stay free for everyone, the next session can start in <N> s.”; when the newest failure was a proxy that cannot carry UDP, the cause sentences are instead “Your proxy does not carry UDP, which the live session needs. Use a proxy that does (SOCKS5 with UDP ASSOCIATE, WireGuard or OpenVPN); if it should, write to support.”, and for a VPN that gave the session no DNS server “Your VPN did not give the session a DNS server. Check the VPN’s DNS setting; if it is set, write to support.”. Nothing was created; send the same request (and Idempotency-Key) again after the wait. The limit is per account for now: until a session of the account gets ready, a session on another proxy waits too
429 rate-limited your account is at its limit of sessions starting: 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, retry_after_seconds and Retry-After). detail reads: “You already have <LIMIT> sessions starting, the most at once. A new one can start as soon as one of them is ready (about <N> s).” (<LIMIT> is that number; “1 session starting” when it is 1). Nothing was created; send the same request again after the wait. The limit is per account for now
503 feature-unavailable no machine could take a new session just then (code: "dispatch_no_live_node", retryable: true, with Retry-After); nothing was created, so send the same request again after the wait, with the same Idempotency-Key if you sent one (on a free trial it can also carry trial_capacity: true) — see No device free
503 maintenance every machine that could run the session is in a booked maintenance window (code: "maintenance", retryable: true, with until, retry_after_seconds and Retry-After); nothing was created, so send the same request again after until, with the same Idempotency-Key if you sent one — see No device free
503 feature-unavailable no machine can run the session’s device profile soon: none runs it, or every one that does is out of service with no known end (code: "device_not_served", retryable: false, archetype; no until, no retry_after_seconds, no Retry-After); nothing was created, so try again later or make a profile with another device — see No device free

An open agent session counts against your plan’s limit until it is closed, whether or not a turn is running: until you close it (DELETE /v1/agent-sessions/{id}) or it ends on its own (after a time without activity, at its time limit, when its budget runs out, or when the machine running it loses it). A session that has ended is closed and its slot is free at once: no DELETE is needed. So on a plan whose limit is 1, a session an earlier run left open makes the next create answer 429 concurrency-limit (current_sessions and limit in the body, no Retry-After), and nothing is created. It is not a fault: list your open sessions with GET /v1/agent-sessions?status=open, close the ones you created and no longer need, and create again. Never close a session you did not create: a person may be using it. A session started with POST /v1/sessions through a proxy is listed here too, as a manual session, and holds one of these slots until it is destroyed. Close sessions in a finally block so a run that fails part-way does not leave one open.

The 429 body also names the sessions holding the slots:

{
  "type": "https://errors.driftstack.dev/concurrency-limit",
  "title": "Concurrent session limit reached",
  "status": 429,
  "detail": "Account already has 1 active sessions; tier permits 1.",
  "current_sessions": 1,
  "limit": 1,
  "open_session_ids": ["agt_3f1c9e2a-6d4b-4c1e-9a7f-2b8d5e0c4a11"],
  "open_session_ids_truncated": false
}
  • open_session_ids — the ids of the sessions the limit counts (status provisioning or active), newest first, at most 100. They are sessions of the account the create is for: yours, or the team owner’s when you create on a team as its admin. Ids only, nothing else about them. They are read just after the refusal, so one may have closed, or another opened, since.
  • open_session_ids_truncated — true when there were more than 100; page GET /v1/agent-sessions?status=open for the rest. A session paused by a bot check still reads active and holds a slot; no session reads paused today.
  • Both are absent when the ids could not be read, and when the key that sent the create cannot list sessions (the list needs read:sessions, broad read or account_owner; a create needs only write). The 429 is the same. A key that can list sessions finds them with ?status=open; a key that cannot needs one that can. Absent never means “no session is open”.
  • A reopen refused at the limit carries the same two fields, read the same way, for the account the reopened session would belong to.
  • current_sessions is your plan’s limit, not a count: it reads the same as limit. If your limit was lowered while more sessions were open, open_session_ids can list more ids than current_sessions says.

A client whose earlier process died can close the session it left behind with DELETE /v1/agent-sessions/{id}, then create again. Close only an id your program created (keep the ids you create): the list can hold a session a person is using or one another program started. The SDKs read both fields on ConcurrencyLimitError from their next releases: openSessionIds / openSessionIdsTruncated from TypeScript 0.6.0, open_session_ids / open_session_ids_truncated from Python 0.6.0, and OpenSessionIDs / OpenSessionIDsTruncated from Go 0.8.0. Until you are on one of those, read them from the error’s raw problem document: err.extensions.open_session_ids (TypeScript), err.problem.get("open_session_ids") (Python) or err.Problem["open_session_ids"] (Go).