# Authentication

Every call that runs a model or reads your requests needs an API key. The catalog (`GET /models`,
`GET /categories`), `POST /quote` and the OpenAPI document are public.

## API keys

Create and revoke keys at [https://doitong.com/developers/dashboard#keys](https://doitong.com/developers/dashboard#keys). A key looks
like `doitong_` followed by 64 hex characters and is shown only once, when you create it. Send it in the
`Authorization` header:

```bash
curl https://api.doitong.com/v1/account -H "Authorization: Bearer $DOITONG_API_KEY"
```

`X-API-Key: <key>` works as well.

> [!WARNING]
> Keep keys on your server: anyone who has a key can spend the API credits it is allowed to spend. If a key leaks,
> revoke it and create a new one.

A request without a key gets `401` with code `missing_api_key`; an unknown, revoked or expired key gets `401` as well.

## Scopes

Each key carries scopes. The model API checks these:

| Action | Scope needed |
|---|---|
| Run a model, cancel a request, delete a request's files, change the storage setting | `models:run` (keys with the older `images:create` or `videos:create` also work) |
| Create an upload URL | `models:run` or `media:upload` |
| Read requests, account | `models:read` (a key that may run models reads too) |

New keys get `models:read` and `models:run` by default. A call without the right scope gets `403` with code
`insufficient_scope`.

## Credit budgets

A key can carry its own budget in API credits (1,000 API credits = $1), set by the account owner: a total budget, a
daily budget (last 24 hours), or both. Before a run is charged, the cost is checked against what the key has already
spent through this API (net of refunds). A run that would exceed it is refused with `402 budget_exceeded` and nothing
is charged.

> [!IMPORTANT]
> A budget refusal is final: retrying will not help until the owner raises the budget.

 A key without a budget can
spend up to the API balance. Separately, a run that the API balance cannot cover gets `402 insufficient_credits`
with a `top_up_url`.

`GET https://api.doitong.com/v1/account` shows the API balance and the key's remaining budget (both in API credits):

```json 200 OK · account
{
  "object": "account",
  "credits": 125000,
  "balance": { "credits": 125000, "usd": "125.00" },
  "top_up_url": "https://doitong.com/developers/dashboard#balance",
  "key": {
    "name": "Production server",
    "prefix": "doitong_3fa9c1e...",
    "scopes": ["models:read", "models:run"],
    "budget_remaining": 50000,
    "rate_limit": 3600,
    "rate_limit_remaining": 3571
  },
  "webhook_signing_secret": "whsec_..."
}
```

`budget_remaining` is `null` when the key has no budget of its own.

## Rate limits

Each key may make 3600 requests per hour unless its limit was set differently. Every authenticated response carries
the current state:

| Header | Meaning |
|---|---|
| `X-RateLimit-Limit` | Requests allowed in the current window |
| `X-RateLimit-Remaining` | Requests left in the current window |
| `X-RateLimit-Reset` | Unix time when the window resets |

Over the limit you get `429` with a `Retry-After` header (seconds). Long polling with `?wait=` counts as one request
per call, so waiting is cheaper than polling quickly.

There is also a limit of 10 requests in progress (`queued` or `running`) per account. Starting one more returns
`429 too_many_inflight`; start it again after one finishes.
