> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pyai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> Authenticate PyAI requests with bearer API keys: pyai_test_ vs pyai_live_ environments, per-product scopes, WebSocket subprotocol auth, and key rotation.

Every request authenticates with a bearer API key.

```bash theme={null}
Authorization: Bearer pyai_live_...
```

`x-api-key: pyai_live_...` is an accepted alias for environments where setting an
`Authorization` header is awkward.

<Warning>
  Keys are opaque strings (up to 512 characters). **Never parse, split, or decode
  them.** They are self-validating and work on every PyAI surface the instant they
  are created, there is no activation or propagation delay.
</Warning>

## Environments

| Prefix | Environment | Use it for |
| - | - | - |
| `pyai_test_` | Sandbox | Evals, prototypes, CI. Works instantly against production models with hard daily caps and **no billing**. |
| `pyai_live_` | Production | Real traffic, billed against your plan and credits. |

**No signup:** `POST /v1/sandbox/keys` returns a `pyai_test_` key
in `api_key`. It works on the first call, skips the credit gate, and is bounded
by a daily unit cap. See the [quickstart](/quickstart).

Because the mint asks for no email, password or card, it is capped per source
network: **2 keys per rolling 24 hours**, after which it returns
`429 sandbox_limit_reached` until the oldest ages out. Keep
the key you mint (in CI, put it in a secret) rather than minting a fresh one per
run, and see [account-creation limits](/errors-and-limits#account-creation-limits)
if you share an egress and need the network raised.

Create a longer-lived key in the [console](https://console.pyai.com). The secret
is shown once. Store it as an environment variable, never in source control.

## Scopes

Keys carry scopes that gate which products they can call:

| Scope | Grants |
| - | - |
| `hear:transcribe` | `POST /v1/audio/transcriptions`, transcription jobs (Hear) |
| `hear:stream` | streaming transcription (`GET /v1/audio/transcriptions/stream?protocol=pyai-hear-v1`) |
| `hear:configure` | `GET`/`PUT /v1/hear/vocabulary` (stored Hear vocabulary) |
| `speak:synthesize` | `POST /v1/audio/speech` (Speak) |
| `speak:clone` | `/v1/voice/clones` (Clone). Signup keys and sandbox mint keys both include it. |
| `speak:design` | `/v1/voice/design` (prompt-to-voice, saved into the Speak catalog) |
| `omni:session` | Omni realtime sessions (`/v1/omni`); agent profile CRUD |
| `cast:render` | `/v1/cast/*` |
| `dub:render` | `/v1/dub/*` (Dub: submit a dubbing job, poll it, read its audio or video) |
| `transcribe:jobs` | Async `POST/GET /v1/transcription/jobs` (with `hear:transcribe`) |
| `kb:manage` | Hosted knowledge bases (`/v1/knowledgebases`). Sandbox keys from `POST /v1/sandbox/keys` omit this. |
| `amd:detect` | AMD stream (`wss://api.pyai.com/v1/amd/stream`) |
| `amd:configure` | `POST /v1/amd/config` |
| `amd:read` | `GET /v1/amd/calls` |
| `recap:configure` | Recap enablement and CRM mapping |
| `recap:read` | Recap call reads |
| `trace:configure` | Trace rule-pack configuration |
| `trace:read` | Trace scorecard reads |
| `telephony:manage` | `/v1/telephony/*` and WhatsApp Business calling. Live keys only; the org must also be entitled. |

`GET /v1/models` and `GET /v1/voices` are catalog reads, any active key may call
them. A request whose key lacks the required scope returns `403 forbidden`.

Cue is unavailable. There is no active Cue scope or grounding behavior on the
Hear streaming route.

## WebSocket authentication

Browsers can't set headers on a WebSocket upgrade, so pass the key as a
subprotocol — **two values, the `pyai.v1` marker first and your key second**:

```
Sec-WebSocket-Protocol: pyai.v1, pyai-key.pyai_live_...
```

```js theme={null}
new WebSocket(url, ["pyai.v1", `pyai-key.${apiKey}`]);
```

Offer both. RFC 6455 makes the server echo the subprotocol it *selects*, so
PyAI selects `pyai.v1` and **never** reflects your key in the response — your
key does not appear in browser devtools or in any intermediary's response-header
log. A server can only select a value the client actually offered: send the key
alone and there is nothing safe left to select, so the 101 carries no
`Sec-WebSocket-Protocol` at all, which browsers accept but Node's `ws` rejects
with `Server sent no subprotocol`. The PyAI SDKs already send both.

Server-side clients may instead append `?api_key=...` to the URL. Never put the
key in any other query parameter.

## Rotation & revocation

Each key in the console has **rotate** and **revoke** controls.

* **Rotate** issues a new secret and invalidates the old one.
* **Revoke** disables the key everywhere within 60 seconds.

After revocation, calls with the old key return `401 unauthorized`.

<Tip>
  If a key is ever exposed, revoke it immediately and mint a new one, there is no
  penalty for rotating often.
</Tip>

## What errors look like

Data-plane auth failures use the OpenAI envelope. Branch on `error.code`. Do
not retry these.

Missing or revoked key (`401`). The gateway code is `invalid_api_key` (or
`missing_api_key` when the header is absent):

```json theme={null}
{
  "error": {
    "message": "Invalid API key.",
    "type": "invalid_request_error",
    "code": "invalid_api_key",
    "param": null
  }
}
```

Key is valid but missing a product scope (`403`). A sandbox mint hitting Clone
looks like this (`speak:clone` is not on `POST /v1/sandbox/keys`):

```json theme={null}
{
  "error": {
    "message": "Key lacks required scope 'speak:clone'.",
    "type": "permission_error",
    "code": "insufficient_scope",
    "param": null
  }
}
```

Call `GET /v1/me` and read `scopes` before you retry. Signup keys include
`speak:clone`; sandbox mint keys do not.

A `pyai_live_` key on an org with no prepaid credit (`402`):

```json theme={null}
{
  "error": {
    "message": "Your prepaid credit balance is exhausted. Add credits or a payment method to continue.",
    "type": "insufficient_quota",
    "code": "credit_exhausted",
    "param": null
  }
}
```

Sandbox `pyai_test_` keys skip the credit gate. They return
`429 daily_cap_exceeded` when the daily unit cap is hit instead.

See [Errors and limits](/errors-and-limits) for the full code table and
[Pricing](https://pyai.com/pricing) for current rates and included usage.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.