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.

Run a goalbash
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.

Paused runs & result codesbash
# 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 user

3 · 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.

dispatch — hit or compilebash
# 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 skills

4 · 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.

extract — fields, schema, or templatebash
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 pages

5 · 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.

live view & jobsbash
# 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.

connect/sessionsbash
# 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.

secrets & accountsbash
# 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

1,000 credits / USD · region US

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.

Flat action floors
Run10 crCache hit2 crAdapt5 crCache miss10 crCompile skill50 crSkill replay1 crLive run10 crAsync job10 crSearch3 crMap2 crCrawl (per page)3 crDeep search3 cr
Metered token rates · per 1M tokens
Modelinput / 1Moutput / 1M
claude-haiku-4-51,100 cr5,500 cr
claude-opus-4-716,500 cr82,500 cr
claude-sonnet-4-63,300 cr16,500 cr
gemini-2-5-flash330 cr2,750 cr
gemini-2-5-pro1,375 cr11,000 cr
gpt-55,500 cr16,500 cr
gpt-5-mini275 cr2,200 cr
kimi-k33,850 cr16,500 cr
together-glm-5-2660 cr2,409 cr
together-qwen3-5-397b770 cr3,850 cr
zai-glm-5-2660 cr2,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.