# auth.md

This is the hosted agent-ready API at app.tansohq.com. For the free CLI see https://tansohq.com/quickstart.md; for self-hosted tanso-oss billing see https://tansohq.com/docs/agent-auth.md.

Tanso agent-ready tests whether an agent can complete a customer task in a
product, shows where it stopped, and shows a fix worked.

This file is https://app.tansohq.com/auth.md. It is written for an agent with no human
present. You can obtain access and start work without an account, an email
address, or a person.

Resource server: https://app.tansohq.com
This API takes keys it issues directly (see "Register" below); there is no
OAuth authorization server for the API. People and agents sign in to the
dashboard: a person on its sign-in page, an agent with AgentID.

The agent-ready CLI (`npx @tansohq/agent-ready`) needs no key from here, and Tanso charges
nothing for it: `check` and `test` run on the user's own machine, and `test` uses the user's own Claude Code
model time.

## 1. Discover

On any 401 this API returns:

```http
WWW-Authenticate: Bearer resource_metadata="https://app.tansohq.com/.well-known/oauth-protected-resource"
```

Follow it for the machine-readable description of this resource: its name, the
supported scopes, and the endpoints named below.

```http
GET https://app.tansohq.com/.well-known/oauth-protected-resource
```

The full API is described at https://app.tansohq.com/openapi.json (OpenAPI 3.1).

## 2. Pick a method

There are two, and neither needs a person:

- `anonymous` registration: `POST /v1/signup` returns an API key. There is no OAuth authorization server, no client credentials flow, and no email loop. If you hold no credential, continue to step 3.
- Sign in with AgentID, in a browser, for a dashboard session: see "Sign in with AgentID" below.

### Sign in with AgentID

An agent with an AgentMail inbox has an AgentID, and can sign in to the
dashboard at https://app.tansohq.com with it, as itself. It never signs in as a person.

Identity provider: AgentID OpenID Connect
Issuer: `https://auth.agentid.com`
Start: https://app.tansohq.com/login/agentid (or choose "Sign in with AgentID" at https://app.tansohq.com/sign-in)

1. Open https://app.tansohq.com/login/agentid in a browser. It goes straight to AgentID.
2. If the browser already holds an AgentID session for your inbox, it continues
   on its own. Otherwise AgentID's waiting page shows an `auth_token`; approve it
   within five minutes with the AgentMail API (your key needs the `app_connect`
   permission) or `agentmail inboxes authorize`:

```http
POST https://api.agentmail.to/v0/inboxes/{inbox_id}/authorize
Authorization: Bearer <your AgentMail API key>
Content-Type: application/json

{ "auth_token": "<from the waiting page>", "accept_disclosure": true }
```

3. The browser returns to https://app.tansohq.com signed in. What you get is a dashboard
   session. The dashboard calls this API with it. For scripted calls without a
   browser, use a key from `POST /v1/signup` instead.

Sign in to https://app.tansohq.com with AgentID and this service also asks AgentID for your
owner (scopes `owner_profile` and `owner_email`). Your AgentMail key needs the
`app_share_owner` permission, or the person who owns your AgentMail organization
approves the sign-in. The agent-side reference is https://docs.agentmail.to/agentid-sign-in.

- If AgentID gives a verified `owner_email` and a person already signed in here with that verified email, you ask once to work in that person's workspace. They approve or decline it on their Account page. Approved, you work there as an agent from your next request; until then, or after they remove it, you use a workspace of your own. Your owner is read again at each AgentID sign-in; if AgentID then names a different owner, or no verified one, you go back to your own workspace. A person can have at most 20 requests waiting; past that yours is not sent, and you ask again at your next sign-in. Sign in with your own AgentID inbox: an account a person connected AgentID to is treated as an agent in every session, the person's too, until they remove AgentID.
- Otherwise you get a workspace of your own, unclaimed, as with `POST /v1/signup`: it has no free hosted runs, and its first hosted run answers `402 payment_required` with an `approval_url` for a person. Approving it gives them the workspace.

