Drıftstack docs
Docs

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.selector is the element the step acted on, exactly as it was sent. Use it to run the step again yourself with POST /steps, or to name the element in your next message.
  • results[i].intent.value is the text a type step 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 with sensitive: true and no value (the third step above), when the step is marked sensitive: 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 step sensitive: true yourself.
  • 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 a reason, and usually a diagnosis to branch on; a step held for your approval is kind: "confirmation_required" and did not run. Both are described under Message.
  • ok is false when 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 show ok: true with that failure still in results, so check each results[i].kind. ok: true does not by itself mean the task is finished: when it is not, the turn carries notice (a sentence) and notice_reason (one word to branch on), for example "notice_reason": "step_limit" — send “continue” to carry on.
  • intents is every step the turn attempted, including any it planned and never ran, so it can be longer than results. Read outcomes from results.

What stays the same between releases

  • intent — in intents and in each results[i].intent — is the replay contract. It has the AgentIntent shape of the OpenAPI document, its fields keep their meaning, and a step sent back to POST /steps exactly as it came is accepted and does the same thing. Build on it.
  • summary and reason on a step, and answer, answer_unavailable, notice and clarifying_question on 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 on kind, diagnosis.category, diagnosis.retryable, notice_reason, warning.kind and 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:

  1. Send the task as a message and check the turn: kind: "plan-executed", ok: true, and every results[i] a success.
  2. Take the steps from results[i].intent, in order (or intents, which also lists any step that was not run). A typed value that was stored as sensitive: true comes back without its value: put the value — or its {{credential:…}} placeholder — back in before you replay. So does the text typed into a prompt by a dialog step, which comes back as {{prompt text not stored}}.
  3. Send them to POST /v1/agent-sessions/{id}/steps on a session at the same starting page, at most eight per call. No model is called, and each step is done exactly as written.
  4. Read the new results the same way. A step that read the page (extract) returns its text in summary (or value / 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

See A shorter idle timeout.

Sizing the token budget

See 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

See 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)

See Streaming the turn (SSE).

Turn and session limits

See 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 reads archetype_source: "random"; a device that came from the profile is the profile’s;
  • mode, model, token budget (token_budget_total) and stop_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:

  • 409 with code: "session_not_ended" — the session is still running;
  • 410 (type session-destroyed, code: "reopen_window_passed", reopen_window_days: 7) — it ended more than 7 days ago; create a new session instead;
  • 409 with code: "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 (fields names which);
  • 404 — the id is not a session you can access (another account’s id gets the same 404 as 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 409 with code: "token_budget_cap_exceeded", token_budget_total (the total now) and max_add_tokens (the most you can still add); nothing is added.
  • Only a session that has not ended (status provisioning, active or paused) can be topped up. A session that has ended is 409 with code: "session_not_running" (session_status, and closed_reason when 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_exceeded and 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_total with a GET first).
  • A body with any other field, or an add_tokens that is not a whole number above 0 (or is over 10,000,000), is 400 and nothing is added.
  • 404 — the id is not a session you can access (another account’s id gets the same 404 as 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 the write scope.

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 usage and 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 notice says 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:

  1. 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.
  2. challenge_customer_handoff — the check needs a person. Solve it in the live view, resume, then send “continue”.
  3. 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”.
  4. 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.

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.