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 whenmodeis 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 403forbiddentier 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 with429(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 inmanualmode, 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}/stepsand 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 afterreadyturnstrue. For anaisession 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-keyheader 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 isfalse, no model runs on any key, and every other field isfalse.credits_fund_turns: whether this account’s turns may be paid from its AI credits:truewhen they are, and also when that cannot be read. Filter by your AI source first. When it isfalse, 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 withon_credits: falseis refused with403andnot_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 withown_key: trueandautomatic_with_key: falseis refused there with403andnot_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_turnsistrue, models withautomatic_with_keyif a usable key is saved, else models withon_credits; when it isfalse, models withincluded_ai, plus models withown_keywhen 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 (statusprovisioningoractive), 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—truewhen there were more than 100; pageGET /v1/agent-sessions?status=openfor the rest. A session paused by a bot check still readsactiveand holds a slot; no session readspausedtoday.- 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, broadreadoraccount_owner; a create needs onlywrite). 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_sessionsis your plan’s limit, not a count: it reads the same aslimit. If your limit was lowered while more sessions were open,open_session_idscan list more ids thancurrent_sessionssays.
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).