For AI product teams
Ship the browser feature without running the fleet.
Your product needs an agent that can sign in, click and read live pages for each of your customers. Twin Browser is that layer as an API: per-customer subtenants, isolated sessions, a live view you can stream into your own UI, and repeat work that replays from a compiled skill instead of a fresh model call.
The problem
What this costs you today.
The demo took a weekend: an LLM, a headless browser, a few selectors. Production is a different product. Every customer needs their own logins and their own isolated session. Sites answer a datacenter IP differently than they answer a person. Someone has to own proxy egress, CAPTCHA, 2FA, session persistence, video capture for support tickets, and a fleet that does not fall over at 9am. None of that is the thing you are selling, and all of it is now on your roadmap — while the model bill grows linearly with every customer you add.
- “Two of our four engineers are maintaining browser infrastructure, not building the product.”
- “Our per-customer COGS goes up every month because every run re-pays for the same reasoning.”
- “We need each customer’s credentials and cookies isolated from every other customer’s, and I do not want to build that.”
- “Support asks what the agent actually did, and all we can show them is a log line.”
- Your agent posts a goaldone
- Subtenant key resolves the sessionrunning
- Browser acts on the live pagequeued
- Frames stream to your UIqueued
- Result + recording returnedqueued
How Twin solves it
The mechanism, not a promise.
Twin is the execution layer, not a framework you adopt. You keep your agent, your prompts and your product; you call an HTTP endpoint (or an MCP tool) when it needs to touch a real browser. Isolation, credentials, egress, recording and metering are the layer’s job.
- 1One subtenant per customerPOST /api/v1/tenants provisions an isolated tenant with its own API key, its own vault and its own sessions. `billing:"parent"` bills your pool and attributes every charge to that subtenant; `billing:"self"` gives them a balance of their own. API-key scopes are enforced, so a customer key cannot provision.
- 2Your agent calls one endpointPOST /api/v1/run takes a URL, a goal in words, and a structured success condition. The tenant is derived from the Bearer key, never from the body — there is no cross-tenant call to get wrong.
- 3Stream the browser into your own UIPOST /api/v1/live returns the same run as Server-Sent Events: a `meta` frame with the runId, JPEG `frame` events, then a terminal `result`. GET /api/v1/runs/{id}/video serves the durable recording afterwards.
- 4Repeat work stops paying for reasoningPOST /api/v1/dispatch fronts the same body with the semantic cache: a re-phrased request matches a skill this key already compiled and replays it, so the planner model is never invoked on the second, tenth or thousandth run.
- 5A wall becomes a callback, not a failureA run that meets a 2FA or approval wall returns `{ status:"paused", sessionId }` instead of failing, resumable via POST /api/v1/runs/{id}/resume — so your product can ask its user for a code rather than showing them an error.
In practice
The actual call, and what it returns.
Two calls: one to give a customer their own isolated tenant, one for that customer’s agent to drive a browser under it. The second call is the only one your product makes at runtime.
# 1 — provision an isolated subtenant (needs the tenants:provision scope).
curl https://twin-browser.com/api/v1/tenants \
-H "Authorization: Bearer $TWIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "acme-corp", "billing": "parent" }'
# → 201
# {
# "tenant": { "id": "…", "name": "acme-corp", "slug": "acme-corp",
# "plan": "…", "billing": "parent" },
# "apiKey": "…" <- returned ONCE; store it against your customer
# }
# 2 — that customer's agent drives a browser with its own key.
curl https://twin-browser.com/api/v1/run \
-H "Authorization: Bearer $SUBTENANT_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.vendor.example.com/invoices",
"prompt": "open the newest unpaid invoice and read its number and total",
"success": { "kind": "extracted" },
"account": "vendor-login",
"persistSession": true
}'
# → 200 { "success": true, "steps": 6, "runId": "…", "credits_charged": 10 }What this call does
- `billing` decides who pays: "parent" charges your pool and attributes the charge to the subtenant; "self" gives the subtenant its own balance.
- The API key is returned exactly once by POST /tenants — there is no endpoint that reads it back.
- Nested resellers are refused with a 409, so the tenancy tree stays one level deep and auditable.
- Swap /run for /live and the identical body streams JPEG frames over SSE, which is what you render inside your own product.
What it costs
Priced per action, not per seat.
You are buying per-run cost that falls with repetition, not a seat licence. These are the flat floors; a run that burns more in model and compute than its floor bills the higher of the two.
| Action | Credits | What you get |
|---|---|---|
| POST /run — an agent run | 10 / run | The synchronous goal-to-result call your product makes. |
| POST /live — the same run, streamed | 10 / run | SSE frames for your UI; the recording is kept for replay. |
| POST /dispatch — cache hit | 2 / run | A re-phrased request matched a compiled skill. No planner model. |
| POST /dispatch — cache miss | 10 / run | Nothing matched: it plans once and caches the result for next time. |
How the unit works
- $1 buys 1,000 credits; the smallest pack is $5.
- A paid action bills the higher of its flat floor and what it actually spent on model, compute and egress — so a cheap run stays cheap.
- “Metered” means the action has no published flat floor on this page: GET /api/v1/pricing serves the live card.
- Subtenants add no per-tenant fee — a "parent"-billed subtenant spends from your pool and every charge is attributed to it.
Be sure this fits
What this does not do.
Every one of these will come up in your evaluation. Here they are first, from us.
It is not a raw browser you drive frame by frame
There is no CDP socket to attach to. You get goal-shaped endpoints (/run, /live, /observe, /extract) and manual control of a PAUSED run via POST /runs/{id}/input. If your product needs to script every keystroke itself, drive Playwright and use Twin for the parts that need planning.
The dashboard is ours, not yours
A subtenant is an isolation and billing boundary in the API. There is no white-label console you can re-skin and hand to your customers — the surface you give them is the one you build on top of these endpoints.
The first run of a new flow still costs a full planning pass
The cache pays off on repetition. A customer whose every request is genuinely novel pays the miss price every time; the economics improve because your customers do the same things repeatedly, not because the cache is magic.
Under the hood
The primitives this runs on.
Nothing here is specific to this problem — the same mechanisms carry every solution on the site.
Semantic dispatch cache
A re-phrased request is embedded, vector-matched against the skills you have already compiled, and replayed deterministically — so the second ask and every one after it skips the planner.
Read the mechanism — Semantic dispatch cacheLive view & session video
POST /api/v1/live streams the browser back as Server-Sent Events while the agent works, and an opted-in run keeps a durable video you can fetch long afterwards.
Read the mechanism — Live view & session videoCredential vault
A write-only per-tenant vault: a stored secret is referenced in a goal as {{secret:NAME}}, resolved inside the browser at fill time, and redacted from every step, frame and log.
Read the mechanism — Credential vaultHuman-in-the-loop handoff
A 2FA prompt, an approval push or a CAPTCHA the agent cannot clear returns status "paused" with a live session — resolve it automatically from a connected inbox, hand back a code, or drive the page yourself.
Read the mechanism — Human-in-the-loop handoffOver MCP, the same work is these tools
run_goalExecute a goal on a target URL you provide — observe, plan, and act until a success condition is met. Takes free text ({ url, prompt, success }) or a one-word built-in goal ({ target, goal }). Full run control: egress country and tier, humanized timing, warm-up, stealth, file upload, credentials, recording. A sign-in run parks by default on a 2FA/approval wall it can’t auto-resolve, returning { status:"paused", sessionId } (resume with submit_verification); pass hitl:false to opt out. If the result carries code:"credential_rejected", the site REFUSED the identifiers the run typed (credentialRejected.secrets names them, never their values) — running the same goal again sends the same wrong ones, so supply different credentials or send the user a connect_account link instead of retrying.
dispatchThe cheap path: fuzzy-match a goal to an existing compiled skill via the semantic cache — a hit replays deterministically (~5× cheaper), a miss compiles and caches it for next time (needs WEB_BASE_URL + TWIN_API_KEY).
submit_runSubmit a goal as an async background job; returns a job id immediately. Same two shapes and the same run controls as run_goal. Like run_goal, a sign-in job parks by default on a 2FA/approval wall — get_job then reports status:"paused" with a sessionId.
get_jobPoll an async job by id for status, success, steps and result.
FAQ
Browser infrastructure for AI products — common questions.
Can I keep my own agent framework?
How do I keep one customer’s logins away from another’s?
What does my customer see when a site asks for a 2FA code?
Do I have to use the semantic cache?
Related
Other problems this layer solves.
Systems with no API
Ops, finance and platform teams blocked by a vendor system that offers no programmatic access.
When the site turns you away
Teams whose authorized automation is being turned away by bot defences on sites they are entitled to use.
Onboarding and migration data entry
Implementation, onboarding and professional-services teams who move customer data into systems by hand.
Try it on your hardest screen.
Start free, point a run at the system that is blocking you, and watch it happen live. If it does not work, the run tells you why — and what to do instead.