If you are embedding browser automation inside your own product, your customers are your tenants, not ours. Sub-tenant provisioning makes that structural rather than something you simulate with one shared key and your own bookkeeping. This guide covers the one call that creates a customer, the billing decision you cannot change afterwards, and the four errors that mean you got something wrong.
You need a scoped key first
Provisioning requires the tenants:provision scope. A default key carries run, skills:read and skills:write and nothing else, so calling POST /api/v1/tenants with one is a 403 rather than a silent failure. That separation is the point: the key your product runs on should not be able to create billing relationships.
Decide the billing mode before you call
It is fixed at creation. "self" gives the sub-tenant its own balance, which you can seed with an opening grant transferred from yours — they run out when they run out. "parent" bills your pool for everything they do, with each charge attributed back to the sub-tenant that made it. Choose "self" for a self-serve product where the customer buys credits; choose "parent" when browser automation is an included feature of your own plan.
Provision the customer
One call creates the tenant, mints its key and fixes the relationship. The API key appears once, in the 201 response, and there is no endpoint that returns it again — write it to your own secret store as you read the response.
curl -X POST https://twin-browser.com/api/v1/tenants \
-H "Authorization: Bearer $TWIN_RESELLER_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": "ab_live_…", ← shown ONCE
# "granted": 0
# }
# Self-billed with an opening grant from your balance instead:
# { "name": "Acme Corp", "billing": "self", "credits": 5000 }The four errors, and what each means
Each of these is a distinct mistake, and the status code tells you which.
- 403 — your key lacks the tenants:provision scope. Use the reseller key.
- 400 — `credits` on a parent-billed sub-tenant. An opening grant only makes sense when they have their own balance.
- 402 — you do not have the credits to fund the grant you asked for. The grant is a transfer from your balance.
- 409 — nested resellers. A sub-tenant cannot provision sub-tenants of its own; the hierarchy is one level deep.
Reconciling the ledger
GET /api/v1/tenants lists your sub-tenants with their balances and what they have billed to you. Under "parent" billing that second number is your cost of goods for that customer, and it is the input to whatever you invoice them — the platform does not set per-sub-tenant pricing, so your margin is the difference between the rate card and your own.
curl "https://twin-browser.com/api/v1/tenants" \
-H "Authorization: Bearer $TWIN_RESELLER_KEY"
# { "subtenants": [
# { "id":"…", "name":"Acme Corp", "billing":"parent",
# "balance": 0, "billed_to_parent": 1240 },
# { "id":"…", "name":"Globex", "billing":"self",
# "balance": 3760, "billed_to_parent": 0 }
# ] }What a sub-tenant does and does not share with you
A sub-tenant is a tenant: its own API key, skill library, credential vault and run history, isolated under default-deny row-level security. It cannot see your skills or secrets and you cannot see its vault. The only surface that crosses any tenant boundary anywhere is the sanitized shared skill corpus, which carries action structure and nothing else.
Making their runs cheap is your margin
Under "parent" billing, every credit a sub-tenant spends is yours. That makes the cost engine a margin lever rather than a nice-to-have: routing their work through dispatch so repeat intents replay at 2 credits instead of compiling at 10 is a direct reduction in what you pay. GET /api/v1/cache/stats on each sub-tenant's key tells you where that is and is not happening.