Docs
Orgs: run Unbrowse for your own users
For teams that build agents for other people: agencies, forward-deployed engineers, and products with an AI assistant. You hold one account, one key and one bill. Each of your users gets their own logins and sessions, and a site one of them teaches Unbrowse works for all of them.
Console: unbrowse.ai/app/org.
How it works
| You | Unbrowse |
|---|---|
| Create an org (name, the app name your users see, optional company domain) | Makes you the owner; teammates who sign in with that domain join |
| Mint an org key and put it on your server | Stores only its hash; shows it once |
Send each call with the org key and X-Unbrowse-End-User: <your user's id> | Runs it in that user's own workspace, created on first use, the same one every time |
| Forward a sign-in link to the right user when a site needs their login | Shows "Your App wants to use your site account", seals the login in that user's workspace, then the link stops working |
| Top up once | Charges every user's verified calls to the org: free allowance first, then paid credits |
Your users never create an Unbrowse account. You never handle their passwords, and neither does your model.
1. Create the org and a key
Sign in at https://unbrowse.ai/app/org, create the org, then press Mint org key. Or over REST, signed in or with a personal key:
curl -X POST https://unbrowse.ai/api/v1/org \
-H "Authorization: Bearer $PERSONAL_KEY" -H "content-type: application/json" \
-d '{"name": "Acme", "appName": "Acme Assistant", "domain": "acme.com"}'
curl -X POST https://unbrowse.ai/api/v1/org/keys -H "Authorization: Bearer $PERSONAL_KEY"
# → { "id": "key_…", "prefix": "ub_live_…", "token": "ub_live_…" } (shown once)domainis optional. It must match the domain you signed in with, cannot be a free-mail domain (gmail.com, outlook.com, …), and belongs to one org only.- Only a person mints org keys. An org key calling
POST /org/keysgets 403, so a leaked key cannot mint its own replacements.
2. Call as your user
MCP (Streamable HTTP):
{
"mcpServers": {
"unbrowse": {
"url": "https://unbrowse.ai/mcp",
"headers": {
"Authorization": "Bearer ub_live_…",
"X-Unbrowse-End-User": "user_8421"
}
}
}
}REST:
curl https://unbrowse.ai/api/v1/runs \
-H "Authorization: Bearer $ORG_KEY" \
-H "X-Unbrowse-End-User: user_8421" \
-H "content-type: application/json" \
-d '{"task": "my open pull requests on github", "idempotencyKey": "pr-1"}'- The user id is yours: 1–128 characters from letters, digits and
_ . : @ -. Use a stable internal id, not an email you might change. - The same id under two different orgs is two different people.
- Without the header, the org key acts as the org itself (useful for testing; the org's own logins are not any user's).
- The header is
X-Unbrowse-End-User. NotX-Unbrowse-User: the edge strips that one from every request (it carries Unbrowse's own signed sign-in).
Clients that cannot send headers (some hosted MCP clients): mint a key for one user and put it in that user's config.
curl -X POST https://unbrowse.ai/api/v1/org/users \
-H "Authorization: Bearer $ORG_KEY" -H "content-type: application/json" -d '{"user": "user_8421"}'
# → { "workspaceId": "ws_…", "user": "user_8421", "token": "ub_live_…" }A per-user key only ever acts as that user. It cannot select another user with the header.
3. When a site needs your user's login
A run refused for sign-in, where the user has no saved login for that site, returns a link instead of guessing:
{ "status": "failed",
"error": { "code": "session_or_permission", "message": "…" },
"signIn": { "url": "https://unbrowse.ai/connect/cr_…", "requestId": "cr_…", "origin": "https://github.com",
"message": "…" } }(mfa_required is the other code that carries signIn.)
unbrowse_credentials_request {origin, reason} creates one on demand; unbrowse_credentials_status {requestId} reports pending, fulfilled, declined or expired.
Send the link to that user (chat, email, in-app). The page says "Acme Assistant wants to use your github.com account" with the reason. They sign in there, or press Don't connect. Once unbrowse_credentials_status says fulfilled, call again with the same input.
- The link is the only authority: the user needs no Unbrowse account. It works once and expires after 30 minutes.
- The login is sealed (AES-256-GCM, per-workspace key) in that user's workspace only. Your other users, your org key and your model never see it; runs fill it into the site's own login form.
- A wrong, used, declined or expired link saves nothing.
- Kept sessions are reused, so the user is not asked again until the site logs them out.
4. One bill
- Every verified call by any of your users is charged to the org: the org's monthly free allowance first, then its paid credits (1 credit = one verified call = $0.001). Failed, refused and challenged calls are free.
- When both are empty, calls get
402 quota_exceededwith a top-up link. Adding user ids never adds allowance. - Top up from
/app/org(Stripe packs of $10, $50, $200). The org page shows calls per user this month. GET /api/v1/usageas a user returns the org's balance withpaidBy: "org"and that user's own calls underown.
5. Tools your users share
When one of your users runs a site Unbrowse had to learn, the route is shared with your other users, so their first call is fast too:
- Only read-only routes that need no login or secret are shared, scrubbed exactly as for the public registry: no recorded traces, no search terms or other user values, no cookies.
- Shared tools appear as
org__<site>__<tool>in discovery, tool lists and/api/v1/sites/<host>, for your users only. Other orgs and everyone else never see them. - They reach the public registry only if you turn on Also share them with everyone on Unbrowse (
POST /api/v1/org/settings {"sharePublic": true}). Off by default. - Anything that signs in or changes a site stays in the one user's workspace.
REST reference
| Method | Path | Caller | Does |
|---|---|---|---|
| POST | /api/v1/org {name, appName?, domain?} | signed-in person | create; you become owner (one org per person) |
| GET | /api/v1/org | member or org key | org, members, users (calls, connected sites), shared tools, keys, balance |
| POST | /api/v1/org/keys | member | mint an org key (403 for an org key) |
| POST | /api/v1/org/keys/revoke {id} | member or org key | revoke an org or per-user key of this org |
| POST | /api/v1/org/users {user} | member or org key | key for one user |
| POST | /api/v1/org/settings {sharePublic} | member | public sharing of org tools |
| GET | /api/v1/connect/:id | anyone with the link | app name, site, reason, status |
| POST | /api/v1/connect/:id {username?, email?, password, totp?} | anyone with the link | save the login once |
| POST | /api/v1/connect/:id/decline | anyone with the link | decline |
Every other endpoint (/runs, /sites/..., /usage, MCP tools) takes the org key plus X-Unbrowse-End-User.
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthorized | missing, wrong or revoked key |
| 403 | forbidden | X-Unbrowse-End-User with a key that is not an org key; an org key minting keys or changing settings |
| 404 | no_org | you are not in an org yet |
| 409 | already_member / domain_taken / request_closed | one org per person; domain claimed; connect link used, declined or expired |
| 422 | invalid_request | malformed user id, bad or free-mail domain, missing password |
| 402 | quota_exceeded | the org's free and paid credits are both empty |
Not yet
Leaving an org, removing a member, transferring ownership, per-user spending caps, custom domains, and hiding the Unbrowse name. The connect page says "secured by Unbrowse".