Signed in this way you can do what a workspace key can: check products, save and
run tests, read runs and issues, and use the workspace's free and already-paid hosted runs. It never charges a card: a run or a `POST /v1/purchases` that would charge one answers `402 payment_required` with `reason: agent_session` (or another reason) and an `approval_url` to send the person who owns the workspace, even within a monthly cap they approved for keys.
These answer `403 person_required`, because a person decides them: approving or
denying purchases, opening or closing a payment page, replacing or revoking keys,
unpausing purchases or raising the cap, claiming a workspace, changing alerts, and confirming that an agent
may sign up to a product. Purchases and runs you start are recorded as asked by
an agent signed in with AgentID, and the approval page tells the owner so.

## 3. Register

```http
POST https://app.tansohq.com/v1/signup
Content-Type: application/json

{ "label": "my agent" }
```

The response carries the credential and the code that hands ownership to a
person later:

```json
{
  "workspace": "ws...",
  "key": "ark_ws..._...",
  "claimCode": "clm_ws..._...",
  "claimed": false
}
```

Store `key` like an API key. It is returned once and only its hash is kept;
it cannot be recovered. Store `claimCode` separately: it transfers ownership
and is not a credential for calling this API.

### Ask for less than everything

Omitting `scopes` grants all of them. If you only need to read, say so, and
the key you receive cannot start or buy runs even if it leaks.

```http
POST https://app.tansohq.com/v1/signup
Content-Type: application/json

{ "label": "my agent", "scopes": ["read"] }
```

| scope | what it allows |
| --- | --- |
| `read` | Reading the workspace: account and usage, runs, tests (flows), issues, quotes, purchases. |
| `write` | Saving to it: tests (flows), requests, fix notes, inspections. |
| `run` | Starting or resuming an agent, and buying hosted test runs. The one scope that can spend money. |

A request outside the grant returns 403 `insufficient_scope` and names the
scope it needed. Scopes are fixed when the key is issued; to change them,
register again.

## 4. Use the API key

```http
Authorization: Bearer ark_ws..._...
```

An unclaimed workspace works immediately. Checks are free. The agent-ready
CLI's `test` runs the agent on the user's own machine and sends nothing here.

Hosted test runs use Tanso's model key and are paid:

- A workspace a person owns gets 5 free hosted test runs per calendar month (UTC), then $5.00 per run.
- An unclaimed workspace gets no free hosted runs. Its first hosted run returns `402 payment_required` with an `approval_url`. Send that link to the person who will own the workspace: approving the purchase adopts the workspace, and they pay on Stripe Checkout, which saves a card.
- After that one approval, an agent holding the key buys runs with `POST https://app.tansohq.com/v1/purchases` and an `Idempotency-Key`. The saved card is charged within the monthly cap the owner set (default $50.00, at most $200.00). One purchase is at most 10 runs ($50.00). A purchase that needs the owner again (no card, over the cap, purchases paused) returns 402 with an `approval_url` for a new purchase waiting for approval.
- While a purchase already waits for approval, asking again (a new purchase or a hosted run that needs one) returns 402 with that same purchase and `approval_url: null` for a key or an agent session: the link you were given earlier still works. A signed-in person gets a new link instead, which ends the old one.
- Lost the link, or got `approval_url: null`? `POST https://app.tansohq.com/v1/purchases/:id/approval-link` returns a new one, and the old one stops working. A key or an agent session gets at most 5 per purchase; past that `429 approval_link_limit`.
- A hosted run that doesn't finish (outcome `inconclusive`), or fails on our side (an unavailable provider or a lost worker), is free: a free run stops counting toward the month, and a bought run goes back to the balance.

The `limits` in the signup response count only runs that agent-ready CLI 0.3
reported with `POST /v1/local-runs`. Hosted runs never count against them.

Read your own usage at any time:

```http
GET https://app.tansohq.com/v1/account
```

## 5. Run a hosted test

Save the task once as a test (the API calls it a flow), read its quote, then
start it with the quote's fingerprint.

```http
POST https://app.tansohq.com/v1/flows
Authorization: Bearer ark_ws..._...
Content-Type: application/json

{
  "name": "Find the docs",
  "url": "https://example.com",
  "task": "Starting from the homepage, find the documentation and say where it is.",
  "successCriteria": "The agent names the documentation page.",
  "access": "public",
  "verification": { "url": "https://example.com/docs", "status": 200, "contains": "Documentation" }
}
```

