Drıftstack docs
Docs

Session lifecycle

A session is a browser session your code drives step by step through the sessions API. On hosted Driftstack only agent sessions load pages, so use agent sessions for real browsing. Every session occupies one of your account’s concurrent slots from creation until destruction; understanding the lifecycle is the difference between using your tier’s capacity well and burning slots on stuck sessions.

On hosted Driftstack

The step-by-step calls on this page (navigate, interact, wait, state, capture) run on a self-hosted deployment. On hosted Driftstack they do not run today:

  • A create (or a profile launch) needs proxy_id, the id of one of your saved proxies. You can leave it out when the profile is bound to a proxy (the profile_id of a create, or the profile you launch). The session then uses that proxy. With neither, the create is refused with 422 proxy-required, and nothing is created. If the profile has no proxy any more, the create is refused with 409 (code: "proxy_detached") until you send a proxy_id. That happens when its proxy was deleted or never shared with this workspace, or when the profile was marked proxy_choice: "detached".
  • With a proxy, the session starts, but navigate, interact, wait, extract, state and capture answer 503 driver-not-integrated and run nothing.

To drive pages on hosted, use an agent session: POST /v1/agent-sessions with the same proxy_id, then send your own steps with POST /v1/agent-sessions/{id}/steps, or a task in plain words with POST /v1/agent-sessions/{id}/message. The quickstart runs one end to end. More on this: Step-by-step operations on the hosted service.

States

The session.status field has five values: creating / ready / busy / destroyed / errored.

              create
                │
                ▼
            ┌──────────┐  (transient — the browser            ┌───────┐
            │ creating │───────────────────────────────────▶  │ ready │
            └──────────┘   is being started)                  └───────┘
                                                                  │  ▲
                                              navigate / interact │  │ call finishes
                                              / wait / capture    │  │
                                                                  ▼  │
                                                              ┌──────┐
                                                              │ busy │
                                                              └──────┘
                                                                  │
                                                                  │ destroy
                                                                  ▼
                                                            ┌───────────┐
                                                            │ destroyed │
                                                            └───────────┘
                                                            (or `errored` if the browser fails)

sessions.create() returns only once the session is ready, although a list or read made at the same time can still show it as creating. Each driving call (navigate, interact, wait, capture) moves the session from ready to busy; while it is creating or busy, any other call returns 409 Conflict. When the call succeeds the session goes back to ready. If the browser fails, the session becomes errored; if you destroy the session mid-call, it becomes destroyed and the interrupted call returns 410 Gone.

Concurrency

Each plan has a hard cap on simultaneously-active sessions. Exceeding the cap returns 429 Too Many Requests on sessions.create(), with current_sessions and limit in the problem body. Unlike rate-limit 429s there is no Retry-After header — capacity frees when one of your sessions ends, so destroy one you created (or wait for your own workflow to finish) and retry.

Plan Tier id Concurrent sessions
Trial trial 1
Starter starter_v3 1
Pro pro_v3 3
Team team_v3 6
Scale scale_v3 12
Enterprise enterprise 20+

Enterprise caps are set by contract.

The earlier plans, no longer offered, keep their caps for the accounts that hold them:

Plan Tier id Concurrent sessions
Personal solo_manual 1
Team team_manual 3
Agency agency_manual 8
API Starter api_starter 2
API Builder api_builder 8
API Scale api_scale 24

Concurrent sessions are the only thing a paid plan meters: there is no monthly hours allowance and no overage charge. That is about total usage; a single session still ends on its own (see How long a session runs on a paid plan below). The free trial has a session limit and a total: a trial session is closed after 20 minutes, and the trial includes 2 hours of session time in total, counted from the day it starts across every session the account runs. Once the 2 hours are used, creating a session is refused with 429 tier-limit (code: "trial_session_time_used").

Pricing source of truth: driftstack.io/pricing.

Create

const session = await client.sessions.create({
  label: 'checkout flow',
  // proxy_id: one of your saved proxies (required on hosted Driftstack)
  // archetype: optional; without it, a random current iPhone your plan includes
  // metadata: optional Record<string, unknown> for your own tracking
});
console.log(session.id, session.created_at);

Returns a Session with id, archetype, status, label, metadata, created_at. The id is the handle for every subsequent call.

Tier check: if you’re at your concurrent cap, the call returns 429 concurrency-limit. If your tier’s profile cap is reached on a profile-binding flow, or the free trial’s 2 hours of session time are used up (code: "trial_session_time_used"), 429 tier-limit. If your account is suspended, 403 forbidden.

