# API tokens

<!-- docs-test setup: owner -->

An API token lets a program -- a script, a CI job, an AI agent -- use the API as a person with the same scopes
could, including making and revoking other tokens when it holds `tokens:write`. Send it as
`Authorization: Bearer us_live_...`. A token belongs to the workspace it was made in; it never needs (or accepts)
a workspace header.

- **Shown once.** The response that creates a token is the only time it is shown; UsherStats keeps a SHA-256 of it.
  Lists show the first characters (`prefix`) so you can tell tokens apart.
- **Scopes** (`module:action`): `analytics:read`, `sites:read`, `sites:write`, `bot:read`, `bot:write`,
  `search:read`, `search:write`, `members:read`, `members:write`, `tokens:read`, `tokens:write`, `billing:read`,
  `billing:write`, `export:read`, and `account:admin` (everything). A call without the scope it needs gets `403`
  naming the scope.
- **Site restriction.** `siteIds` limits a token to those sites; every other site is `404` to it, as if it did not
  exist. A site-restricted token cannot create sites or manage members.
- **Expiry.** `expiresAt` (ISO 8601) ends it; a revoked or expired token gets `401` saying which.
- **Never wider than its maker.** A token can only be given scopes, sites and a lifetime its creator has. Tokens a
  person made are revoked when they leave the workspace, and those exceeding a new role when they are demoted.

| Route | Scope |
|---|---|
| `GET /v1/tokens` | `tokens:read` |
| `POST /v1/tokens` | `tokens:write` |
| `DELETE /v1/tokens/{id}` | `tokens:write` |

The examples assume `$US_TOKEN` holds a token with `account:admin`.

## Create a token

```sh
curl https://api.usherstats.com/v1/tokens \
  -H "Authorization: Bearer $US_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "nightly report", "scopes": ["sites:read", "analytics:read"], "expiresAt": "2027-01-01T00:00:00Z"}'
```
<!-- expect 201; NEW_TOKEN=.token; NEW_TOKEN_ID=.id -->

The response has the token in `token` (`us_live_` and 40 letters and digits) with `"shownOnce": true`. Store it now.

## Use it

```sh
curl https://api.usherstats.com/v1/me \
  -H "Authorization: Bearer $NEW_TOKEN"
```
<!-- expect 200 -->

`/v1/me` answers with the token's AuthContext: its workspace, scopes and sites. A call it lacks the scope for:

```sh
curl https://api.usherstats.com/v1/tokens \
  -H "Authorization: Bearer $NEW_TOKEN"
```
<!-- expect 403 -->

## List tokens

```sh
curl https://api.usherstats.com/v1/tokens \
  -H "Authorization: Bearer $US_TOKEN"
```
<!-- expect 200 -->

## Revoke a token

It stops working at once.

```sh
curl -X DELETE https://api.usherstats.com/v1/tokens/$NEW_TOKEN_ID \
  -H "Authorization: Bearer $US_TOKEN"
```
<!-- expect 204 -->

```sh
curl https://api.usherstats.com/v1/me \
  -H "Authorization: Bearer $NEW_TOKEN"
```
<!-- expect 401 -->

## A token that makes tokens

An agent token with `tokens:write` can hand out narrower tokens, but nothing it does not hold itself:

```sh
curl https://api.usherstats.com/v1/tokens \
  -H "Authorization: Bearer $US_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "agent", "scopes": ["tokens:write", "sites:read"], "expiresAt": "2027-12-31T00:00:00Z"}'
```
<!-- expect 201; AGENT_TOKEN=.token -->

```sh
curl https://api.usherstats.com/v1/tokens \
  -H "Authorization: Bearer $AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "too wide", "scopes": ["sites:write"], "expiresAt": "2027-12-01T00:00:00Z"}'
```
<!-- expect 403 -->

```sh
curl https://api.usherstats.com/v1/tokens \
  -H "Authorization: Bearer $AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "reader", "scopes": ["sites:read"], "expiresAt": "2027-12-01T00:00:00Z"}'
```
<!-- expect 201 -->
