# Authentication

Every API call carries a secret key in the Authorization header. Keys are scoped to what they may do, and can be limited by address, model and spend.

## Send your key

Send the key as a bearer token on every request to `https://api.vizoapi.com/v1`. Requests without a valid key get HTTP 401.

```bash title="cURL"
curl https://api.vizoapi.com/v1/models \
  -H "Authorization: Bearer $VIZO_API_KEY"
```
```python title="Python"
import os, requests

res = requests.get(
    "https://api.vizoapi.com/v1/models",
    headers={"Authorization": f"Bearer {os.environ['VIZO_API_KEY']}"},
    timeout=30,
)
```
```javascript title="Node.js"
const res = await fetch("https://api.vizoapi.com/v1/models", {
  headers: { Authorization: `Bearer ${process.env.VIZO_API_KEY}` },
});
```

A key starts with `va_01` followed by 64 characters. Store it without the word `Bearer`.

> **Warning**
> Keep keys on your server. Never put a key in a web page, a mobile app, a public repository or a log. Anyone with the key can spend your Credits within its limits.

## Key types

Create keys in the console under **API keys**. The full key is shown once, when you create it.

| Key | Belongs to | Use it for |
| --- | --- | --- |
| Project key | One project | Models, generations and media assets of that project, and the organization's balance with `balance:read` |
| Billing key | The organization | Reading the balance and the ledger, read-only |

Generations, uploads and webhooks belong to the project of the key that created them. A project key cannot read another project's tasks.

## Scopes

A key may call only the operations its scopes allow. Give each key the smallest set it needs. A call outside the key's scopes gets HTTP 403 with `error.code` `scope_denied`.

| Scope | Key | Allows |
| --- | --- | --- |
| `models:read` | Project | [List models](/docs/api/listModels) and [get a model](/docs/api/getModel) |
| `generations:create` | Project | [Create a generation](/docs/api/createGeneration) and [estimate a price](/docs/api/createEstimate) |
| `generations:read` | Project | [Get a generation](/docs/api/getGeneration) |
| `generations:list` | Project | [List generations](/docs/api/listGenerations), to find a task after a lost response |
| `assets:upload` | Project | [Start](/docs/api/createAssetUpload) and [complete](/docs/api/completeAssetUpload) an upload |
| `assets:reference` | Project | Use an uploaded asset (`ast_...`) as a generation input |
| `assets:download` | Project | [Download an asset](/docs/api/downloadAsset) |
| `assets:delete` | Project | [Delete an asset](/docs/api/deleteAsset) |
| `usage:read` | Both | Read usage and limits |
| `balance:read` | Project | [Get the balance](/docs/api/getWallet) |
| `billing:read` | Billing | [Get the balance](/docs/api/getWallet) and [list ledger entries](/docs/api/listLedger) |
| `billing:export` | Billing | Export billing records |

A server that only creates videos and downloads them needs `models:read`, `generations:create`, `generations:read`, `generations:list` and `assets:download`.

## Restrict a key

Each project key can carry limits of its own. Set them when you create the key, or change them later in the console.

| Limit | What happens outside it |
| --- | --- |
| **IP allowlist**: up to 10 IPv4 or IPv6 addresses or CIDR ranges. Empty allows any address. | HTTP 403, `error.code` `ip_not_allowed` |
| **Model allowlist**: up to 200 model IDs. Empty allows every model, including models added later. | HTTP 403, `error.code` `model_not_allowed` |
| **Daily and monthly spend limits** in Credits. Days and months follow UTC. Credits held for open tasks count toward the limit. | The task closes as `failed` with `error.code` `budget_exceeded`; nothing is charged |
| **Expiry date**. | HTTP 401 `invalid_credentials` once the key has expired |

Spend limits are checked when a task is funded, so a create returns HTTP 202 first. Read the task to see a `budget_exceeded` closure, or receive it as a `generation.failed` webhook.

## Rotate a key

Keys do not rotate in place. To replace one without downtime:

1. Create a new key with the same scopes and limits.
2. Deploy it to your servers and confirm requests succeed with it.
3. Revoke the old key in the console. Revocation takes effect immediately.

Revoke a key at once if it may have been exposed. Tasks it already created keep running and stay readable with any key of the same project.

## Authentication errors

| HTTP | `error.code` | Meaning | What to do |
| --- | --- | --- | --- |
| 401 | `invalid_credentials` | The key is missing, malformed, expired or revoked. | Check the header and the key in the console. |
| 403 | `scope_denied` | The key lacks the scope for this operation. | Add the scope or use another key. |
| 403 | `ip_not_allowed` | The request came from an address outside the key's IP allowlist. | Call from an allowed address or update the allowlist. |
| 403 | `model_not_allowed` | The key may not use this model. | Add the model to the key's model allowlist. |
| 429 | `rate_limited` | Too many requests with invalid keys from your address in one minute. | Fix the key, then wait the seconds in `Retry-After`. |