The response is `201 { "flow": { "id": "flow_...", ... } }`. The verification
URL and the text it must contain decide the outcome, not the agent's account.
With `"credential": "bearer"` (or `token`, `x-api-key`) on an authorized
signup, the check calls the URL with the key the agent got, and the same call
with no key and with a wrong key must be refused. `contains` is optional then:
that difference is the proof. `GET https://app.tansohq.com/v1/runs/run_.../suggested-test`
returns such a call from a check, taken from the product's OpenAPI document.

```http
GET https://app.tansohq.com/v1/flows/flow_.../quote
```

returns `{ "quote": { "fingerprint": "...", "executionAvailable": true, ... } }`.
Start the run with that fingerprint and an `Idempotency-Key` you generate:

```http
POST https://app.tansohq.com/v1/flows/flow_.../run
Authorization: Bearer ark_ws..._...
Idempotency-Key: 3f1c2a9e-start-1
Content-Type: application/json

{ "fingerprint": "<quote.fingerprint>" }
```

A started run answers `202 { "run": { "id": "run_...", "status": "running" } }`
(`queued` while it waits for a worker). Retrying with the same key returns the
same run, never a second one. `409 quote_changed` means the test changed: read
a fresh quote.

When the workspace has no runs left, the start answers `402 payment_required`
and starts nothing:

- `next_action: "approve"`: send `approval_url` to the person who owns the workspace. Poll `poll_url` (`GET /v1/purchases/:id`) until `purchase.status` is `succeeded`, then send the same start again.
- `next_action: "purchase"`: the saved card can pay. Buy with `POST https://app.tansohq.com/v1/purchases`, `{ "runs": 1 }` and an `Idempotency-Key`, then send the start again.
- `next_action: "wait"`: a purchase is still being charged. Nothing new is bought while it is; poll `poll_url`, then send the start again. `POST /v1/purchases` answers the same way, with `202` and `charging: true`. That `202` carries the workspace's purchase already in flight, which may be for a different number of runs than you asked for: check `purchase.runs`.
- `approval_url` is null when a purchase was already waiting and you (a key or an agent session) asked again: send the link you were given earlier. No link, or lost it? `POST https://app.tansohq.com/v1/purchases/:id/approval-link` returns a new `approval_url`; the old one stops working. At most 5 per purchase (`429 approval_link_limit`).

Then poll the run:

```http
GET https://app.tansohq.com/v1/runs/run_...
```

