API reference
Core routes, exact shapes.
The complete Driftstack API is documented in a standard machine-readable format (an OpenAPI 3.1 spec), generated from the same validation rules (Zod schemas) the server enforces at runtime. There is no second source of truth for public request and response shapes. This page is a curated map of common resources and runnable request patterns; the interactive reference carries the complete operation and schema catalog.
Interactive reference uses Scalar — try requests against your API key directly in the browser.
The complete living reference is docs.driftstack.dev — this page is a curated snapshot.
Surface map
Common routes, grouped.
Sessions
- POST /v1/sessions
- GET /v1/sessions
- GET /v1/sessions/:id
- POST /v1/sessions/:id/navigate
- POST /v1/sessions/:id/interact
- POST /v1/sessions/:id/wait
- GET /v1/sessions/:id/state
- POST /v1/sessions/:id/capture
- DELETE /v1/sessions/:id
Archetypes
- GET /v1/archetypes
Read the public device, iOS, and Safari catalog without an API key. The catalog and create-payload generator reference shows how to resolve capabilities to a current archetype id.
Agent sessions
- POST /v1/agent-sessions
- GET /v1/agent-sessions
- GET /v1/agent-sessions/:id
- POST /v1/agent-sessions/:id/message
- POST /v1/agent-sessions/:id/mode
- POST /v1/agent-sessions/:id/input-event
- POST /v1/agent-sessions/:id/takeover
- POST /v1/agent-sessions/:id/handback
- POST /v1/agent-sessions/:id/livekit-token
- GET /v1/agent-sessions/:id/transcript
- GET /v1/agent-sessions/:id/gui-control-key
- DELETE /v1/agent-sessions/:id
Recipes
- GET /v1/agent-sessions/:id/recipe-suggestion
- POST /v1/recipes
- GET /v1/recipes
- GET /v1/recipes/:id
- DELETE /v1/recipes/:id
Prefill recipe metadata from an agent session, then create, browse, inspect, or delete saved recipes.
Profiles
- POST /v1/profiles
- GET /v1/profiles
- GET /v1/profiles/:id
- PATCH /v1/profiles/:id
- DELETE /v1/profiles/:id
API keys
- POST /v1/api-keys
- GET /v1/api-keys
- POST /v1/api-keys/:id/rotate
- DELETE /v1/api-keys/:id
Customer API keys, OAuth applications, and SDK automation require a paid tier. Free is supported through the desktop app's browser-authorized restricted device credential; it is not a customer key to paste into API samples.
Webhooks
- POST /v1/webhooks
- GET /v1/webhooks
- GET /v1/webhooks/:id
- PATCH /v1/webhooks/:id
- DELETE /v1/webhooks/:id
- POST /v1/webhooks/:id/rotate-secret
- POST /v1/webhooks/:id/test
- GET /v1/webhooks/:id/deliveries
- POST /v1/webhook-deliveries/:id/replay
Account
- GET /v1/account/me
- GET /v1/account/audit-log
- GET /v1/account/audit-log/export
- GET /v1/account/email-preferences
- PUT /v1/account/email-preferences
- GET /v1/account/rate-limits
- GET /v1/account/me/byok-anthropic-key
- PUT /v1/account/me/byok-anthropic-key
- DELETE /v1/account/me/byok-anthropic-key
- POST /v1/account/me/byok-anthropic-key/test
- GET /v1/account/me/bundled-llm-settings
- PATCH /v1/account/me/bundled-llm-settings
- GET /v1/account/me/bundled-llm-status
Team
- POST /v1/team/invites
- GET /v1/team/invites
- POST /v1/team/invites/accept
- GET /v1/team/members
- DELETE /v1/team/members/:id
Billing — crypto orders
- POST /v1/billing/crypto-checkout
- POST /v1/billing/crypto-checkout/quote
- GET /v1/billing/crypto-orders
- GET /v1/billing/crypto-orders/:id
- PATCH /v1/billing/crypto-orders/:id
- POST /v1/billing/crypto-orders/:id/cancel
- GET /v1/billing/crypto-orders/:id/receipt
- GET /v1/billing/crypto-orders/:id/receipt.txt
- GET /v1/billing/crypto-orders/:id/receipt.pdf
Status
- GET /v1/status
- GET /v1/status/stream
- GET /v1/status/sla
- POST /v1/status/subscribe
- GET /v1/status/subscribe/confirm
- GET /v1/status/subscribe/unsubscribe
Auth flows
- POST /v1/auth/signup
- POST /v1/auth/login
- POST /v1/auth/logout
- POST /v1/auth/verify-email
- POST /v1/auth/magic-link/request
- POST /v1/auth/magic-link/consume
- POST /v1/auth/password-reset/request
- POST /v1/auth/password-reset/confirm
- POST /v1/auth/refresh
Billing
- POST /v1/billing/checkout-session
- POST /v1/billing/portal-session
- GET /v1/billing
Common patterns
Three flows, four languages.
Most integrations are built on the same three operations: spin up a session, drive it, capture artifacts. Below: each one in cURL, TypeScript, Python, and Go. Each block declares the environment values and imports it uses.
These API-key and SDK examples require a paid tier. On Free, sign in from the desktop app instead; browser authorization stores its restricted device credential automatically.
1. Create a session
The minimal "hello world" — provision an iPhone 17 Safari session, return its id. Default archetype if you don't pass one.
# cURL
: "${DRIFTSTACK_API_KEY:?Set DRIFTSTACK_API_KEY}"
curl --fail-with-body -X POST https://api.driftstack.dev/v1/sessions \
-H "authorization: Bearer $DRIFTSTACK_API_KEY" \
-H "content-type: application/json" \
-d '{"archetype":"iphone17_ios18_7_safari26_4"}'
// TypeScript
import { Driftstack } from "@driftstack/sdk";
const apiKey = process.env.DRIFTSTACK_API_KEY;
if (!apiKey) throw new Error("DRIFTSTACK_API_KEY is required");
const client = new Driftstack({ apiKey });
const session = await client.sessions.create({
archetype: "iphone17_ios18_7_safari26_4",
});
console.log(session.id);
# Python
import os
from driftstack import Driftstack
client = Driftstack(api_key=os.environ["DRIFTSTACK_API_KEY"])
session = client.sessions.create({"archetype": "iphone17_ios18_7_safari26_4"})
print(session.id)
// Go
package main
import (
"context"
"fmt"
"log"
"os"
driftstack "github.com/driftstackdev/driftstack-api/packages/sdk-go"
)
func main() {
apiKey := os.Getenv("DRIFTSTACK_API_KEY")
if apiKey == "" { log.Fatal("DRIFTSTACK_API_KEY is required") }
client := driftstack.New(apiKey)
defer client.Close()
session, err := client.Sessions.Create(context.Background(), &driftstack.CreateSessionRequest{
Archetype: "iphone17_ios18_7_safari26_4",
})
if err != nil { log.Fatal(err) }
fmt.Println(session.ID)
}
2. Drive the session
Navigate, tap, wait. interact
handles taps, typing, scrolling, and key presses;
wait
blocks until a DOM condition is met or a timeout fires.
# cURL
: "${DRIFTSTACK_API_KEY:?Set DRIFTSTACK_API_KEY}"
: "${DRIFTSTACK_SESSION_ID:?Set DRIFTSTACK_SESSION_ID}"
SESSION_URL="https://api.driftstack.dev/v1/sessions/$DRIFTSTACK_SESSION_ID"
AUTH="authorization: Bearer $DRIFTSTACK_API_KEY"
curl --fail-with-body -X POST "$SESSION_URL/navigate" \
-H "$AUTH" -H "content-type: application/json" \
-d '{"url":"https://example.com"}'
curl --fail-with-body -X POST "$SESSION_URL/interact" \
-H "$AUTH" -H "content-type: application/json" \
-d '{"action":{"kind":"tap","selector":"button.cta"}}'
curl --fail-with-body -X POST "$SESSION_URL/wait" \
-H "$AUTH" -H "content-type: application/json" \
-d '{"condition":{"kind":"selector","selector":"main"},"timeout_ms":5000}'
// TypeScript
import { Driftstack } from "@driftstack/sdk";
const apiKey = process.env.DRIFTSTACK_API_KEY;
const sessionId = process.env.DRIFTSTACK_SESSION_ID;
if (!apiKey || !sessionId) throw new Error("Set DRIFTSTACK_API_KEY and DRIFTSTACK_SESSION_ID");
const client = new Driftstack({ apiKey });
await client.sessions.navigate(sessionId, { url: "https://example.com" });
await client.sessions.interact(sessionId, {
action: { kind: "tap", selector: "button.cta" },
});
await client.sessions.wait(sessionId, {
condition: { kind: "selector", selector: "main" },
timeout_ms: 5000,
});
# Python
import os
from driftstack import Driftstack
client = Driftstack(api_key=os.environ["DRIFTSTACK_API_KEY"])
session_id = os.environ["DRIFTSTACK_SESSION_ID"]
client.sessions.navigate(session_id, {"url": "https://example.com"})
client.sessions.interact(session_id, {
"action": {"kind": "tap", "selector": "button.cta"},
})
client.sessions.wait(session_id, {
"condition": {"kind": "selector", "selector": "main"},
"timeout_ms": 5000,
})
// Go
package main
import (
"context"
"log"
"os"
driftstack "github.com/driftstackdev/driftstack-api/packages/sdk-go"
)
func main() {
apiKey, sessionID := os.Getenv("DRIFTSTACK_API_KEY"), os.Getenv("DRIFTSTACK_SESSION_ID")
if apiKey == "" || sessionID == "" { log.Fatal("set DRIFTSTACK_API_KEY and DRIFTSTACK_SESSION_ID") }
client := driftstack.New(apiKey)
defer client.Close()
ctx := context.Background()
if _, err := client.Sessions.Navigate(ctx, sessionID, &driftstack.NavigateRequest{URL: "https://example.com"}); err != nil { log.Fatal(err) }
if _, err := client.Sessions.Interact(ctx, sessionID, &driftstack.InteractRequest{Action: driftstack.NewTapAction("button.cta")}); err != nil { log.Fatal(err) }
if _, err := client.Sessions.Wait(ctx, sessionID, &driftstack.WaitRequest{Condition: driftstack.NewSelectorCondition("main"), TimeoutMS: 5000}); err != nil { log.Fatal(err) }
}
3. Capture a screenshot
capture takes one of three kinds:
screenshot,
dom_snapshot, or
pdf. The response carries the
file's contents directly as base64-encoded text (the
data field) — nothing is stored
server-side.
# cURL
: "${DRIFTSTACK_API_KEY:?Set DRIFTSTACK_API_KEY}"
: "${DRIFTSTACK_SESSION_ID:?Set DRIFTSTACK_SESSION_ID}"
curl --fail-with-body -X POST "https://api.driftstack.dev/v1/sessions/$DRIFTSTACK_SESSION_ID/capture" \
-H "authorization: Bearer $DRIFTSTACK_API_KEY" \
-H "content-type: application/json" \
-d '{"kind":"screenshot"}' \
| jq -r .data | openssl base64 -d -A > out.png
// TypeScript
import { writeFileSync } from "node:fs";
import { Driftstack } from "@driftstack/sdk";
const apiKey = process.env.DRIFTSTACK_API_KEY;
const sessionId = process.env.DRIFTSTACK_SESSION_ID;
if (!apiKey || !sessionId) throw new Error("Set DRIFTSTACK_API_KEY and DRIFTSTACK_SESSION_ID");
const client = new Driftstack({ apiKey });
const shot = await client.sessions.capture(sessionId, {
kind: "screenshot",
});
writeFileSync("out.png", Buffer.from(shot.data, "base64"));
# Python
import base64
import os
from pathlib import Path
from driftstack import Driftstack
client = Driftstack(api_key=os.environ["DRIFTSTACK_API_KEY"])
shot = client.sessions.capture(os.environ["DRIFTSTACK_SESSION_ID"], {"kind": "screenshot"})
Path("out.png").write_bytes(base64.b64decode(shot.data))
// Go
package main
import (
"context"
"encoding/base64"
"log"
"os"
driftstack "github.com/driftstackdev/driftstack-api/packages/sdk-go"
)
func main() {
apiKey, sessionID := os.Getenv("DRIFTSTACK_API_KEY"), os.Getenv("DRIFTSTACK_SESSION_ID")
if apiKey == "" || sessionID == "" { log.Fatal("set DRIFTSTACK_API_KEY and DRIFTSTACK_SESSION_ID") }
client := driftstack.New(apiKey)
defer client.Close()
shot, err := client.Sessions.Capture(context.Background(), sessionID, &driftstack.CaptureRequest{Kind: driftstack.CaptureScreenshot})
if err != nil { log.Fatal(err) }
data, err := base64.StdEncoding.DecodeString(shot.Data)
if err != nil { log.Fatal(err) }
if err := os.WriteFile("out.png", data, 0o644); err != nil { log.Fatal(err) }
}
Error reference
What can go wrong, and what to do about it.
Every error follows the web standard for machine-readable
errors — RFC 9457
application/problem+json — and
carries a stable type URI: a
web link that identifies, and explains, the error kind. The
SDKs turn these into named error classes; the cURL caller
gets the same JSON straight.
| Status | Type URI | When | SDK class |
|---|---|---|---|
| 400 | errors.driftstack.dev/validation-failed | Zod schema mismatch on request body. | ValidationError |
| 401 | errors.driftstack.dev/unauthorized | API key missing, malformed, or revoked. | AuthError |
| 404 | errors.driftstack.dev/not-found | Resource id doesn't exist or isn't visible to this key. | NotFoundError |
| 409 | errors.driftstack.dev/conflict | Resource state precludes the operation (e.g. subscription already active). | ConflictError |
| 410 | errors.driftstack.dev/session-destroyed | Session was destroyed; create a new one. | SessionDestroyedError |
| 429 | errors.driftstack.dev/tier-limit | Account hit a tier cap (profile count, api-keys per account). | TierLimitError |
| 429 | errors.driftstack.dev/rate-limited | Per-account rate limit; retry-after header carries seconds. | RateLimitError (retryable) |
| 429 | errors.driftstack.dev/concurrency-limit | Your plan's limit on sessions running at the same time (the tier-bound concurrent-session cap) — end a session or wait for one to finish. | ConcurrencyLimitError |
| 500 | errors.driftstack.dev/internal | Server-side fault. Logged + alerted. | InternalError (retryable) |
| 503 | errors.driftstack.dev/feature-unavailable | Feature disabled at deploy time (e.g. avatar upload without R2 bucket). | FeatureUnavailableError (NOT retryable) |
SDK consumers can use isRetryable(err)
(TypeScript) / equivalent predicates in Python + Go to filter
which errors to retry without re-implementing the mapping.
Spec posture
Stable contract. Versioned. Auto-generated.
- → Public request and response shapes are defined with Zod schemas. The OpenAPI 3.1 spec is generated from those schemas — there is no second source of truth.
- → Every error case maps to the web standard for machine-readable errors — an RFC 9457
application/problem+jsonresponse with a stabletypeURI (a link that explains the error). - → Breaking changes ship under a new path version.
/v1stays stable;/v2would be a new prefix, not a silent shape change.