Operations

Sign in, survive 2FA, and never hold a password you should not

The four ways past a login wall — a vaulted secret, an auto-resolved emailed code, a code you hand back, and a connect link the account owner opens — and how to pick between them.

Most automation treats a second factor as a failure. It does not have to be. A sign-in run that meets a wall it cannot clear PARKS by default: the browser stays open, the credit reservation is refunded, and there are four ways forward. This guide covers all four and, more importantly, how to decide which one a given situation calls for.

Decide first: whose password is it?

This is the fork that determines everything else. If the credential is yours to hold — a service account, your own portal login — store it in the vault and reference it by name. If it belongs to your customer, do not hold it at all: mint a connect link and let them sign in themselves, so only the resulting session is ever captured. Choosing the vault for someone else's password is the decision that gets regretted later.

Path 1 — a vaulted secret

POST /api/v1/secrets stores an encrypted, per-tenant value that no read ever returns. Reference it in the prompt as {{secret:NAME}}; the value is resolved inside the browser at fill time, after the planner has decided what to type, and is redacted from the returned path, the frames and the logs. A run that needs a secret you have not stored returns code "credential_missing" with the name it wanted.

Store once, reference by tokenbash
curl -X POST https://twin-browser.com/api/v1/secrets \
  -H "Authorization: Bearer $TWIN_API_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"PORTAL_PASSWORD","value":"…"}'

# GET /api/v1/secrets returns NAMES only. There is no read that returns a value.

Path 2 — no human at all, for an emailed code

Connect an IMAP inbox and the engine fetches and fills the emailed verification code itself; the pause never surfaces. The inbox is validated against a real IMAP connection before it is stored, so a wrong host or app-password is rejected at setup rather than discovered mid-run. The app-password is write-only and never returned.

Connect an inbox so emailed codes resolve themselvesbash
curl -X POST https://twin-browser.com/api/v1/email-inbox \
  -H "Authorization: Bearer $TWIN_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "host": "imap.gmail.com",
    "port": 993,
    "user": "ops@acme.com",
    "pass": "…app password…",
    "secure": true
  }'
# → { "name": "email:ops@acme.com", "host": "imap.gmail.com", "user": "ops@acme.com" }

# GET  /api/v1/email-inbox            → { inboxes: [ { name, host, user } ] }
# DELETE /api/v1/email-inbox?name=…   → disconnect

Path 3 — hand back a code the run cannot fetch

An authenticator (TOTP) code, a phone approval, or a code that arrives somewhere Twin cannot read parks the run. Branch on status === "paused", keep the sessionId, and POST it back with the code. The same call can also supply a credential the run turned out to need — merged into the parked session's vault so the same browser continues, with the values redacted from every frame, step and log.

Resume a parked runbash
# The run came back as:
# { "status":"paused", "sessionId":"sess_…", "reason":"…",
#   "challenge":"sms-code", "codeRequired":true }

curl -X POST https://twin-browser.com/api/v1/runs/run_…/resume \
  -H "Authorization: Bearer $TWIN_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "sessionId": "sess_…", "code": "483920" }'

# …or supply the credential it discovered it needed:
#   { "sessionId": "sess_…",
#     "secrets": { "email": "ops@acme.com", "password": "…" } }

Path 3b — take the wheel yourself

Some steps are neither a code nor automatable: an unusual consent dialog, a one-off confirmation. GET /api/v1/runs/{id}/stream mirrors the parked browser as a live screencast (free — it drives no agent), and POST /api/v1/runs/{id}/input sends a click, fill, key, scroll or goto. Coordinates are FRACTIONS of the viewport, 0..1 from the top-left, hit-tested server-side, so they work from a screenshot at any size. There is no "done" call: the run re-checks the page after each interaction and continues by itself once the block clears.

Click at 62% across, 41% downbash
curl -X POST https://twin-browser.com/api/v1/runs/run_…/input \
  -H "Authorization: Bearer $TWIN_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "event": { "kind": "click", "x": 0.62, "y": 0.41 } }'
# → { "ok": true, "url": "https://…" }

Path 4 — the connect handoff

When the password is not yours, or the site hard-challenges automated sign-in from datacenter IPs and simply cannot be signed into by an agent, mint a link. It is single-use, scoped to one host, and expires in about thirty minutes. The person opens it, signs in by hand — password, second factor, CAPTCHA — and the resulting SESSION is captured. Re-run the task and it restores.

Mint a link, then check it was usedbash
curl -X POST https://twin-browser.com/api/v1/connect/sessions \
  -H "Authorization: Bearer $TWIN_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "host": "portal.example.com",
        "account": "acme-ops",
        "redirectUri": "https://your.app/connected" }'
# → 201 { "url": "https://twin-browser.com/connect/…", "expiresAt": "…", "ttlMinutes": 30 }

curl "https://twin-browser.com/api/v1/connect/sessions" \
  -H "Authorization: Bearer $TWIN_API_KEY"
# → { "sessions": [ { "id":"…", "status":"connected", "connected_at":"…" } ] }

Several logins on one site

Give each an `account` label — at connect time, and on every later run. GET /api/v1/accounts lists what you have as { label, host, emailPreview }, masked and never a credential, so an agent can discover which logins exist before choosing one. Each labelled account also gets its own sticky egress identity and its own cadence budget.

The one popup OAuth cannot do

A "Continue with Google" sign-in only completes in a real local browser, so the hosted connect flow cannot drive it. For those, capture the cookies on the user's own machine and POST them to /api/v1/sessions/import with the host; they are assembled into the same encrypted session a hosted sign-in would have produced, stored under both apex and www forms of the host.

Common questions

Am I charged for a run that parks?
No. The reservation is refunded when it parks, and the run floor is charged when a resume completes it. Watching a parked session and driving it are also free — they mirror and steer a browser that is already open.
Can Twin generate my authenticator codes?
No. An emailed or texted code can be fetched when you have connected an inbox or a phone number. A TOTP code or a phone approval is something only you hold, and the run parks and waits for it.
Why are the input coordinates fractions instead of pixels?
So they work regardless of the viewer's screen. A fraction of the viewport is hit-tested server-side against the observed element map, which means a click read off a screenshot at any size lands on the right element.
How long does a connect link last?
About thirty minutes, single use, scoped to one host. There is also a cap on how many can be pending at once — the endpoint answers 429 rather than minting unbounded links.

Delegate the work. Keep the decision.

Hand off a real task, set the guardrails, and let repeated work compile into a skill that replays deterministically at near-zero cost.