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 rules the server itself enforces. 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.
Try requests with your own API key directly in the browser.
The complete living reference is docs.driftstack.io — 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 pick a device, iOS and Safari combination from the
catalog and get the archetype id to pass to
POST /v1/sessions or POST /v1/profiles.
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/steps
- POST /v1/agent-sessions/:id/stop
- POST /v1/agent-sessions/:id/resume
- GET /v1/agent-sessions/:id/transcript
- POST /v1/agent-sessions/:id/livekit-token
- GET /v1/agent-sessions/:id/captures/:captureId
- GET /v1/agent-sessions/:id/page-state
- GET /v1/agent-sessions/:id/network
- GET /v1/agent-sessions/:id/cookies
- POST /v1/agent-sessions/:id/cookies/set
- POST /v1/agent-sessions/:id/history
- POST /v1/agent-sessions/:id/files
- GET /v1/agent-sessions/:id/downloads
- GET /v1/agent-sessions/:id/downloads/content
- 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
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/whoami
- GET /v1/usage
- GET /v1/usage/series
- GET /v1/account/rate-limits
- GET /v1/account/limits
- GET /v1/account/me/bundled-llm-status
API keys, team members, billing and orders, and sign-in are managed in the dashboard, not through the API. API keys, OAuth applications, and SDK automation are included on every plan, the free trial too. Create a key in the dashboard and use it in these examples.
Status
- GET /v1/status
- GET /v1/status/incidents
- GET /v1/status/incidents/:id
- GET /v1/status/stream
- GET /v1/status/sla
Common patterns
Three flows, four languages.
Most integrations are built on the same three operations: start an agent session through one of your saved proxies, give it a task in plain words, and close it. 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 work on every plan, the free trial included. Create an API key in the dashboard first. Agent sessions also need the id of one of your saved proxies (find a proxy's id) and a way to pay for the AI (who pays for the AI). On hosted Driftstack only agent sessions load pages.
1. Start an agent session
The minimal "hello world" — start an iPhone Safari session that goes
out through your saved proxy, wait until it is
ready, and return its id.
Without a proxy_id the create is
refused with 422 proxy-required, and
nothing is created.
# cURL
: "${DRIFTSTACK_API_KEY:?Set DRIFTSTACK_API_KEY}"
: "${DRIFTSTACK_PROXY_ID:?Set DRIFTSTACK_PROXY_ID}"
API="https://api.driftstack.dev/v1/agent-sessions"
AUTH="authorization: Bearer $DRIFTSTACK_API_KEY"
AGENT_ID=$(curl --fail-with-body -sS -X POST "$API" \
-H "$AUTH" -H "content-type: application/json" \
-d "$(jq -n --arg proxy "$DRIFTSTACK_PROXY_ID" '{mode: "ai", proxy_id: $proxy}')" \
| jq -r .id)
# status is "active" from the start: wait up to two minutes for ready.
for _ in $(seq 60); do
STATE=$(curl --fail-with-body -sS -H "$AUTH" "$API/$AGENT_ID") || { echo "$STATE" >&2; exit 1; }
[ "$(echo "$STATE" | jq -r .status)" = closed ] && { echo "the session did not start" >&2; exit 1; }
[ "$(echo "$STATE" | jq -r .ready)" = true ] && break
sleep 2
done
echo "$AGENT_ID"
// TypeScript
import { Driftstack } from "@driftstack/sdk";
const apiKey = process.env.DRIFTSTACK_API_KEY;
const proxyId = process.env.DRIFTSTACK_PROXY_ID;
if (!apiKey || !proxyId) throw new Error("Set DRIFTSTACK_API_KEY and DRIFTSTACK_PROXY_ID");
const client = new Driftstack({ apiKey });
const agent = await client.agentSessions.create({ mode: "ai", proxy_id: proxyId });
// status is "active" from the start: wait up to two minutes for ready.
for (let i = 0; i < 60; i++) {
const s = await client.agentSessions.get(agent.id);
if (s.status === "closed") throw new Error("The session did not start: " + s.closed_reason);
if (s.ready !== false) break;
await new Promise((resolve) => setTimeout(resolve, 2_000));
}
console.log(agent.id);
# Python
import os
import time
from driftstack import Driftstack
client = Driftstack(api_key=os.environ["DRIFTSTACK_API_KEY"])
agent = client.agent_sessions.create(
{"mode": "ai", "proxy_id": os.environ["DRIFTSTACK_PROXY_ID"]}
)
# status is "active" from the start: wait up to two minutes for ready.
for _ in range(60):
s = client.agent_sessions.get(agent["id"])
if s["status"] == "closed":
raise RuntimeError(f"The session did not start: {s['closed_reason']}")
if s.get("ready") is not False:
break
time.sleep(2)
print(agent["id"])
// Go
package main
import (
"context"
"fmt"
"log"
"os"
"time"
driftstack "github.com/driftstackdev/driftstack-go"
)
func main() {
apiKey, proxyID := os.Getenv("DRIFTSTACK_API_KEY"), os.Getenv("DRIFTSTACK_PROXY_ID")
if apiKey == "" || proxyID == "" { log.Fatal("set DRIFTSTACK_API_KEY and DRIFTSTACK_PROXY_ID") }
client := driftstack.New(apiKey)
defer client.Close()
ctx := context.Background()
agent, err := client.AgentSessions.Create(ctx, &driftstack.CreateAgentSessionRequest{
Mode: "ai",
ProxyID: proxyID,
}, nil)
if err != nil { log.Fatal(err) }
// Status is "active" from the start: wait up to two minutes for IsReady.
for i := 0; i < 60; i++ {
s, err := client.AgentSessions.Get(ctx, agent.ID)
if err != nil { log.Fatal(err) }
if s.Status == "closed" {
reason := "unknown"
if s.ClosedReason != nil { reason = *s.ClosedReason }
log.Fatal("The session did not start: " + reason)
}
if s.IsReady() { break }
time.Sleep(2 * time.Second)
}
fmt.Println(agent.ID)
}
2. Give it a task
Send the task in plain words; the agent plans the taps and page loads,
runs them, and answers. kind is
plan-executed when it ran, with the
answer it read back. The idempotency
key makes a retried message the same turn rather than a second one.
# cURL
: "${DRIFTSTACK_API_KEY:?Set DRIFTSTACK_API_KEY}"
: "${DRIFTSTACK_AGENT_SESSION_ID:?Set DRIFTSTACK_AGENT_SESSION_ID}"
curl --fail-with-body -sS --max-time 3000 -X POST \
"https://api.driftstack.dev/v1/agent-sessions/$DRIFTSTACK_AGENT_SESSION_ID/message" \
-H "authorization: Bearer $DRIFTSTACK_API_KEY" \
-H "content-type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"user_message":"Open https://example.com and tell me the page title."}' \
| jq '{kind, answer}'
// TypeScript
import { randomUUID } from "node:crypto";
import { Driftstack } from "@driftstack/sdk";
const apiKey = process.env.DRIFTSTACK_API_KEY;
const agentId = process.env.DRIFTSTACK_AGENT_SESSION_ID;
if (!apiKey || !agentId) throw new Error("Set DRIFTSTACK_API_KEY and DRIFTSTACK_AGENT_SESSION_ID");
const client = new Driftstack({ apiKey });
const reply = await client.agentSessions.message(
agentId,
"Open https://example.com and tell me the page title.",
{ idempotencyKey: randomUUID() },
);
if (reply.kind === "plan-executed") console.log(reply.answer ?? reply.results);
else console.log(reply.kind, reply);
# Python
import os
import uuid
from driftstack import Driftstack
client = Driftstack(api_key=os.environ["DRIFTSTACK_API_KEY"])
reply = client.agent_sessions.message(
os.environ["DRIFTSTACK_AGENT_SESSION_ID"],
"Open https://example.com and tell me the page title.",
idempotency_key=str(uuid.uuid4()),
)
if reply["kind"] == "plan-executed":
print(reply.get("answer", reply["results"]))
else:
print(reply["kind"], reply)
// Go
package main
import (
"context"
"fmt"
"log"
"os"
"time"
driftstack "github.com/driftstackdev/driftstack-go"
)
func main() {
apiKey, agentID := os.Getenv("DRIFTSTACK_API_KEY"), os.Getenv("DRIFTSTACK_AGENT_SESSION_ID")
if apiKey == "" || agentID == "" { log.Fatal("set DRIFTSTACK_API_KEY and DRIFTSTACK_AGENT_SESSION_ID") }
client := driftstack.New(apiKey)
defer client.Close()
reply, err := client.AgentSessions.Message(context.Background(), agentID,
"Open https://example.com and tell me the page title.",
&driftstack.MessageOptions{IdempotencyKey: "task-" + time.Now().Format(time.RFC3339Nano)})
if err != nil { log.Fatal(err) }
if reply.Kind == "plan-executed" {
fmt.Println(reply.Answer)
} else {
fmt.Println(reply.Kind)
}
}
3. Close the session
An open session counts against your plan's concurrent-session limit
until it closes. Close it when you are done; a session left unused
closes on its own (an AI session after about 5 minutes with no step
running and no input). Closing answers
204 and is idempotent: closing a closed
session answers 204 too.
# cURL
: "${DRIFTSTACK_API_KEY:?Set DRIFTSTACK_API_KEY}"
: "${DRIFTSTACK_AGENT_SESSION_ID:?Set DRIFTSTACK_AGENT_SESSION_ID}"
curl --fail-with-body -sS -X DELETE \
"https://api.driftstack.dev/v1/agent-sessions/$DRIFTSTACK_AGENT_SESSION_ID" \
-H "authorization: Bearer $DRIFTSTACK_API_KEY"
// TypeScript
import { Driftstack } from "@driftstack/sdk";
const apiKey = process.env.DRIFTSTACK_API_KEY;
const agentId = process.env.DRIFTSTACK_AGENT_SESSION_ID;
if (!apiKey || !agentId) throw new Error("Set DRIFTSTACK_API_KEY and DRIFTSTACK_AGENT_SESSION_ID");
const client = new Driftstack({ apiKey });
await client.agentSessions.close(agentId);
# Python
import os
from driftstack import Driftstack
client = Driftstack(api_key=os.environ["DRIFTSTACK_API_KEY"])
client.agent_sessions.close(os.environ["DRIFTSTACK_AGENT_SESSION_ID"])
// Go
package main
import (
"context"
"log"
"os"
driftstack "github.com/driftstackdev/driftstack-go"
)
func main() {
apiKey, agentID := os.Getenv("DRIFTSTACK_API_KEY"), os.Getenv("DRIFTSTACK_AGENT_SESSION_ID")
if apiKey == "" || agentID == "" { log.Fatal("set DRIFTSTACK_API_KEY and DRIFTSTACK_AGENT_SESSION_ID") }
client := driftstack.New(apiKey)
defer client.Close()
if err := client.AgentSessions.Close(context.Background(), agentID); 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 | Request body failed validation. | 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 plan limit (profile count, api-keys per account, or the free trial’s session time). | TierLimitError |
| 429 | errors.driftstack.dev/rate-limited | Per-account rate limit; retry-after header carries seconds. | RateLimitError (retryable) |
| 429 | errors.driftstack.dev/concurrency-limit | At create: your plan's limit on sessions running at the same time (the tier-bound concurrent-session cap) — end a session you started or wait for one to finish. On an AI agent-session message with ai_tasks_in_flight: true it is a different limit: 3 AI tasks are already running on the account — wait for one to finish, then send the message again; do not close sessions for it. | ConcurrencyLimitError |
| 500 | errors.driftstack.dev/internal | Unexpected error on our side. | InternalError (retryable) |
| 503 | errors.driftstack.dev/feature-unavailable | Feature not enabled on this deployment (for example, avatar upload without file storage configured). One exception: on a session create, code: "dispatch_no_live_node" means no machine could take it just then; nothing was created, so send the same request again after Retry-After. Another: on an AI message, code: "ai_provider_unavailable" means Driftstack's included AI is temporarily unavailable; nothing ran, so send the message again after retry_after_seconds (also Retry-After unless streamed), with a new Idempotency-Key if you sent one. | FeatureUnavailableError (the SDK does not retry it; retry dispatch_no_live_node yourself after Retry-After) |
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 once, and the OpenAPI 3.1 spec is generated from those definitions — 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.