Quickstart (curl)
This page runs your first Driftstack session using only curl — no
SDK, no install step. The core create, drive, capture, and destroy
lifecycle uses plain HTTPS calls, so this is the fastest way to see
that wire contract directly. Live video and event streams use their
documented streaming transports. If you’d rather start in TypeScript,
Python, or Go, use the SDK quickstart instead.
You will need a Driftstack account on any paid tier (including a Manual tier)
(sign up or
sign in) and curl.
Free is an interactive desktop tier, so this HTTP quickstart is paid-only. The desktop’s browser sign-in automatically stores a restricted
ds_test_…device credential; it is not a general sandbox/customer key and should not be copied into curl or an SDK.
1. Get an API key
- Open app.driftstack.dev/api-keys.
- Click Create key, give it a name, and copy the value. The full key is shown once — Driftstack stores only a hash, so if you lose it you revoke it and mint a new one.
- Export it in your shell:
export DRIFTSTACK_API_KEY="ds_live_…"
Customer keys on every paid tier, including Manual, start with ds_live_ and
authenticate through an Authorization: Bearer header on every call. Free
does not create or rotate customer keys. Its browser-authorized desktop
credential starts with ds_test_ and is restricted to the supported desktop
route surface.
Pick the narrowest scopes that fit the job — a scope is a permission
attached to the key. This page uses read + write: account/state
requests are reads, while create/navigate/capture/destroy drive the
session. Keep account_owner (full account control) for dashboards,
not runtime automation. Full list: API key scopes.
2. Check the key works
curl -H "Authorization: Bearer $DRIFTSTACK_API_KEY" \
https://api.driftstack.dev/v1/account/me
{
"id": "acc_…",
"email": "[email protected]",
"name": "Your Name",
"tier": "api_starter",
"status": "active",
"timezone": "Europe/Amsterdam",
"mfa_enrolled": false,
"concurrent_session_cap": 2,
"concurrent_session_active": 0,
"profile_cap": 25,
"profile_count": 0,
"teams": []
}
A 200 with a flat account object (no wrapper envelope) means the
key is good. A 401 means the key is wrong, malformed, or revoked —
check the dashboard. A 403 after a downgrade to Free means customer API
access is paused; its RFC 9457 detail tells you to upgrade to a tier that
includes the apiAccess feature. Upgrade to resume an unexpired, unrevoked
key. concurrent_session_cap /
concurrent_session_active are worth noting now: they’re the
concurrency budget the next step draws from.
3. Create a session
curl -X POST \
-H "Authorization: Bearer $DRIFTSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "label": "first-session" }' \
https://api.driftstack.dev/v1/sessions
{
"id": "ses_…",
"account_id": "acc_…",
"api_key_id": "key_…",
"status": "ready",
"archetype": "iphone17_ios18_7_safari26_4",
"purpose": "production_customer",
"label": "first-session",
"metadata": null,
"egress_capabilities": null,
"egress_capability_report": null,
"created_at": "2026-07-07T12:00:00.000Z",
"updated_at": "2026-07-07T12:00:01.000Z",
"last_state_at": null,
"destroyed_at": null
}
The 201 response is the session record, already ready — the
create call holds until the phone-browser runtime is allocated and
responding, so there’s nothing to poll before you drive it. All body
fields are optional: omitting archetype (the device + OS + browser
identity the session presents) gives you the locked default shown
above. Sessions can also start from a saved
profile via profile_id.
Every session occupies one concurrent slot until you destroy it.
Hitting your tier’s cap returns a 429 — see
Concurrency & backpressure.
4. Drive it
Point the session at a URL:
curl -X POST \
-H "Authorization: Bearer $DRIFTSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com" }' \
https://api.driftstack.dev/v1/sessions/ses_…/navigate
{
"url": "https://example.com",
"final_url": "https://example.com/",
"status": 200,
"duration_ms": 1412
}
Then read the live page state — URL, title, cookies, storage, and a
page_state block that tells you whether the page loaded or errored:
curl -H "Authorization: Bearer $DRIFTSTACK_API_KEY" \
https://api.driftstack.dev/v1/sessions/ses_…/state
{
"url": "https://example.com/",
"title": "Example Domain",
"cookies": [],
"local_storage": {},
"page_state": { "state": "loaded" },
"captured_at": "2026-07-07T12:00:04.000Z"
}
The full action surface — interact (tap / type / scroll / press),
wait, extract, search, login — is on the
Sessions reference.
5. Capture a screenshot
curl -X POST \
-H "Authorization: Bearer $DRIFTSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "kind": "screenshot" }' \
https://api.driftstack.dev/v1/sessions/ses_…/capture
{
"kind": "screenshot",
"data": "iVBORw0KGgo…",
"encoding": "base64",
"byte_size": 184320,
"duration_ms": 412
}
Captures return the bytes inline — there is no download URL to fetch.
For a screenshot, data is the PNG base64-encoded; decode it before
saving:
curl -s -X POST \
-H "Authorization: Bearer $DRIFTSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "kind": "screenshot" }' \
https://api.driftstack.dev/v1/sessions/ses_…/capture \
| python3 -c 'import sys, json, base64; open("shot.png","wb").write(base64.b64decode(json.load(sys.stdin)["data"]))'
kind can also be dom_snapshot (the serialised page HTML, returned
as plain text with encoding: "utf8") or pdf. Screenshots cap at
4 MiB, PDFs at 8 MiB.
6. Destroy the session
curl -X DELETE \
-H "Authorization: Bearer $DRIFTSTACK_API_KEY" \
https://api.driftstack.dev/v1/sessions/ses_…
Returns 204 No Content, frees the concurrent slot immediately, and
is idempotent — destroying an already-destroyed session is a no-op.
Always destroy when you’re done: a forgotten session keeps holding a
slot (on the free tier it’s auto-destroyed after the 20-minute
session cap; paid tiers have no time cap, so the slot stays held
until you release it).
Two habits worth starting now
- Know which POSTs are safe to retry.
Idempotency-Keyis honoured on four endpoints — agent-session creation, agent-session messages, and the two billing checkouts — where a retry replays the original response. It is not honoured onPOST /v1/sessions: retrying that after a dropped connection mints a second session and takes another concurrent slot. See Idempotency keys for the exact list. - Prefer webhooks to polling.
POST /v1/webhookssubscribes an HTTPS endpoint of yours to events likesession.completedandsession.failed. See Webhook endpoints.
Next steps
- SDK quickstart — the same flow in TypeScript, Python, or Go.
- Sessions reference — every session endpoint and field.
- Concurrency & backpressure — caps per tier and how to back off.
- Errors — the stable problem types every endpoint returns.
- Rate limits — request-rate buckets, separate from concurrency.
Stuck? Email [email protected]
with your account ID (acc_…) and the x-request-id header from any
error response.