# Authentication

API keys, scopes, expiry, revocation, and the difference between test and live keys.

Every request carries an API key as a bearer token:

```sh
curl https://api.moan.hot/v1/account \
  -H "Authorization: Bearer mh_live_4Hk2pQ9..."
```

Keys are server secrets. Never ship one in a mobile app, a browser bundle or a public repository. Your own backend calls the API on behalf of your users.

## Key format

A key is `mh_live_` or `mh_test_` followed by 32 random characters. The first 12 characters (for example `mh_live_4Hk2`) are the **prefix**: the dashboard shows it, and every generation records the `api_key_prefix` that created it, so you can tell keys apart without seeing the secret.

We store only a keyed hash of each key. The full key is shown **once**, when you create it. If you lose it, create a new one.

## Test and live

The prefix selects the mode, and the two modes never see each other's data.

| | `mh_test_` | `mh_live_` |
|---|---|---|
| Charges | Never | The variant price, on success |
| Outputs | Sample files on a simulated timeline | The real effect on your input |
| Webhooks | Real, signed with the test secret | Real, signed with the live secret |
| Works while your account is in review | Yes | Reads only; creates answer `account_pending_review` |

See [Test mode](/docs/test-mode/) for how to force failures and timeouts.

## Scopes

A key carries one or more scopes. A call without the right scope answers `403 insufficient_scope`.

| Scope | Allows |
|---|---|
| `generations:write` | `POST /files`, `POST /generations`, `POST /generations/{id}/cancel` |
| `generations:read` | `GET` on generations, files, effects, prices and usage |
| `account:read` | `GET /account`, `GET /usage` |

Give each service the narrowest set it needs. A worker that only reads results needs `generations:read`.

## Expiry and rotation

A key can have an optional expiry date. After it passes, the key answers `401 api_key_expired`. To rotate without downtime:

1. Create the new key.
2. Deploy it everywhere the old one runs.
3. Revoke the old key once its "last used" time in the dashboard stops moving.

## Revocation

Revoking a key in the dashboard takes effect within seconds on every server. A revoked key answers `401 api_key_revoked`. Revocation cannot be undone.

## Disabling and deleting

Disabling a key in the dashboard stops it on the next request; calls with it answer `403 api_key_disabled` until you enable it again. Deleting a key revokes it first if it still works, then removes it from the dashboard list. A deleted key cannot be restored.

You can also rename a key or change its scopes. The change applies to the next request; the mode and expiry of a key stay as created. A revoked or expired key can be deleted but not edited, and while the account is suspended an edit can only take access away.

## IP allowlist

Optionally, restrict a key to a list of IP addresses or ranges in the dashboard. Calls from anywhere else are refused with a 401.

## Errors

| Code | Status | Meaning |
|---|---|---|
| `invalid_api_key` | 401 | No `Authorization` header, a malformed key, or an unknown key |
| `api_key_expired` | 401 | The key's expiry date has passed |
| `api_key_revoked` | 401 | The key was revoked |
| `api_key_disabled` | 403 | The key is disabled in the dashboard |
| `insufficient_scope` | 403 | The key lacks the scope this route needs |
| `account_pending_review` | 403 | A live create while your account is still in review |
| `account_suspended` | 403 | The account is suspended; only `GET /account` still answers |

Full list on the [Errors](/docs/errors/) page.
