Guide — the developer loop
The whole workflow end-to-end with real request and response shapes: get a key, run a goal, cut the cost with the semantic cache, extract structured data, watch runs live, resume human-in-the-loop pauses, hand end-users a sign-in link, and read the live rate card. Endpoint details live in the API reference.
1 · Get a key
Create a per-tenant API key in the dashboard under Keys & Secrets — it starts with ab_live_ and is shown once. Every call is a Bearer request; the tenant is always derived from the key, never from the body. Each run is scoped to your tenant, metered in credits, and written to the audit log.
2 · First run — POST /run
A run is a url (the authorization signal), a natural-language prompt, and a structured success condition the engine must satisfy. Synchronous, ~10 credits; the response carries the verified action path and exactly what was charged.
curl https://twin-browser.com/api/v1/run \
-H "Authorization: Bearer ab_live_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.acme.com/invoices",
"prompt": "export last month as CSV",
"success": { "kind": "urlIncludes", "value": "/exports" }
}'
# → 200 { "success": true, "path": [ … ], "credits_charged": 10, "engineRev": "…" }
#
# success is a structured condition — one of:
# { "kind": "urlIncludes", "value": "…" } { "kind": "textVisible", "value": "…" }
# { "kind": "statusText", "match": "…" } { "kind": "extracted" }
# { "kind": "allOf" | "anyOf", "conditions": [ … ] }A sign-in run that hits a 2FA or approval wall parks by default instead of failing — resume it with a code, drive the browser yourself, or hand the sign-in to your end-user. Set hitl:false to opt out and get a plain miss.
# A sign-in run that hits a 2FA/approval wall PARKS by default (hitl) instead of failing:
# → 200 { "status": "paused", "sessionId": "…", "reason": "…", "challenge": "sms-code" }
#
# Three ways forward from a paused run:
# 1. POST /api/v1/runs/{id}/resume { "sessionId": "…", "code": "123456" }
# 2. Watch + drive it yourself:
# GET /api/v1/runs/{id}/stream (SSE screencast, free)
# POST /api/v1/runs/{id}/input { "event": { "kind": "click", "x": 0.42, "y": 0.61 } }
# (coordinates are 0..1 viewport fractions; kinds: click | fill | key | scroll | goto)
# 3. POST /api/v1/connect/sessions — send your END-USER a link to sign in by hand.
#
# Two result codes tell you what a run needs:
# "code": "credential_missing" + missingSecret → store it via POST /secrets, or use a connect link
# "code": "connect_required" + connectUrl → a ready one-time link; hand it to the user3 · Dispatch vs. run — the semantic cache
POST /run always plans live. POST /dispatch takes the same body but first vector-matches your request against the skills already compiled for that host — a HIT replays the verified path deterministically with no LLM in the loop. Default to dispatch for anything you will ask twice.
# Same body as /run — but fronted by the semantic cache:
curl https://twin-browser.com/api/v1/dispatch \
-H "Authorization: Bearer ab_live_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.acme.com/invoices",
"prompt": "download last month'"'"'s invoices as a CSV",
"success": { "kind": "urlIncludes", "value": "/exports" }
}'
# HIT → your re-phrased request matches an already-compiled skill for that host:
# deterministic blind replay, no LLM (~2 credits)
# MISS → live discovery + minimize, then cached with its embedding
# so the NEXT similar request is a hit (~10 credits)
# The verified action path is returned in BOTH modes.
# Or manage skills explicitly:
# POST /api/v1/skills { "target": "…", "goal": "…", "as": "acme-export" } (~50 cr)
# POST /api/v1/skills/acme-export/run → deterministic replay, no LLM (~1 cr)
# GET /api/v1/skills → list compiled skills4 · Extract — structured JSON out of any page
POST /extract reads a page and returns JSON matching the fields or schema you ask for (~5-credit floor). When a per-host template matches, extraction is deterministic — zero LLM, zero metered cost.
curl https://twin-browser.com/api/v1/extract \
-H "Authorization: Bearer ab_live_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://news.ycombinator.com",
"fields": ["title", "points", "url"]
}'
# → 200 { …structured result…, "credits_charged": 5 }
#
# Describe the output with "fields" (flat list) or "schema" (JSON-schema-like) —
# or neither: a per-host extraction template (see GET /api/v1/templates) matches
# many URLs and returns structured data deterministically, zero LLM, 0 metered cost.
# "template": "name" forces one; "template": false disables (then schema/fields required)
# "waitMs": 2000 adds settle time for JS-hydrated pages5 · Watch it work — live view, HITL, async jobs
POST /live streams the browser back as Server-Sent Events while the run executes, and emits an await-user step when a second factor needs you. For background work, submit the same body to /jobs and poll, stream, or receive an HMAC-signed webhook.
# Run a goal and stream the LIVE browser view back as Server-Sent Events:
curl -N https://twin-browser.com/api/v1/live \
-H "Authorization: Bearer ab_live_…" \
-H "Content-Type: application/json" \
-d '{ "url": "…", "prompt": "…", "success": { "kind": "extracted" } }'
# event: meta → { "runId": "…" }
# event: frame → JPEG frames of the browser
# event: step → on a 2FA wall: { "type": "await-user", "token": "…", "challenge": "sms-code" }
# answer with POST /api/v1/live/resume { "token": "…", "code": "123456" }
# event: result → terminal result; a durable video is then at GET /api/v1/runs/{id}/video
# Prefer fire-and-forget? Submit the same body as an async job:
# POST /api/v1/jobs → 202 { "jobId": "…" } (optional callbackUrl + callbackSecret,
# GET /api/v1/jobs/{id} HMAC-signed completion webhook)
# GET /api/v1/jobs/{id}/stream (SSE status frames instead of polling)
# POST /api/v1/jobs/{id}/cancel (full refund of reserved credits)6 · End-user logins — connect links
When the credential belongs to someone whose password you should not hold — or a site blocks automated login outright — mint a connect link. The end-user signs in by hand once; Twin Browser captures the session; every later run is already logged in. OAuth-shaped, for sites without OAuth.
# Mint a one-time link your END-USER opens to sign in by hand — password, 2FA,
# CAPTCHA — inside a Twin-Browser-hosted browser. The session is captured; the
# password is never stored.
curl https://twin-browser.com/api/v1/connect/sessions \
-H "Authorization: Bearer ab_live_…" \
-H "Content-Type: application/json" \
-d '{ "host": "linkedin.com", "account": "work", "redirectUri": "https://yourapp.com/done" }'
# → 201 { "id": "…", "url": "https://…", "expiresAt": "…", "ttlMinutes": 30 }
# (the url is shown once; the link is scoped to that one host)
# Check whether the user finished before re-running:
# GET /api/v1/connect/sessions
# → { "sessions": [{ "id", "host", "account", "status", … }] }
# status: pending | active | connected | failed | expired | cancelled
# Re-run the original task afterwards → "sessionRestored": true — already logged in.For accounts you do own, store the credential once in the write-only secrets vault and reference it by name — the value never leaves the vault except at fill-time inside the browser. Discover the logins you already hold with GET /accounts.
# Store credentials once, reference them by name — values are write-only,
# encrypted at rest, and redacted from every log and stream:
curl https://twin-browser.com/api/v1/secrets \
-H "Authorization: Bearer ab_live_…" \
-H "Content-Type: application/json" \
-d '{ "name": "ACME_PASSWORD", "value": "…" }'
# Then in a prompt: "sign in as jo@acme.com with {{secret:ACME_PASSWORD}}"
# Which logins do I already have? (metadata only — never a credential)
# GET /api/v1/accounts?host=acme.com
# → { "accounts": [{ "label": "work", "host": "acme.com", "emailPreview": "j…@acme.com" }] }
# Pass a label as "account" on any run and that login + its saved session apply.7 · Beyond curl — MCP, extension, adapters
Everything above is plain REST, but the glue code is already written: npx -y twin-browser-mcp exposes 37 tools (dispatch, runs, skills, extract, connect links, verification resume) natively to Cursor, Claude Desktop, Claude Code, or Cline — MCP setup. For popup-OAuth sign-ins, the capture extension ships a session from the user's own browser (cookies only, never a password). LangChain / AutoGen adapters come from makeTwinBrowserTools(). The full machine-readable surface is GET /api/v1/openapi (public, no key).
8 · What it costs — the live rate card
Flat per-action credit floors plus metered passthrough — the same numbers your Settings → Billing tab bills against, served live from the platform (never a stale copy). Machine-readable at GET /api/v1/pricing.
Rate card
A paid action bills the higher of its flat floor or its metered LLM cost — so a cached replay costs the floor and nothing more, and a heavy cold run bills what it actually consumed. Every rate below is live from the platform, never a copy.
Cache verdict
| Action | Floor | ≈ USD |
|---|---|---|
| Cache hit · no LLM | 2 cr | $0.002 |
| Adapt · shared scaffold | 5 cr | $0.005 |
| Cache miss · cold | 10 cr | $0.010 |
| Skill replay | 1 cr | $0.001 |
| Compile a skill | 50 cr | $0.050 |
Run surface
| Action | Floor | ≈ USD |
|---|---|---|
| Run | 10 cr | $0.010 |
| Live run | 10 cr | $0.010 |
| Async job | 10 cr | $0.010 |
Data surface
| Action | Floor | ≈ USD |
|---|---|---|
| Search | 3 cr | $0.003 |
| Map | 2 cr | $0.002 |
| Crawl · per page | 3 cr | $0.003 |
| Deep search | 3 cr | $0.003 |
Metered token rates · credits per 1M tokens
| Model | Input | Output |
|---|---|---|
| claude-fable-5 | 11,625 cr | 58,125 cr |
| claude-fable-5-1 | 11,625 cr | 58,125 cr |
| claude-haiku-4-5 | 1,162.5 cr | 5,812.5 cr |
| claude-opus-5 | 5,812.5 cr | 29,062.5 cr |
| claude-opus-5-5 | 4,650 cr | 23,250 cr |
| claude-sonnet-5 | 2,325 cr | 11,625 cr |
| claude-sonnet-5-5 | 2,325 cr | 11,625 cr |
| gemini-3-1-flash-lite | 290.625 cr | 1,743.75 cr |
| gemini-3-1-pro-preview | 2,325 cr | 13,950 cr |
| gemini-3-5-flash-lite | 348.75 cr | 2,906.25 cr |
| gemini-3-7-flash | 871.875 cr | 4,359.375 cr |
| gemini-3-8-flash | 871.875 cr | 4,359.375 cr |
| gemini-3-8-flash-lite-tts | 581.25 cr | 6,975 cr |
| gemini-3-8-flash-tts | 581.25 cr | 10,462.5 cr |
| gpt-5-4 | 2,906.25 cr | 17,437.5 cr |
| gpt-5-4-mini | 871.875 cr | 5,231.25 cr |
| gpt-5-4-nano | 232.5 cr | 1,453.125 cr |
| gpt-5-5 | 5,812.5 cr | 34,875 cr |
| gpt-5-6-luna | 232.5 cr | 1,395 cr |
| gpt-5-6-terra | 2,325 cr | 13,950 cr |
| gpt-5-mini | 290.625 cr | 2,325 cr |
| gpt-5-nano | 58.125 cr | 465 cr |
| gpt-6-1-sol | 2,325 cr | 11,625 cr |
| gpt-6-astra | 11,625 cr | 58,125 cr |
| gpt-6-luna | 116.25 cr | 581.25 cr |
| gpt-6-sol | 2,325 cr | 11,625 cr |
| inception-mercury-2 | 290.625 cr | 871.875 cr |
| inception-mercury-2-5 | 46.5 cr | 174.375 cr |
| inception-mercury-edit-2 | 290.625 cr | 871.875 cr |
| kimi-k2-6 | 1,104.375 cr | 4,650 cr |
| kimi-k2-7-code | 1,104.375 cr | 4,650 cr |
| kimi-k3 | 3,487.5 cr | 17,437.5 cr |
| muse-spark-1-2 | 1,453.125 cr | 4,940.625 cr |
| muse-spark-1-3 | 1,453.125 cr | 4,940.625 cr |
| together-deepseek-v4-1-flash | 348.75 cr | 1,395 cr |
| together-deepseek-v4-flash | 162.75 cr | 325.5 cr |
| together-deepseek-v4-pro | 1,534.5 cr | 4,603.5 cr |
| together-glm-5-2 | 1,627.5 cr | 5,115 cr |
| together-glm-5-3 | 1,627.5 cr | 5,115 cr |
| together-glm-5-3-flash | 174.375 cr | 581.25 cr |
| together-kimi-k3 | 3,487.5 cr | 17,437.5 cr |
| together-minimax-m3 | 348.75 cr | 1,395 cr |
| zai-glm-5-2 | 1,627.5 cr | 5,115 cr |
| zai-glm-5-3 | 1,627.5 cr | 5,115 cr |
| zai-glm-5-3-flash | 174.375 cr | 581.25 cr |
Reselling. Twin meters every call at cost-plus and rolls it into your tenant's credit balance, so you can wrap the API under your own brand and price — POST /api/v1/tenants provisions subtenants with per-subtenant billing modes and scoped keys. Per-subtenant usage lives on your Billing tab.