Agent sessions
An agent session is an iPhone Safari browser that an AI drives from plain-language messages, or that runs steps you write. This reference is in several pages, each short enough to fetch whole. Everything you may need to call is listed here, with a link to its section. To run a task from start to finish, read Run AI tasks from your code first; it is the shorter path and covers create, message, stop and close.
The pages: what a step can do, the session resource and errors, create, message, run a list of steps and secrets, live view and browser state.
| Call | What it does | Section |
|---|---|---|
POST /v1/agent-sessions |
Start a session | Create |
GET /v1/agent-models |
How your plan may run each model | Models |
GET /v1/agent-sessions |
List your sessions | List |
GET /v1/agent-sessions/{id} |
Read one session (ready, status) |
Get |
POST /v1/agent-sessions/{id}/message |
Give the AI a task in plain words | Message |
POST /v1/agent-sessions/{id}/steps |
Run steps you write, with no AI | Run a list of steps |
POST /v1/agent-sessions/{id}/stop |
Stop the running task | Stop the running turn |
DELETE /v1/agent-sessions/{id} |
Close the session | Close |
POST /v1/agent-sessions/{id}/reopen |
Reopen an ended session | Reopen |
POST /v1/agent-sessions/{id}/budget |
Add tokens | Add tokens |
POST /v1/agent-sessions/{id}/resume |
Resume a session a check paused | Resume |
GET /v1/agent-sessions/{id}/transcript |
Read the session’s history as a stream | Transcript stream |
GET /v1/agent-sessions/{id}/frames |
List the frames of the page | List the frames |
GET /v1/agent-sessions/{id}/page-state |
Read the page’s address and title | Page state |
GET /v1/agent-sessions/{id}/captures/{captureId} |
Fetch a screenshot | Fetch a screenshot |
POST /v1/agent-sessions/{id}/secrets |
Register a session secret | Register a secret |
GET /v1/agent-sessions/{id}/secrets |
List session secrets | List secrets |
DELETE /v1/agent-sessions/{id}/secrets/{handle} |
Delete a session secret | Delete a secret |
POST /v1/agent-sessions/{id}/livekit-token |
Get a token for live video | Live video |
GET /v1/agent-sessions/{id}/network |
Read the network log | Network log |
GET /v1/agent-sessions/{id}/network/{entryId} |
Read one request’s detail | Request detail |
GET /v1/agent-sessions/{id}/cookies |
Read the cookie jar | Read the cookie jar |
POST /v1/agent-sessions/{id}/cookies/set |
Import cookies | Import cookies |
GET /v1/agent-sessions/{id}/profile-notes |
Read the profile’s notes | Read the notes |
PUT /v1/agent-sessions/{id}/profile-notes |
Replace the profile’s notes | Replace the notes |
POST /v1/agent-sessions/{id}/history |
Go back or forward | Step browser history |
POST /v1/agent-sessions/{id}/tabs |
Open, switch, close or list tabs | Tabs |
POST /v1/agent-sessions/{id}/files |
Upload a file | Upload a file |
GET /v1/agent-sessions/{id}/downloads |
List downloads | List downloads |
GET /v1/agent-sessions/{id}/downloads/content |
Fetch a download | Fetch a download |
Other sections people look for: Sizing the token budget, Streaming the turn, Turn and session limits, Step results are the record, When a check pauses the session and Errors.
Choosing the n-th element
See Choosing the n-th element.
Driving by selector
See Driving by selector.
Step results are the record
A turn that ran steps (kind: "plan-executed") lists every step that ran in
results, in order. Each entry carries the step itself as intent — the
selector it acted on, the value it typed, the address it opened — and what
happened: a step that succeeded has a summary of what the session reported,
and one that failed has a reason instead. Build your program on results:
answer is text written for a person, while results is what was done on the
page. A list of steps you send with POST /steps
answers in the same shape.
A message:
{
"user_message": "Sign in at https://shop.example.com/login as ana@example.com with the password {{credential:cred_9f2c41ab7d6e4850bc13f07a5e9d2c88}}, open Orders and tell me the latest order number."
}
What came back:
{
"kind": "plan-executed",
"session": { ...AgentSession },
"intents": [ ... ],
"results": [
{
"kind": "success",
"intent": { "kind": "navigate", "url": "https://shop.example.com/login" },
"summary": "navigated to https://shop.example.com/login"
},
{
"kind": "success",
"intent": { "kind": "interact", "action": "type", "selector": "#email", "value": "ana@example.com" },
"summary": "typed into #email"
},
{
"kind": "success",
"intent": { "kind": "interact", "action": "type", "selector": "#password", "sensitive": true },
"summary": "typed into #password"
},
{
"kind": "success",
"intent": { "kind": "interact", "action": "tap", "selector": "#sign-in" },
"summary": "tapped #sign-in: the button “Sign in”"
},
{
"kind": "success",
"intent": { "kind": "interact", "action": "tap", "selector": "a[href=\"/orders\"]" },
"summary": "tapped a[href=\"/orders\"]: the link “Orders”"
}
],
"ok": true,
"answer": "Your latest order is A-10482, placed on 2 October 2026."
}
How to read it:
results[i].intent.selectoris the element the step acted on, exactly as it was sent. Use it to run the step again yourself withPOST /steps, or to name the element in your next message.results[i].intent.valueis the text atypestep typed. A step that typed a saved credential or a session secret shows its placeholder ({{credential:cred_…}}), never the value. A secret is left out, and the step comes back withsensitive: trueand novalue(the third step above), when the step is markedsensitive: true, when its selector names a password, one-time-code or card field (#password,[autocomplete="one-time-code"],#cvv), or when it typed, outside an embedded frame, into a field the session had already read as secret on the page it was on. A password field with a plain name on a page the session has not read yet, or inside an embedded frame, is not recognised: mark such a stepsensitive: trueyourself.results[i].summary, on a step that succeeded, is what the session reported, on one line. After a tap, it names the element the selector resolved to when the session can say (the button “Sign in”), so you can check the tap landed where you meant.- A failed step is
kind: "failure"with areason, and usually adiagnosisto branch on; a step held for your approval iskind: "confirmation_required"and did not run. Both are described under Message. okisfalsewhen the last planned steps had a failure, when a step is waiting for your approval, or when the turn did less than the message asked for (the full rule is under Message). A turn that recovered from a failed step can showok: truewith that failure still inresults, so check eachresults[i].kind.ok: truedoes not by itself mean the task is finished: when it is not, the turn carriesnotice(a sentence) andnotice_reason(one word to branch on), for example"notice_reason": "step_limit"— send “continue” to carry on.intentsis every step the turn attempted, including any it planned and never ran, so it can be longer thanresults. Read outcomes fromresults.
What stays the same between releases
intent— inintentsand in eachresults[i].intent— is the replay contract. It has theAgentIntentshape of the OpenAPI document, its fields keep their meaning, and a step sent back toPOST /stepsexactly as it came is accepted and does the same thing. Build on it.summaryandreasonon a step, andanswer,answer_unavailable,noticeandclarifying_questionon a turn, are text for a person. Their wording can change between releases, without notice: show them, log them, but do not match on them. A program branches onkind,diagnosis.category,diagnosis.retryable,notice_reason,warning.kindand the typed fields (value,values,captureId,dialog), never on the words.
Turn an AI run into fixed steps
An AI turn tells you exactly what it did, so a task that works once can run again without the AI:
- Send the task as a message and check the turn:
kind: "plan-executed",ok: true, and everyresults[i]a success. - Take the steps from
results[i].intent, in order (orintents, which also lists any step that was not run). A typed value that was stored assensitive: truecomes back without itsvalue: put the value — or its{{credential:…}}placeholder — back in before you replay. So does the text typed into a prompt by adialogstep, which comes back as{{prompt text not stored}}. - Send them to
POST /v1/agent-sessions/{id}/stepson a session at the same starting page, at most eight per call. No model is called, and each step is done exactly as written. - Read the new
resultsthe same way. A step that read the page (extract) returns its text insummary(orvalue/values), and that text is the page’s, so read it; do not compare it with the earlier run’s words.
What the AI did that is not a step is not replayed: it looked at the page
(and at a screenshot) before each round of steps, and it waited out a page
that was still loading. Add a wait step (idle, or selector_visible for
the element you need next) where the page changes in between. A tap_at
needs no screenshot in a list of steps (see tap_at above), so the AI’s
point is sent as it was; a page whose layout changed puts something else
under it, so a tap by selector is the sturdier replay where the page offers
one.
Resource shape
This section is on its own page: Resource shape.
Create
POST /v1/agent-sessions
This section is on its own page: Create.
A shorter idle timeout
Sizing the token budget
No device free
See No device free.
Launching through the profile’s proxy
See Launching through the profile’s proxy.
Create errors
See Create errors.
List
GET /v1/agent-sessions
Your agent sessions, newest first, cursor-paginated. Returns the standard
envelope: { data, has_more, next_cursor }. limit defaults to 50 and
caps at 100 — that is the page size, not a ceiling on what you can reach;
pass the prior page’s next_cursor to continue. All three SDKs wrap this
as list(), with iterate() to walk every page.
Filter by status. ?status= lists only the sessions with
that status:
status |
Lists the sessions whose status reads |
|---|---|
provisioning |
provisioning: still starting, with a start-up step in provisioning_detail |
active |
active: running, or still starting with no step reported (ready: false; see Resource shape) |
paused |
paused (no session reads this today: a session paused by a bot check reads active) |
closed |
closed: ended |
open |
anything but closed: provisioning, active or paused |
Without status the list is every session, as before. One value per request.
Any other value — an unknown word, an empty status=, or status sent twice —
is refused with 400 validation-failed, and issues.fieldErrors.status names
the field; nothing is listed. So is status under another spelling of the key
(status[]=open, status[0]=open, Status=open): send the plain key
status, once. Keep the same status when you pass
next_cursor: the cursor works the same with a filter. A session’s status can
change while you page, so a session that ended between two pages may be missing
from an open walk or appear in a later closed one.
curl -s "https://api.driftstack.dev/v1/agent-sessions?status=open" \
-H "Authorization: Bearer $DRIFTSTACK_API_KEY"
The SDKs take it from their next releases: list({ status: 'open' }) from
TypeScript 0.6.0, list(status="open") from Python 0.6.0 and
List(ctx, &ListAgentSessionsQuery{Status: "open"}) from Go 0.8.0;
iterate() takes it too and keeps it on every page. Earlier versions do not
send it (TypeScript 0.5.0 ignores a status it does not know and lists every
session), so until you upgrade, call GET /v1/agent-sessions?status=open
directly, as in the curl above.
Requires read:sessions, broad read, or account_owner.
Team members need the admin role here, unlike the plain session
list. An agent session carries the model transcript and live control
state, so the collection is not widened to read-only members — a member
acting on an owner gets 403. See
Team RBAC.
Get
GET /v1/agent-sessions/{id}
Returns the resource above. Cross-account lookups return 404 (no existence disclosure).
Message
POST /v1/agent-sessions/{id}/message
This section is on its own page: Message.
When a turn is refused
Confirming a purchase, payment or account deletion
See Confirming a purchase, payment or account deletion.
When it stops
See When it stops.
Streaming the turn (SSE)
Turn and session limits
Close
DELETE /v1/agent-sessions/{id}
Sets status='closed' with closed_at stamped and closed_reason: "customer-closed", ends the session’s browser, and returns 204 No Content.
Idempotent: closing a closed session also returns 204. Closing frees one of
your open-session slots, and a profile-backed session saves the profile’s
cookies and storage as it ends — close in a finally block.
Closing also stops the turn the session is running, if it is running one. The AI
call in flight is stopped, and the POST /message that started it answers
kind: "stopped" with the steps that ran and a usage covering what that call
had used when it stopped — the same answer Stop gives,
so a close never leaves you paying out a call you closed the session to cut
short. A close does not fail, or wait long, on any of this: with nothing running
it answers straight away, a turn that will not stop is left to finish on its own
rather than holding the close open, and closing stays safe to repeat.
Optionally say why with the reason query parameter, for example
DELETE /v1/agent-sessions/{id}?reason=user_stop. One of user_stop (a
person stopped it), new_chat, opened_other_chat or chat_deleted (an app
ended it for a chat change), signed_out, switched_account or app_quit.
It is stored as the session’s customer_close_reason, and the session’s
closed_reason_detail then says which it was, so whoever reads the session
later can tell a deliberate stop from an app that closed it. Without it,
customer_close_reason is null. Only the call that actually closes the
session records it: closing an already-closed session changes nothing, its
reason included. Any other value is refused with 400 and the session is not
closed.
Close only a stuck session
DELETE /v1/agent-sessions/{id}?only_if_stale=1 closes the session only when
it is stale: the Mac running it reported it and then sent no report for more
than 35 seconds. Otherwise the answer is 409 with code: "session_not_stale"
and liveness saying why, and the session, with any turn it is running, is
left alone:
liveness: "fresh"— the session is still reporting in.liveness: "unknown"— this server has no report for the session, so it cannot be shown to be stale.
An already-closed session still answers 204. Any value other than 1 is
refused with 400 and nothing is closed. The key must be spelled exactly
only_if_stale: another spelling (ONLY_IF_STALE, only_if_stale[]) is
ignored like any unknown key, and the session is closed unconditionally. It
combines with reason.
A server that honours the parameter lists it in the session’s
close_conditions (["only_if_stale"]). An older server ignores the parameter
and closes the session anyway, so read the session first and send
only_if_stale=1 only when close_conditions includes it.
Reopen an ended session
POST /v1/agent-sessions/{id}/reopen
Starts a new agent session with the configuration of one that has ended,
so you can pick a closed run up again without assembling the create body
yourself. The request body is empty ({}, or none).
The new session belongs to the same account and runs with the ended session’s:
- proxy (
proxy_id) — the one its traffic was going out through when it ended; - profile (
profile_id), when it ran one; - device (
archetype). A device drawn at random is usually the same device again (a new one is drawn if that device is no longer offered at random) and still readsarchetype_source: "random"; a device that came from the profile is the profile’s; mode,model, token budget (token_budget_total) andstop_on_exit_ip_change.
Nothing of the ended session is carried over: no transcript, no steps, no
page, no cookies. The new session starts with an empty transcript and a full
token budget. Driftstack does not keep what happened in a session, so there is
nothing to restore beyond its configuration — a profile-backed session still
starts from the profile’s saved cookies and storage, exactly as a new launch of
that profile does. The start page (initial_url) and a fixed geolocation
sent in the original create are not kept either, so the new session opens on
the default page with its location derived from the proxy exit.
Explicit geolocation is not applied yet; the session uses the proxy exit location.
network_detail is not kept either: create a new session to ask
for request detail again. To carry the conversation over instead, create a
session with continue_from_agent_session_id.
The answer is 201 with the new session, whose resumed_from names the ended
one:
{ "id": "agt_<new-uuid>", "status": "active", "resumed_from": "agt_<ended-uuid>", "transcript_length": 0, ... }
When it can be reopened. Once it has ended — whatever ended it: you, an
idle timeout, a time limit or a failure — and for 7 days after its
closed_at. Otherwise nothing is started:
409withcode: "session_not_ended"— the session is still running;410(typesession-destroyed,code: "reopen_window_passed",reopen_window_days: 7) — it ended more than 7 days ago; create a new session instead;409withcode: "reopen_configuration_unavailable"— its configuration cannot start a session today: it ran without a proxy of yours, its profile was deleted, or a device or model it used is no longer offered (fieldsnames which);404— the id is not a session you can access (another account’s id gets the same404as one that does not exist). Who may reopen is who may act on the session: the account it belongs to, or an admin of that account’s team.
A reopen is a new session. It counts against your concurrent-session limit,
your plan and the free trial’s session time exactly as a
create does, gets the same refusals (429 concurrency-limit,
429 tier-limit with code: "trial_session_time_used", 402 trial-ended,
503 with code: "dispatch_no_live_node" when no device is free,
503 maintenance during a booked maintenance window, the proxy check’s 422,
and the rest), and is billed as a new session. Send an
Idempotency-Key to make a retry safe: the same key replays the session the
first request started.
curl -X POST https://api.driftstack.dev/v1/agent-sessions/$SESSION_ID/reopen \
-H "Authorization: Bearer $DRIFTSTACK_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"
From an SDK: client.agentSessions.reopen(id) in TypeScript,
client.agent_sessions.reopen(id) in Python and
client.AgentSessions.Reopen(ctx, id, nil) in Go.
Add tokens to a running session
POST /v1/agent-sessions/{id}/budget
Adds tokens to a session that is still running, so a long job does not have to start over in a new session. The session keeps its page, cookies and transcript.
{ "add_tokens": 500000 }
add_tokens is the only field: a whole number above 0. It is added to both
token_budget_total and token_budget_remaining in one step, and the answer is
200 with the session and its new numbers. If a turn stopped because the
budget ran low (notice_reason: "budget_low"), top up first, then send the
next message (for example continue) and the agent plans again with the new
budget. A message sent before the top-up is refused without running, and says
how many tokens to add.
- The new total may not pass 10,000,000 tokens. Past that the answer is
409withcode: "token_budget_cap_exceeded",token_budget_total(the total now) andmax_add_tokens(the most you can still add); nothing is added. - Only a session that has not ended (
statusprovisioning,activeorpaused) can be topped up. A session that has ended is409withcode: "session_not_running"(session_status, andclosed_reasonwhen it has one). That includes a session the budget itself ended (closed_reason: "budget-exhausted"): top up before the budget runs out, or reopen it, which starts a new session with the ended one’s total, top-ups included. - Top-ups sent at the same time never lose one another’s tokens, and together
they never pass the limit: near 10,000,000, the one that would pass it is
refused with
token_budget_cap_exceededand adds nothing. - A top-up that arrives while a turn is spending the session’s last tokens can
lose: the turn still closes the session (
budget-exhausted). Top up before the budget is that low. - A top-up is not idempotent: if you retry one whose answer you lost, the
tokens are added again (check
token_budget_totalwith aGETfirst). - A body with any other field, or an
add_tokensthat is not a whole number above 0 (or is over 10,000,000), is400and nothing is added. 404— the id is not a session you can access (another account’s id gets the same404as one that does not exist). Who may top up is who may act on the session: the account it belongs to, or an admin of that account’s team, with a key that has thewritescope.
It works the same whether the session runs on your own Anthropic key or on Driftstack’s included AI. Each top-up is recorded in the account’s audit log.
curl -X POST https://api.driftstack.dev/v1/agent-sessions/$SESSION_ID/budget \
-H "Authorization: Bearer $DRIFTSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"add_tokens": 500000}'
From an SDK: client.agentSessions.addTokens(id, 500000) in TypeScript,
client.agent_sessions.add_tokens(id, 500000) in Python and
client.AgentSessions.AddTokens(ctx, id, 500000) in Go.
Stop the running turn
POST /v1/agent-sessions/{id}/stop
Stops the turn a POST /message started, without closing the session. Anyone
who may send a message to the session may stop its turn; a session you cannot
access answers 404, as it does everywhere else. The request body is empty
({}).
It requests the stop and returns at once — it does not wait for the turn to wind down:
// 202 — a turn was running and has been asked to stop
{ "status": "stop_requested", "session_id": "<id>" }
// 200 — nothing was running, so there was nothing to stop
{ "status": "no_turn_running", "session_id": "<id>" }
Stopping is idempotent: calling it again, or calling it when nothing is running, is always safe.
A stop sent right after the message is covered too: the turn counts as running
from the moment the message is accepted, before any planning starts. If you get
200 no_turn_running while your own POST /message is still waiting for its
answer, either the turn has just finished (its response is on its way) or the
message has not reached the turn yet; asking again a second later is safe and
settles which. A 503 (feature-unavailable) carrying stop_unconfirmed: true means we could not confirm the stop and the turn may still be running;
call it again. A 503 of the same type without that field means AI chat is
not enabled on this deployment, and calling again will not help.
The SDKs return the { status, session_id } body so you can branch on it:
agentSessions.stop(id) in TypeScript, agent_sessions.stop(id) in Python
(sync and async) and AgentSessions.Stop(ctx, id) in Go. Each raises the 503
as the typed feature-unavailable error, with stop_unconfirmed among the extra
fields, and none of them retries a stop by itself. Because message() blocks
until the turn ends, call stop from another thread, task or goroutine.
The turn itself ends on its own POST /message response, which comes back
as kind: "stopped" with the steps that ran and a notice saying how far it
got (or, if the turn was already finishing, as its ordinary result). That
response — not the 202 — is your signal that the session will accept the next
message; send it after the response arrives. On the streaming lane the terminal
response frame carries the stopped body, preceded by a notice frame with
the same sentence.
If the message was already answered 202 still-running, no further
response comes on that request. With its Idempotency-Key, the same request
sent again with that key returns the stopped body once the turn has ended.
Without a key, the turn has ended once POST /{id}/stop answers
200 no_turn_running.
What happens to the work in progress:
- Nothing new starts. No further step is sent to the browser and no further planning call is made once the stop is observed.
- A step already running is not abandoned blind. If a tap, a typed value or
a navigation was already on its way to the browser, the turn waits a short,
bounded time (about fifteen seconds) for its result and reports it. If it does
not answer in time, the step is reported as a failure with
diagnosis.category: "unknown": it may have happened. Check the page before doing it again. A step that only reads or waits is cut short at once. - A model call in progress is ended, and what it used is still counted in
usageand against the session’s token budget. - The read-back is skipped. If every step had already run, the turn ends
without answering the question; the
noticesays so.
Run a list of steps
POST /v1/agent-sessions/{id}/steps
This section is on its own page: Run a list of steps.
What each step looks like
See What each step looks like.
How steps run
See How steps run.
How long a step waits for a missing element
See How long a step waits for a missing element.
Preview steps (eval accounts)
See Preview steps (eval accounts).
Steps inside an embedded frame
See Steps inside an embedded frame.
Dialogs
See Dialogs.
List the frames of a page
GET /v1/agent-sessions/{id}/frames
This section is on its own page: List the frames of a page.
Session secrets
This section is on its own page: Session secrets.
Limits
See Limits.
Register a session secret
POST /v1/agent-sessions/{id}/secrets
This section is on its own page: Register a session secret.
List session secrets
GET /v1/agent-sessions/{id}/secrets
This section is on its own page: List session secrets.
Delete a session secret
DELETE /v1/agent-sessions/{id}/secrets/{handle}
This section is on its own page: Delete a session secret.
Fetch a captured screenshot
GET /v1/agent-sessions/{id}/captures/{captureId}
This section is on its own page: Fetch a captured screenshot.
Live video (LiveKit)
POST /v1/agent-sessions/{id}/livekit-token
This section is on its own page: Live video (LiveKit).
Live transcript stream (SSE)
GET /v1/agent-sessions/{id}/transcript
This section is on its own page: Live transcript stream (SSE).
Resume a challenge-paused session
POST /v1/agent-sessions/{id}/resume
When the session detects a bot-challenge (DataDome /
Arkose / PerimeterX / AWS-WAF / GeeTest / …) it auto-pauses the
session and emits a session.challenge_detected
webhook. After you resolve the challenge (e.g. in the live view),
call this to resume the agent.
Body: { "challenge_id"?: "<id-from-the-event>" }
challenge_id (optional) correlates to the
session.challenge_detected you are responding to — when present,
Driftstack checks it against the active challenge (a stale id leaves
the session paused); when absent, it is a manual override resume.
Response 202:
{ "status": "resume_requested", "session_id": "<id>" }
404 if the session is not found or not owned by your account; 409
if the session is in a terminal state (resume requires an active
session). Not available on every deployment.
A session paused by a bot check still reads status: "active" — the pause is
not reflected in status. Watch for the
session.challenge_detected webhook instead.
When a check pauses the session
Driftstack does not solve verification checks and does not use a solver key, so a check is yours to deal with in the live view. A code the site sends by email or text message is always yours to enter. The session pauses as soon as the check is detected on the page, and stays paused until it is resumed — by the resume call above, or the live view’s Resume button.
What a message sent to a paused session gets back: the step is refused, the
turn ends, and notice says what is going on with notice_reason beside it
(the same pair on the notice frame of the
progress stream). A program branches on the
word:
challenge_solver_pending— not sent: Driftstack does not solve checks, and no solver key can be saved; if you ever see it, treat it as a check that needs a person.challenge_customer_handoff— the check needs a person. Solve it in the live view, resume, then send “continue”.verification_email/verification_sms— the site sent a code to your inbox or your phone. Enter it in the live view, resume, then send “continue”.unexplained_pause— the session is paused and the page shows no check, or the reason could not be confirmed. Open the live view to see what the page is showing, resume, then send “continue”.
A solver key cannot be saved. If one was saved before, Settings shows it as stored and not used, and a signed-in person can delete it there; an API key cannot.
The nine endpoints below operate on the live, running session
(they are what the desktop app’s page overlay, network pane, Cookies
drawer, back/forward buttons, Files pane, and download bar call). Reads
need read:sessions (broad read or account_owner also work); writes
gate on the broad write scope (see the scope note at the top of this
page). Apart from the page-state poll, each returns a discriminated
200 body in every case — status is one of ok, unavailable
(the session is not running, cannot be reached right now, or live
control is not enabled on this deployment), timeout (the session did
not reply in time), or error (the session reported a failure; reason
says why) — so expected-inert states surface as data, not HTTP errors.
The network log answers only ok or unavailable. A malformed body or
query is a 400; an unknown or cross-account session id is a 404.
The cookie read and import, proxy change, history step, and both download
endpoints share a per-account limit of 16 requests in flight at once. Past
it, the response is status: "error" with a reason asking you to retry
shortly.
Page state
GET /v1/agent-sessions/{id}/page-state
This section is on its own page: Page state.
Network log
GET /v1/agent-sessions/{id}/network
This section is on its own page: Network log.
Request detail
GET /v1/agent-sessions/{id}/network/{entryId}
This section is on its own page: Request detail.
Read the cookie jar
GET /v1/agent-sessions/{id}/cookies
This section is on its own page: Read the cookie jar.
Import cookies
POST /v1/agent-sessions/{id}/cookies/set
This section is on its own page: Import cookies.
Read the profile’s notes
GET /v1/agent-sessions/{id}/profile-notes
This section is on its own page: Read the profile’s notes.
Replace the profile’s notes
PUT /v1/agent-sessions/{id}/profile-notes
This section is on its own page: Replace the profile’s notes.
Step browser history
POST /v1/agent-sessions/{id}/history
This section is on its own page: Step browser history.
Tabs
POST /v1/agent-sessions/{id}/tabs
This section is on its own page: Tabs.
Upload a file
POST /v1/agent-sessions/{id}/files
This section is on its own page: Upload a file.
List downloads
GET /v1/agent-sessions/{id}/downloads
This section is on its own page: List downloads.
Fetch a download
GET /v1/agent-sessions/{id}/downloads/content
This section is on its own page: Fetch a download.
Audit log
Creating and closing an agent session are recorded in the account’s
audit log (agent_session.created, agent_session.destroyed), which
you read in the Driftstack dashboard. A
reopened session is recorded as agent_session.created, with
resumed_from naming the ended session. Each
top-up is recorded as
agent_session.budget.added, with the tokens added and the new totals.
Errors
This section is on its own page: Errors.