> This page is for Voice AI Platform.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.hams.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.hams.ai/_mcp/server.

# Authentication

> The two kinds of API key, the header that carries each, and which endpoints take which.

Every request carries one API key. Which kind depends on where the request comes from:

| Key        | Header               | Made for                      |
| ---------- | -------------------- | ----------------------------- |
| Secret key | `X-Secret-Key: sk_…` | Your servers                  |
| Public key | `X-Public-Key: pk_…` | Browsers and embedded widgets |

Both are created in the dashboard; [API keys](/platform/api-keys) explains the options each one
offers.

> **Warning**
>
> Send one key per request. A request that carries both `X-Secret-Key` and `X-Public-Key` is
> rejected with `400` before it reaches any endpoint.

## Secret key

A secret key authenticates a server. It belongs to one entity, the workspace you see in the
dashboard, so the request needs nothing else to say where to work:

```bash
curl https://api.hams.ai/outbound-batch/batches \
  -H "X-Secret-Key: $HAMS_SECRET_KEY"
```

A secret key has a role, `WRITER` or `READER`. Readers can list and fetch; writers can also
create, update and delete batches and widget configuration. A key can be limited to an IP
allowlist (single addresses or CIDR ranges), and calls from any other address get `403`. Secret
keys are for server-to-server traffic: the API does not answer browser CORS preflights for them,
and a secret key that ends up in a page should be revoked and replaced.

## Public key

A public key authenticates a browser page or a widget, where a secret cannot be hidden. It only
reaches the endpoints under `/v1/`, which are the chat session and widget configuration calls:

```bash
curl -X POST https://api.hams.ai/v1/ai-agents/$AGENT_ID/chat/init \
  -H "X-Public-Key: $HAMS_PUBLIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

Three restrictions keep a public key safe even though it is visible:

* **Domains.** The request's `Origin` (or `Referer`) must match one of the key's allowed domains.
  A public key cannot be created without at least one.
* **Scopes.** An optional scope list names the operations the key may call, such as `chat:init`
  or `widget-config:read`. A key without a list may call all of them.
* **reCAPTCHA.** When a reCAPTCHA secret is configured on the key, starting a chat also requires a
  `g-token` header carrying a Google reCAPTCHA token, verified against the key's score threshold.

## Which endpoints take which key

| Endpoints                                                                   | Key    |
| --------------------------------------------------------------------------- | ------ |
| `/ai-agents/{agent_id}/chat/…`, `/ai-agents/{agent_id}/widget-config`       | Secret |
| `/outbound-batch/…`                                                         | Secret |
| `/v1/ai-agents/{agent_id}/chat/…`, `/v1/ai-agents/{agent_id}/widget-config` | Public |

## Errors

| Status | `error_code`                                   | Meaning                                                                         |
| ------ | ---------------------------------------------- | ------------------------------------------------------------------------------- |
| `400`  | —                                              | Both keys were sent; the body carries only `detail`                             |
| `401`  | `MISSING_API_KEY`, `INVALID_API_KEY`           | No key in the header, or a key that does not exist or was revoked               |
| `403`  | `FORBIDDEN`                                    | The key's role does not allow the operation, or the IP is outside the allowlist |
| `403`  | `DOMAIN_NOT_ALLOWED`                           | The page's origin is not in the public key's domains                            |
| `403`  | `KEY_SCOPE_NOT_ALLOWED`                        | The public key's scopes do not include this operation                           |
| `400`  | `RECAPTCHA_TOKEN_REQUIRED`, `RECAPTCHA_FAILED` | reCAPTCHA is enabled on the key and the token is missing or scored too low      |

> **Note**
>
> The voice workers that run calls use an internal credential of their own. Their endpoints are not
> part of this reference.