For procurement, AP and supply-chain teams

One agent across every portal you have a login for.

Forty suppliers, forty portals, forty logins, and no two built the same. Twin keeps a labelled login and its own session per portal, lets a human sign in once where software cannot, and then pulls invoices, order status and documents on your schedule.

The problem

What this costs you today.

Every supplier has their own portal, and every portal is the same three screens in a different order. Somebody on the team signs into each one on a rota, checks order status, downloads the invoices, and re-types the totals into the ERP. The portals with an EDI feed are the lucky ones. The rest are a person with a password manager and a spreadsheet, and when that person is on leave the numbers are simply late.

  • “We have logins for thirty-eight supplier portals and no integration with any of them.”
  • “Half of them block anything that is not a person, so scripts never worked.”
  • “Each portal needs its own login and its own session — one shared account is not an option.”
  • “Month-end close waits on someone downloading PDFs by hand.”
portal.example.com
  1. Restore the portal sessiondone
  2. Open the invoice listingrunning
  3. Expand rows deterministicallyqueued
  4. Record numbers and documentsqueued
  5. POST the result to your ERPqueued
One Twin run for supplier and customer portals — the work happens in a real browser, under your guardrails.

How Twin solves it

The mechanism, not a promise.

The credential system, not the browser, is what makes this tractable: every login is a labelled account with its own encrypted session, and every run-shaped endpoint takes that label. Where a portal refuses software outright, a human signs in once through a hosted link and the captured session carries every later run.

  1. 1Label a login per portalEach stored login has a label and a host. GET /api/v1/accounts lists what you already have — label, host and a masked email preview, never a credential — so a run can be planned before a credit is spent.
  2. 2Let a human sign in where software cannotPOST /api/v1/connect/sessions mints a single-use link scoped to one host. Your colleague or the supplier’s user completes the sign-in — password, 2FA, CAPTCHA — in a hosted browser, and the session is captured. The password is never stored.
  3. 3Re-use the session, not the credentialPass `account` and `persistSession` on a run and the stored cookie jar is restored before the run and re-persisted after (`sessionRestored: true`), so day-to-day work does not touch a password or a second factor at all.
  4. 4Run the sweep in the backgroundPOST /api/v1/jobs submits the same body asynchronously and returns 202 with a jobId. Poll GET /api/v1/jobs/{id}, stream it, or pass callbackUrl and callbackSecret for an HMAC-signed webhook when it finishes.
  5. 5Read line items deterministicallyPOST /api/v1/extract with `rowSelector` and `rowFields` reads every structurally-alike row off a listing by selector — no model call for the reading itself — and reports `rowsFound`.

In practice

The actual call, and what it returns.

The portal that will not let software sign in gets a human, once. Everything after that is an async job against a restored session.

Connect + sweepsupplier-sweep.shbash
# 1 — the portal hard-blocks automated sign-in, so a HUMAN does it once.
curl https://twin-browser.com/api/v1/connect/sessions \
  -H "Authorization: Bearer $TWIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "host": "portal.supplier.example.com", "account": "acme-supplies" }'

# → 201 { "id": "…", "url": "https://twin-browser.com/connect/…",
#         "host": "portal.supplier.example.com", "account": "acme-supplies",
#         "expiresAt": "…", "ttlMinutes": 30 }
#
#   Send that url to the person who owns the login. Check it landed:
#   GET /api/v1/connect/sessions → status: pending | active | connected | failed

# 2 — every later run restores that session. No password, no 2FA.
curl https://twin-browser.com/api/v1/jobs \
  -H "Authorization: Bearer $TWIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://portal.supplier.example.com/invoices",
    "prompt": "list every invoice issued this month with its number, date and total",
    "success": { "kind": "extracted" },
    "account": "acme-supplies",
    "persistSession": true,
    "callbackUrl": "https://ops.example.com/hooks/twin",
    "callbackSecret": "…"
  }'

# → 202 { "jobId": "…" }      poll GET /api/v1/jobs/{id}

