Subtrace

Browser replay for AI agents.

Contents

Quick Start

1. Install the latest version of Subtrace from here.

2. Login:

subtrace login

3. Record a workflow:

subtrace run -o rec.subtrace -- https://news.ycombinator.com

4. Upload the recording:

subtrace recordings upload rec.subtrace

5. Connect via MCP:

subtrace mcp install --target claude-code

6. Try a browser replay:

claude "Use recording ID rec_188F5AAE753EC398776AA61253C83FE3 to create a browser session via the Subtrace MCP server, and tell me what the top story on HackerNews is."

API Reference

All requests require a Bearer token:

curl -H "Authorization: Bearer ${SUBTRACE_API_KEY}" https://api.subtrace.com/api/...

Auth

Verify token

GET /api/whoami
Returns: {"project_id": "proj_..."}

Recordings

Upload a recording

POST /api/recordings
Body: raw binary (.subtrace file)
Returns: {
  "recording_id": "rec_...",
  "download_url": "https://api.subtrace.com/api/recordings/rec_.../download",
  "size_bytes": 52428800,
  "tags": {"site": "www.example.com", "started_at": "...", "finished_at": "...", "uploaded_at": "..."},
  "page_transitions": ["https://www.example.com/", "https://www.example.com/login"]
}

List recordings

GET /api/recordings
GET /api/recordings?q=site:macys.com
GET /api/recordings?q=site:macys.com -site:nike.com

Search syntax: key:value (regex), -key:value (negation), space-separated (AND).
Quoted values: key:"value with spaces" (JSON string escaping).

Returns: [{...}, ...]

Get a recording

GET /api/recordings/{recordingID}
Returns: {...}

Delete a recording

DELETE /api/recordings/{recordingID}
Returns: 204 No Content

Download a recording

GET /api/recordings/{recordingID}/download
GET /api/recordings/{recordingID}/download?filename=custom.subtrace
Returns: raw binary (.subtrace file)

List links inside a recording

