Skip to main content

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.

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

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) }
}

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.

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+json response with a stable type URI (a link that explains the error).
  • → Breaking changes ship under a new path version. /v1 stays stable; /v2 would be a new prefix, not a silent shape change.