What this call does

  • The connect link is single-use, scoped to one host, and expires in about thirty minutes — only the resulting session is kept, never the password.
  • `account` is the label that keys both the credential and the session, so thirty-eight portals are thirty-eight labels against one API key.
  • The job settles credits on its first terminal status; POST /api/v1/jobs/{id}/cancel aborts a running job with a full refund.
  • The completion webhook is HMAC-signed with `callbackSecret`, so your endpoint can verify the payload came from Twin.
Every endpoint, every field

What it costs

Priced per action, not per seat.

A portal sweep is priced per run, not per portal or per seat. Adding the thirty-ninth supplier costs one more run, not a new licence.

Credit cost of the actions this solution uses
ActionCreditsWhat you get
POST /jobs — an async sweep10 / portal runSubmitted, run in the background, settled on its terminal status.
POST /run — a synchronous check10 / runFor a single portal you want an answer from right now.
POST /extract — read the rowsmeteredMetered: the higher of a flat floor and the actual model cost. Deterministic `rowFields` reading incurs no model cost at all.
POST /skills/{name}/run — a compiled sweep1 / portal runOnce a portal’s flow is compiled, its daily sweep replays at the flat replay rate.

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.
  • Connecting an account is not itself a billed action — you pay for the runs that use the session, not for storing it. GET /api/v1/pricing serves the live rate card.
The full rate card

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.

A connected session is not permanent

Portals expire sessions, and some force a fresh sign-in on a schedule no automation can satisfy. When that happens the run returns a connect-required result with a fresh link rather than pretending — which means somebody has to click it. Budget for that on the strictest portals.

It does not hold your suppliers’ passwords for them

Where the login belongs to someone whose credentials you should not possess, the connect link is the whole point: they sign in, Twin captures the session, and no password enters your systems. If you were hoping to store their credentials centrally, this deliberately does not do that.

It does not normalize what the portals give you

Twin returns what the page says — the numbers, the rows, the document. Mapping forty suppliers’ idea of "invoice total" onto your chart of accounts is your ERP’s job, not the browser layer’s.

Under the hood

The primitives this runs on.

Nothing here is specific to this problem — the same mechanisms carry every solution on the site.

Over MCP, the same work is these tools

  • connect_account

    Get a one-time link that lets a HUMAN sign into a site by hand, so Twin Browser captures the session and later runs are already logged in. Use it when a run returns code:"credential_missing", when it returns code:"credential_rejected" and you have no better credential to supply (the one stored is wrong — a retry re-types it), or when a site hard-challenges automated logins from datacenter IPs and simply cannot be signed into by an agent. Never ask the user to paste a password — send this link. Re-run the task afterwards and it restores the session (sessionRestored:true).

  • list_connections

    Check whether the user finished a connect link you sent them (pending | active | connected | failed | expired) before re-running the task.

  • list_accounts

    List the stored logins you already have as { label, host, emailPreview } — masked, never a credential. Pass a returned label as account on run_goal/run_skill and that login plus its own saved session are applied automatically. Nothing back for a host? Use connect_account to have the user sign in once.

  • submit_run

    Submit 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.

  • extract

    Read an authorized page and return structured JSON matching the fields or JSON schema you request.

Every MCP tool

FAQ

Supplier and customer portalscommon questions.

What if a portal refuses to let an agent sign in at all?
That is what POST /api/v1/connect/sessions is for. It returns a single-use link scoped to one host; a person completes the sign-in in a hosted browser and the resulting session is captured. Later runs restore it and report sessionRestored: true.
Can we keep several logins for the same supplier?
Yes. Logins are labelled, and both the credential and the stored session are keyed by that label, so one host can carry a service login and a named buyer’s login side by side. Pass the label as `account` on the run.
How do we get the result into our ERP?
Either poll GET /api/v1/jobs/{id}, or pass callbackUrl and callbackSecret when you submit the job and receive an HMAC-signed webhook on completion. There is no ERP-specific connector — the payload is JSON and the mapping is yours.
Is any of this stored where a supplier could see it?
No. Credentials and sessions are encrypted and isolated per tenant under default-deny row-level security, and a credential leaves the system exactly once — at fill time, inside the browser.

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.