> Documentation index: https://unbrowse.ai/llms.txt. Fetch it to find every page.

# 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](https://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:

```sh
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)
```

- `domain` is 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/keys` gets 403, so a leaked key cannot mint its own
  replacements.

## 2. Call as your user

MCP (Streamable HTTP):

```json
{
  "mcpServers": {
    "unbrowse": {
      "url": "https://unbrowse.ai/mcp",
      "headers": {
        "Authorization": "Bearer ub_live_…",
        "X-Unbrowse-End-User": "user_8421"
      }
    }
  }
}
```

REST:

```sh
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`. **Not** `X-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.

```sh
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:

```json
{ "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_exceeded` with 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/usage` as a user returns the org's balance with `paidBy: "org"` and that user's own calls under `own`.

## 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".
