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 31 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.
Token pricing
Every paid action bills the higher of its flat floor or the metered LLM cost (tokens × rate). Pricing is shown upfront so there are no surprises.
| Model | input / 1M | output / 1M |
|---|---|---|
| claude-haiku-4-5 | 1,100 cr | 5,500 cr |
| claude-opus-4-7 | 16,500 cr | 82,500 cr |
| claude-sonnet-4-6 | 3,300 cr | 16,500 cr |
| gemini-2-5-flash | 330 cr | 2,750 cr |
| gemini-2-5-pro | 1,375 cr | 11,000 cr |
| gpt-5 | 5,500 cr | 16,500 cr |
| gpt-5-mini | 275 cr | 2,200 cr |
| kimi-k3 | 3,850 cr | 16,500 cr |
| together-glm-5-2 | 660 cr | 2,409 cr |
| together-qwen3-5-397b | 770 cr | 3,850 cr |
| zai-glm-5-2 | 660 cr | 2,420 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.