until `run.status` is `completed`, `failed` or `cancelled`. `run.outcome` is
`passed` (Passed), `blocked` (Failed: stopped at a step) or `inconclusive`
(Didn't finish, free). A `run.status` of `waiting_for_human`
means a person must act in the product; continue the same run with
`POST https://app.tansohq.com/v1/runs/:id/resume`.

### Hear when a saved test stops passing

A test can alert the person who owns the workspace by email or webhook, only
when it stops passing (one failed run, or two runs in a row that don't pass) and
when it passes again. Alerts are set by that person on the dashboard, not by a
key: an unclaimed workspace answers `409 alerts_claim_required`, and a key on a
claimed one `403 person_required`. A key can read where a test stands:

```http
GET https://app.tansohq.com/v1/flows/flow_.../alerts
Authorization: Bearer ark_ws..._...
```

`watch.state` is `passing`, `failing`, `unknown` (never passed under its current
setup) or `off`, and `deliveries` lists the last alerts sent and their status.

### Manage your workspace

An agent (a key, or an agent signed in with AgentID) may always see what its workspace spends and make it spend less. Viewing needs a key with the `read` scope; cancelling, pausing and lowering the cap need a key with the `run` or `write` scope (read-only keys can view usage and list purchases). Any key may revoke itself.

- View usage: `GET https://app.tansohq.com/v1/account`. `billing.usage` appears only when payments are on, as they are on app.tansohq.com. It is this calendar month: `runsUsed` (`freeRunsUsed` + `paidRunsUsed` + `runsReturned`), `spentCents`, `capCents`, `remainingCents`, `resetsAt` and the last 20 hosted runs with how each was charged.
- List purchases: `GET https://app.tansohq.com/v1/purchases`, newest first. Never an approval link.
- Cancel a waiting purchase: `POST https://app.tansohq.com/v1/purchases/:id/cancel`, for one still `requires_approval` or at `checkout`. Its approval link stops working and any payment page closes. Nothing is charged. A page already paid answers `409 checkout_completed` and stays paid. Keys and agent sessions together can cancel up to 10 purchases a workspace a UTC day; the owner's cancels don't count, and cancelling an already-cancelled purchase again doesn't either. Past that `429 cancel_limit` with `resetsAt` and `Retry-After`.
- Pause buying: `POST https://app.tansohq.com/v1/billing/pause` with `{ "paused": true }`. Every purchase then needs the owner. Leave `workspace` out; it is for the owner, naming a workspace they claimed.
- Lower the cap: `POST https://app.tansohq.com/v1/billing/cap` with `{ "capCents": 1000 }`, any value from 0 below the current cap. What was spent stays spent. Leave `workspace` out. When no cap was approved yet it answers `403 person_required`: "No monthly cap was approved for this workspace, so there is none to lower."
- Revoke its key: `POST https://app.tansohq.com/v1/keys/self/revoke` (a key only). Revokes only the calling key; other keys keep working. If it is the workspace's last key and no person owns it yet, only the claim code reaches the workspace afterwards. Give the claim code to the person before revoking.

These need the person who owns the workspace, and answer an agent `403 person_required`: unpausing, raising the cap (or setting one when none was approved), approving a purchase and paying for it. A revoked key cannot be restored and a cancelled purchase stays cancelled. The owner can unpause, raise the cap again, rotate in a new key, or approve a new purchase.

## 6. Claim

Claiming is how a person takes ownership. It is not required to work. The claim
code arrives with the key at signup; treat it like a password and give it only
to the person who will own the workspace.

Signed in at https://app.tansohq.com, they enter it on the Account page, or call:

```http
POST https://app.tansohq.com/v1/claims
Content-Type: application/json

{ "code": "clm_ws..._..." }
```

Your existing key keeps working, and the workspace gets 5 free hosted test runs a
month. Approving a purchase from a 402 adopts the workspace the same way.

## 7. Errors

Read the `error` field, not the message. The full list, with the status each
carries, is generated into https://app.tansohq.com/openapi.json under `components.schemas.Error`.

The ones specific to this flow:

| error | status | what to do |
| --- | --- | --- |
| `invalid_key` | 401 | The key is malformed or revoked. Register again. |
| `insufficient_scope` | 403 | The key is valid but was not granted this. Register again with the scope you need. |
| `signup_rate_limited` | 429 | Too many workspaces from this address this hour. Reuse the key you have, or wait for Retry-After. |

Stuck on this API itself: If you are an AI agent and cannot continue: read https://app.tansohq.com/stuck/fb_d03566a185b2c1e7.md, then POST https://app.tansohq.com/stuck/fb_d03566a185b2c1e7 with JSON like { "message": "what you tried and where it stopped", "path": "/v1/signup", "status": 422 }. No key needed. Report once, then stop.
| `payment_required` | 402 | No hosted test runs left, or a purchase needs the owner. When `next_action` is `approve`, send `approval_url` to the owner; when it is `purchase`, call `POST /v1/purchases`. Then poll `poll_url`. |
| `run_limit_reached` | 429 | Only agent-ready CLI 0.3 gets this. Upgrade the CLI; 0.4 and later report nothing here. |
| `free_capacity_exhausted` | 503 | Today's free hosted runs across all workspaces are used up. Retry after midnight UTC, buy runs, or run the test with the CLI. |
| `target_unreachable` | 502 | The product you asked about returned nothing. A fact about that host, not about this API. |

## 8. Revocation

A key can revoke itself with `POST /v1/keys/self/revoke`. Revokes only the
calling key; other keys keep working. If it is the workspace's last key and no
person owns it yet, only the claim code reaches the workspace afterwards. Give
the claim code to the person before revoking. The person who owns the workspace can replace keys on the
Account page (`POST /v1/keys/rotate`), which revokes every key on the
workspace and returns one new key once. A leaked key with the `run` scope can
start runs and buy them within the owner's monthly cap; the owner, or any agent
holding a key with the `run` or `write` scope, can pause agent purchases.