Drive: navigate, interact, wait

A session is driven through three primary methods plus state introspection. On hosted Driftstack these answer 503 driver-not-integrated (see above); use an agent session’s steps there.

POST /v1/sessions/:id/navigate — go to a URL.

const result = await client.sessions.navigate(session.id, {
  url: 'https://example.com/checkout',
  wait_until: 'load', // the default; or 'domcontentloaded', 'networkidle'
  timeout_ms: 30_000,
});
console.log(result.final_url, result.status, result.duration_ms);

wait_until controls when the call returns. load returns on the load event; domcontentloaded is faster but earlier; networkidle is accepted and currently waits for the load event, the same as load. For a single-page app that renders its content after the page loads, follow navigate with a wait for the element you need.

POST /v1/sessions/:id/interact — tap, scroll, type, or press keys on the page.

POST /v1/sessions/:id/wait — block until a selector appears, a URL pattern is reached, or a timeout elapses.

GET /v1/sessions/:id/state — live page introspection: current url, title, cookies + local_storage, and a captured_at timestamp. This call runs in the live browser session and marks it busy while it captures, so poll it sparingly and only while the session is ready (a call made while the session is busy returns 409); to check creating / busy without tying up the session, use GET /v1/sessions/:id or the list endpoint instead. When acting as a team owner, state requires the admin role because it returns browser secrets such as cookies and local storage; a member can still read list/detail metadata but gets 403 for state, and the session is left untouched. Self-account read:sessions access is unchanged.

Capture

POST /v1/sessions/:id/capture returns a screenshot, DOM snapshot, or PDF.

const shot = await client.sessions.capture(session.id, { kind: 'screenshot' });
// shot.kind, shot.data, shot.encoding, shot.byte_size, shot.duration_ms

The response carries the capture inline — data is the content itself (base64-encoded for screenshots and PDFs) — and nothing is stored server-side. Persist the bytes yourself if you need them long-term.

Destroy

await client.sessions.destroy(session.id);

destroy is idempotent — calling it twice on the same id is a no-op the second time. It releases the concurrent slot immediately. If the session was bound to a profile, the profile’s storage state is captured and saved on a clean destroy.

Always destroy. A forgotten session holds its concurrent slot until it ends, and that can take hours. A try / finally around your session work is the safe pattern:

const session = await client.sessions.create({
  proxy_id: 'a1b2c3d4-...', // your saved proxy's id; required on hosted (422 without it)
});
try {
  await client.sessions.navigate(session.id, { url: 'https://example.com' });
  // … your logic
} finally {
  await client.sessions.destroy(session.id);
}

Python and Go SDK examples follow the same pattern (with block in Python sync; defer in Go).

How long a session runs on a paid plan

A paid plan has no hour limit, but a session does not run for ever. A session that no AI agent drives (one started with POST /v1/sessions through a proxy, a profile launch, or a manual session in the desktop app) runs for up to 4 hours (closed_reason: "max_duration") and is ended sooner after 30 minutes with no input. A session the AI agent drives is not sent the 4-hour limit; it is ended after about 5 minutes with nothing running and no input. Any session still open about 12 hours after it was created is closed, however busy it is (orphaned-lifetime), and a session also ends when the machine running it loses it. Until a session ends, it holds its concurrent slot, which is why the try / finally pattern above matters. Its slot is free as soon as it has ended, with no DELETE needed. The free trial is the exception: a trial session is closed after 20 minutes (closed_reason: "trial_session_limit"), or sooner if the trial’s 2 hours of session time run out during it (closed_reason: "trial_time_used"). Each trial session gets 20 minutes or 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 still running within about two minutes of the mark.

When a free trial ends

Once a free trial has ended, creating a session is refused with 402 until the account chooses a plan. Every other call keeps working. We keep your profiles, proxies and settings for 30 days after your trial ends. The 402 problem body’s type ends in trial-ended; when the trial ends, and whether it has, is shown in the dashboard. A paid plan that has ended is refused the same way, with code: "plan_ended" and the detail “Your plan has ended…”; it keeps its profiles, proxies and settings. After a failed renewal payment, a session start is refused the same way at once (there is no grace period), with code: "payment_failed" and the detail “Your last payment didn’t go through…”; sessions already running are closed within about 2 minutes. While the card is still being retried, everything works again as soon as the payment goes through. Once the last retry fails, the plan has ended: a session start then gets code: "plan_ended", and the account chooses a plan again.

