D driftstack docs

Session lifecycle

A session is one running iPhone Safari instance on the modified WebKit fork. 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.

States

              create


            ┌───────┐    navigate / interact / wait      ┌────────┐
            │ ready │───────────────────────────────────▶│ active │
            └───────┘                                    └────────┘

                                                              │ destroy
                                                              │ or idle ≥ idle_timeout

                                                        ┌──────────┐
                                                        │ destroyed│
                                                        └──────────┘

In practice you don’t observe ready separately — the SDK’s sessions.create() returns once the session is active and ready for the first method call.

Concurrency

Each tier has a hard cap on simultaneously-active sessions. Exceeding the cap returns 429 Too Many Requests on sessions.create(), with a Retry-After header indicating when capacity will free up (worst case = soonest tracked session’s idle-timeout boundary).

TierConcurrent sessions
Trial pack1
Solo Manual1
Team Manual3
Agency Manual8
API Starter2
API Builder8
API Scale24
EnterpriseCustom

Concurrent caps are the only metering on paid tiers — there are no hour caps and no overage charges. Run sessions for as long as your workflow needs within your concurrent cap.

Pricing source of truth: driftstack.dev/pricing.

Create

const session = await client.sessions.create({
  label: 'checkout flow',
  // archetype: optional override of the locked default
  // metadata: optional Record<string, unknown> for your own tracking
});
console.log(session.id, session.created_at);

Returns a Session with id, archetype, state, 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. If your tier’s profile cap blocks profile binding, 402 profile_cap_reached. If your account is suspended, 403 forbidden.

Drive: navigate, interact, wait

A session is driven through three primary methods plus state introspection.

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

const result = await client.sessions.navigate(session.id, {
  url: 'https://example.com/checkout',
  wait_until: 'networkidle', // or 'load' (default), 'domcontentloaded'
  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 waits until network is quiet for a brief window — best for SPAs that load content after the initial render.

POST /v1/sessions/:id/interact — synthesise touch / scroll / type input on the iPhone Safari runtime. Subject to the realistic-input behavioural-simulation layer that ships with every session.

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

GET /v1/sessions/:id/state — read-only introspection: current url, title, ready_state, viewport. Cheap; safe to poll at low frequency.

Capture

POST /v1/sessions/:id/capture returns a screenshot or full-page render.

const shot = await client.sessions.capture(session.id, { kind: 'screenshot' });
// shot.kind, shot.format, shot.bytes_url, shot.captured_at

Captures are stored on the EU-resident object-storage sub-processor (Cloudflare R2) and the response includes a signed URL that’s valid for a bounded window (~15 minutes). Persist the bytes 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. Forgotten sessions burn concurrent slots until their idle timeout fires. A try / finally around your session work is the safe pattern:

const session = await client.sessions.create();
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).

Idle timeout

If a session sees no API call for the per-tier idle window, the runtime auto-destroys it and emits a session.destroyed webhook with reason: "idle_timeout". This protects against stuck workflows burning your concurrent capacity indefinitely.

Default idle window: 10 minutes. Higher tiers may extend (configured per-account by the control plane).

To keep a session alive during a slow workflow, periodically call any method — sessions.getState() is the cheapest heartbeat.

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 — concurrent cap reached. Retry-After header tells you when slots free up.
  • 402 Payment Required — trial-pack credit exhausted (trial_pack_exhausted) or profile cap reached (profile_cap_reached).
  • 404 Not Found — session ID doesn’t exist (or already destroyed and TTL-evicted).
  • 409 Conflict — operation invalid for the current state (e.g. navigate after destroy).
  • 503 Service Unavailable — fleet at capacity beyond your account’s cap (rare; surfaces during regional fleet incidents).

The SDKs map these to typed error classes — catch RateLimitError, ConcurrencyLimitError, PaymentRequiredError, etc.

Session events on the webhook bus

If you’ve configured a webhook endpoint, every state transition fires an event:

  • session.created
  • session.destroyed (with reason: customer_destroyed, idle_timeout, fleet_evicted)
  • session.error (runtime crash; auto-destroyed)

See the webhook events catalog for full payload shapes and signature verification.

Notes

  • A session destroyed by idle-timeout requires a fresh sessions.create(); sessions are not resumable after destroy. Plan your workflow to either keep a session alive with periodic activity or to recreate cleanly when a long pause is expected.
  • Session-level resource quotas (per-session bandwidth, memory) are not customer-facing today. Fleet-level enforcement runs internally; tier concurrent caps are the only customer-visible meter.

Next steps