Browser replay for AI agents.
Contents
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."
All requests require a Bearer token:
curl -H "Authorization: Bearer ${SUBTRACE_API_KEY}" https://api.subtrace.com/api/...
Verify token
GET /api/whoami
Returns: {"project_id": "proj_..."}
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)
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": "..."}
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"}
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.
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
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:
create_browser — create a browser session from a recordingdelete_browser — delete a browser sessionscreenshot — capture screenshotset_location — navigate to URLget_location — get current URLclick — click at coordinatestype — type textpress_key — press keyboard keysscroll — scroll at coordinatesManual 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}"
}
}
}
}
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
});
The web dashboard is available at the root URL of the API server.
/login — enter your API token/p/{projectID} — list all recordings/p/{projectID}/r/{recordingID} — view a recording (tags, page transitions, screenshots with interaction overlays)A .subtrace file is a zip archive containing:
blobs/ — content-addressed binary blobstree/v1/ — symlinks pointing to blobsWell-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