GET /api/recordings/{recordingID}/links
GET /api/recordings/{recordingID}/links?q=tree/v1/exchanges/*

Pattern uses filepath.Match glob syntax.
Without ?q=, returns all links.

Returns: {"tree/v1/tags.json": "blob_...", "tree/v1/journal.json": "blob_...", ...}

Get a blob from a recording

GET /api/recordings/{recordingID}/blobs/{blobID}
GET /api/recordings/{recordingID}/blobs/{blobID}?filename=screenshot.png
Returns: raw binary (application/octet-stream)

Browsers

Create a browser

POST /api/browsers
Body: {
  "recording_id": "rec_...",      // required
  "headless": false,              // default: false
  "stealth": false,               // default: false
  "timeout_seconds": 60,          // default: 60, max: 259200 (72h)
  "kiosk_mode": false             // default: false
}
Returns: {
  "session_id": "brow_...",
  "recording_id": "rec_...",
  "cdp_ws_url": "wss://...",
  "created_at": "...",
  ...
}

List browsers

GET /api/browsers?status=active&limit=20&offset=0
status: "active" (default), "deleted", "all"
Returns: [{...}, ...]
Headers: X-Has-More, X-Next-Offset

Get a browser

GET /api/browsers/{sessionID}
Returns: {...}

Delete a browser

DELETE /api/browsers/{sessionID}
Returns: 204 No Content

Update a browser

PATCH /api/browsers/{sessionID}
Body: {"proxy_id": "..."}
Returns: {...}

Get browser logs

GET /api/browsers/{sessionID}/logs
Returns: {"stdout": "...", "stderr": "..."}

Computer Controls

All endpoints: POST /api/browsers/{sessionID}/computer/{action}

Screenshot

POST /api/browsers/{sessionID}/computer/screenshot
Body: {} or {"region":{"x":0,"y":0,"width":100,"height":100}}
Returns: image/png binary

Click

POST /api/browsers/{sessionID}/computer/click_mouse
Body: {
  "x": 100,                    // required
  "y": 200,                    // required
  "button": "left",            // "left", "right", "middle"
  "click_type": "click",       // "down", "up", "click"
  "num_clicks": 1,
  "hold_keys": ["shift"]       // modifier keys
}

Move mouse

POST /api/browsers/{sessionID}/computer/move_mouse
Body: {"x": 100, "y": 200, "hold_keys": []}

Type text

POST /api/browsers/{sessionID}/computer/type
Body: {"text": "hello world", "delay": 50}  // delay in ms between keys

Press keys

POST /api/browsers/{sessionID}/computer/press_key
Body: {
  "keys": ["Enter", "ctrl+a"],   // key combos
  "duration": 0,                  // hold duration ms
  "hold_keys": []
}

Scroll

POST /api/browsers/{sessionID}/computer/scroll
Body: {
  "x": 500,           // scroll at this position
  "y": 500,
  "delta_x": 0,       // horizontal scroll
  "delta_y": 100,     // vertical scroll (+ = down)
  "hold_keys": []
}

Drag

POST /api/browsers/{sessionID}/computer/drag_mouse
Body: {
  "path": [[100,100], [200,200], [300,100]],  // points to drag through
  "button": "left",
  "delay": 0,                                  // delay before drag starts
  "hold_keys": []
}

Get current URL

GET /api/browsers/{sessionID}/computer/location
Returns: {"url": "https://..."}

Navigate to URL

POST /api/browsers/{sessionID}/computer/location
Body: {"url": "https://example.com"}

Python SDK

pip install subtrace
import subtrace

client = subtrace.Client(token="st_...")

# List recordings
for rec in client.recordings.list():
    print(rec.recording_id, rec.tags.get("site", ""))

# Filter recordings by tag
for rec in client.recordings.list(q="site:macys.com"):
    print(rec.recording_id)

# Run a rollout (creates browser, provides MCP config, cleans up on exit)
with client.rollout(recording_id="rec_...") as r:
    print(r.task)           # task description (from tree/v1/task.md)
    print(r.session_id)     # browser session ID
    print(r.mcp.url)        # MCP endpoint URL
    print(r.mcp.headers)    # MCP auth headers

    # ... run your agent ...

    signals = r.signals()   # collect reward signals from entrypoint.js

See examples/mcp_rollout.py and examples/simple_agent.py for full examples.

CLI

Install: download from mac.subtrace.com

# Login
subtrace login

# Record a workflow
subtrace run -o recording.subtrace -- https://example.com

# Replay a recording locally
subtrace run -i recording.subtrace -- about:blank

# Edit files inside a recording
subtrace edit recording.subtrace                    # edit entrypoint.js (default)
subtrace edit recording.subtrace tree/v1/task.md    # edit task description

# Recordings
subtrace recordings upload recording.subtrace
subtrace recordings list
subtrace recordings delete rec_...

# Browsers
subtrace browsers create rec_...
subtrace browsers create recording.subtrace         # uploads + creates in one step
subtrace browsers create rec_... --headless --timeout 300
subtrace browsers list
subtrace browsers get brow_...
subtrace browsers delete brow_...
subtrace browsers logs brow_...

# Computer controls
subtrace computers screenshot brow_... --to screenshot.png
subtrace computers click-mouse brow_... -x 100 -y 200
subtrace computers move-mouse brow_... -x 100 -y 200
subtrace computers type brow_... --text "hello world"
subtrace computers press-key brow_... --key Enter --key Tab
subtrace computers scroll brow_... -x 500 -y 500 --delta-y 100
subtrace computers drag-mouse brow_... --point 100,100 --point 200,200
subtrace computers get-location brow_...
subtrace computers set-location brow_... --url https://example.com

MCP

For AI agents (Claude, Cursor, etc.) to control browsers via Model Context Protocol.

Setup for Claude Code:

subtrace mcp install --target claude-code

Available tools:

Manual setup:

claude mcp add subtrace https://api.subtrace.com/mcp -t http -H "Authorization: Bearer ${SUBTRACE_API_KEY}"

Or add to MCP config:

{
  "mcpServers": {
    "subtrace": {
      "url": "https://api.subtrace.com/mcp",
      "headers": {
        "Authorization": "Bearer ${SUBTRACE_API_KEY}"
      }
    }
  }
}

CDP (Chrome DevTools Protocol)

Each browser exposes a WebSocket endpoint for direct CDP access:

wss://api.subtrace.com/api/browsers/{sessionID}/cdp?secret={cdp_secret}

The cdp_ws_url field in browser responses contains the full URL.

Use any CDP client (Puppeteer, Playwright, etc.) to connect:

const browser = await puppeteer.connect({
  browserWSEndpoint: cdp_ws_url
});

Web Dashboard

The web dashboard is available at the root URL of the API server.

File Format

A .subtrace file is a zip archive containing:

Well-known paths:

Path Description
tree/v1/runtime/entrypoint.js Replay runtime (URL matching, request handling, reward signals)
tree/v1/journal.json Newline-delimited JSON event log (page transitions, interactions, screenshots)
tree/v1/tags.json Flat key-value metadata ({"site": "...", "started_at": "...", ...})
tree/v1/task.md Task description in markdown (optional)
tree/v1/exchanges/* Recorded HTTP request/response pairs

Canonical tags:

Key Description
site Hostname of the first page transition
started_at Recording start time (RFC 3339, nanosecond precision)
finished_at Recording finish time
uploaded_at Upload time (set server-side)
num_events Total journal event count
num_page_transitions Number of page transitions
num_exchanges Number of HTTP exchanges

Editing files inside a recording:

subtrace edit recording.subtrace                    # edit entrypoint.js
subtrace edit recording.subtrace tree/v1/task.md    # edit task description

3.096 · 44387e95442d9 · 2026-02-11 02:42 UTC