Error shapes

Every error returned by the session endpoints conforms to the problem+json shape with a type URL identifying the error class:

  • 429 Too Many Requests (https://errors.driftstack.dev/rate-limited) — global / per-bucket rate limit exceeded. Retry-After carries the wait time.
  • 429 Too Many Requests (https://errors.driftstack.dev/concurrency-limit) — concurrent-session cap reached. Wait for an active session to finish.
  • 429 Too Many Requests (https://errors.driftstack.dev/tier-limit) — a tier-derived cap (e.g. profile count) is reached, or the free trial’s 2 hours of session time are used up (code: "trial_session_time_used").
  • 404 Not Found — session ID doesn’t exist (or was destroyed and has since been cleaned up).
  • 409 Conflict — the session is still creating, or another operation is already running (busy); retry once it reports ready.
  • 410 Gone (https://errors.driftstack.dev/session-destroyed) — the session is destroyed or errored, or this operation lost a race to destroy; create a fresh session.
  • 502 Bad Gateway / 503 Service Unavailable — the browser session hit an error, or the requested feature is not available (driver-error / driver-not-integrated / feature-unavailable).

The SDKs map these to typed error classes — catch RateLimitError, ConcurrencyLimitError, the tier-limit class (TierLimitError in TypeScript, QuotaExceededError in Python and Go), SessionDestroyedError, DriverError, etc. The full mapping lives at /reference/errors.

Session events on the webhook bus

If you’ve configured a webhook endpoint, terminal session events fire on the bus. session.completed and session.failed come only from the direct sessions this page describes; an agent session sends no session.completed or session.failed (read its status and closed_reason instead), and of the events below it sends only session.challenge_detected and session.profile_save_failed.

  • session.completed — one per logical destroy of a non-terminal session: a customer-driven destroy, the session time limit of the free trial, a session that ended on its own where it ran, an account suspension reclaiming its live sessions, or a paid plan that lapsed (a renewal payment that did not go through, reason_code: "payment_failed", or a plan that ended, reason_code: "plan_ended"). All but the first also send auto_destroyed: true and a reason; branch on auto_destroyed if you attribute completions. A close at the plan’s session limit may arrive with either of two reasons (auto-destroyed: free-tier session duration cap or the session ended where it ran), and the session ended where it ran also covers every other way a session started with POST /v1/sessions or a profile launch ends where it ran: 30 minutes without activity, its 4-hour limit, the trial’s used-up session time, the close about 12 hours after create and the machine stopping it. So do not read the cause from the reason text.
  • session.failed — the session ended because of an unrecoverable error (for example, a timeout or a crash during a page action). Create a new session to continue.
  • session.egress_capability_changed — the session reported its proxy capabilities. It fires on every report, not only when the state changed: there is no change detection, so identical consecutive reports each emit an event with a fresh event_id. Treat it as “here is the current capability state” and compare against what you last stored, rather than as a transition signal. Note also that warnings carries video-stream faults (streaming_blank, streaming_failed) alongside proxy ones like dead_proxy, so this event can fire when only the video stream degraded.
  • session.challenge_detected — the session detected a bot-check (DataDome / Arkose / PerimeterX / AWS-WAF / GeeTest / …). The session auto-pauses; resolve the challenge (e.g. in the live view) and it resumes.
  • session.profile_save_failed — a profile-backed session did not replace the stored profile at teardown. Failure reasons are terminal and the next restore will be stale; superseded is harmless and means a newer save of the same profile landed first, and profile_not_loaded means the session started without its saved profile, so its changes were not saved over it and the saved profile is kept as it was.

Intermediate state transitions (e.g. a hypothetical session.created) are not on the bus today. Read the session resource or list endpoint for persisted lifecycle status; do not poll sessions.getState while the session is busy with another operation. See the webhook events catalog for full payload shapes and signature verification.

Notes

  • A destroyed session requires a fresh sessions.create(); sessions are not resumable after destroy. Plan your workflow to recreate cleanly when a long pause is expected.
  • A session left in busy after a server crash is not reset automatically: the operation that was running may already have changed the page, and repeating it could duplicate that work. Destroy the session and create a fresh one.
  • There are no per-session bandwidth or memory limits for you to manage today. The limits that govern sessions are your tier’s concurrent-session cap and the session time limits above (rate limits and profile caps are separate — see Error shapes above).

Next steps