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

# API keys & authentication

> Create a Dairo API key, scope it to least privilege, and use it across the API, the SDKs, the CLI, and the MCP server.

Every request to Dairo authenticates with an API key, sent as a bearer token
in the `Authorization` header. One key works across the REST API, the SDKs,
the CLI, and the MCP server.

The fastest way to confirm a key works is `whoami` — it returns your `userId`,
your `plan`, the key's scopes as `apiKey.scopes`, and your current usage. It
answers for any valid key, whatever its scopes.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl https://api.dairo.app/v1/whoami \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```ts title="TypeScript" theme={null}
  const me = await dairo.whoami();
  console.log(me.userId, me.plan, me.apiKey.scopes);
  ```

  ```python title="Python" theme={null}
  me = dairo.whoami()
  print(me.user_id, me.plan, me.api_key.scopes)
  ```

  ```bash title="CLI" theme={null}
  dairo whoami
  ```
</CodeGroup>

## Create a key

API keys are long-lived credentials for server-side code, CI, agents, and the
CLI. Create one from the dashboard, or over the API with a key that carries
`keys:write`.

<CodeGroup>
  ```ts title="TypeScript" theme={null}
  const { apiKey, secret } = await dairo.apiKeys.create({
    name: "production-worker",
    scopes: ["messages:send", "messages:read"],
  });
  console.log(apiKey.id);   // the key id   (safe to store and display)
  console.log(secret);      // dairo_live_… (shown once — store it now)
  ```

  ```python title="Python" theme={null}
  created = dairo.api_keys.create(
      name="production-worker",
      scopes=["messages:send", "messages:read"],
  )
  print(created.api_key.id)
  print(created.secret)  # dairo_live_… (shown once)
  ```

  ```bash title="CLI" theme={null}
  dairo api-key create --name production-worker --scope messages:send --scope messages:read
  # Prints the one-time secret. Store it now; it is never shown again.
  ```

  ```bash title="cURL" theme={null}
  curl -X POST https://api.dairo.app/v1/api-keys \
    -H "Authorization: Bearer $DAIRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"name":"production-worker","scopes":["messages:send","messages:read"]}'
  ```
</CodeGroup>

<Warning>
  The secret (`dairo_live_…`) is shown **once**, at creation, and never again.
  Capture it immediately. If you lose it, revoke the key and create a new one.
</Warning>

List and revoke keys whenever you need to:

```bash theme={null}
dairo api-key list
dairo api-key revoke key_123
```

### Lock a key to trusted IPs

Pass `allowedIps` at creation — up to 50 IPv4/IPv6 addresses or CIDR ranges —
to bind a key to specific source addresses. Enforcement is deny-by-default:
the correct secret from an unlisted IP is rejected with a `403`. See
[Audit logs](/compliance/audit-logs#api-key-ip-allowlisting) for the full
setup and lockout recovery.

## Scope every key to what it needs

Each key carries a set of scopes that decide what it can do. Every resource
owns its own `read`/`write` pair, and a read is never gated behind a write
scope. The ones you'll reach for first:

| Scope                                | Grants                                                        |
| ------------------------------------ | ------------------------------------------------------------- |
| `messages:read`                      | Read messages, threads, attachments, and delivery events.     |
| `messages:send`                      | Send, schedule, and cancel outbound messages.                 |
| `inboxes:read` / `inboxes:write`     | Read / create, update, and delete inboxes.                    |
| `contacts:read` / `contacts:write`   | Read / create, update, and delete contacts and their handles. |
| `domains:read` / `domains:write`     | Read / add, verify, update, and delete domains.               |
| `webhooks:read` / `webhooks:write`   | Read / create, update, and delete webhooks.                   |
| `templates:read` / `templates:write` | Read / author and version templates.                          |
| `audiences:read` / `audiences:write` | Read / create, add members to, and broadcast to audiences.    |
| `keys:read` / `keys:write`           | Read / create, update, and revoke API keys.                   |

Bundles save you the enumeration: request `messages` and it expands to
`messages:read` + `messages:send`; `letters` and `phone` bundle the same way,
and `admin` expands to every scope. See [Permissions & scopes](/concepts/scopes)
for the full list and the exact scope each endpoint needs.

Grant the narrowest set a workload needs. A key can only mint child keys whose
scopes are a subset of its own — so a `messages:send`-only key can never
create one with broader access.

## Use your key from any surface

<Tabs>
  <Tab title="SDKs">
    The Python SDK reads `DAIRO_API_KEY` from the environment, or takes the key
    explicitly:

    ```python theme={null}
    from dairo import Dairo
    dairo = Dairo()                        # reads DAIRO_API_KEY
    dairo = Dairo(api_key="dairo_live_…")  # explicit
    ```

    The JavaScript SDK never reads the environment on its own — so browser and
    edge runtimes stay predictable — and the other SDKs also take the key
    explicitly:

    ```ts theme={null}
    const dairo = new Dairo({ apiKey: process.env.DAIRO_API_KEY! });
    ```
  </Tab>

  <Tab title="CLI">
    The CLI reads `DAIRO_API_KEY` first, then the local config. To store a key
    in the config, pipe it to `dairo auth token set` — the token is read from
    stdin only, never a positional argument, so your secret stays out of shell
    history and process listings:

    ```bash theme={null}
    printf '%s' "$DAIRO_API_KEY" | dairo auth token set
    ```

    In CI, prefer the environment variable.
  </Tab>

  <Tab title="MCP server">
    The hosted MCP endpoint is `https://mcp.dairo.app/mcp`. It authenticates
    with the same bearer key and advertises typed tool schemas, so an agent
    discovers required fields before it calls:

    ```bash theme={null}
    claude mcp add --transport http dairo https://mcp.dairo.app/mcp
    ```

    See the [MCP server guide](/agent-first/mcp-server) for full setup.
  </Tab>
</Tabs>

## Handle a failed request

Auth failures — and every other error — come back in one shape:
`{ "error": { "type", "code", "message", "param" } }`. Branch on the stable
machine `code`, never on the prose `message`. See [Errors](/concepts/errors)
for the full code list.

## Verify webhook signatures

Webhook deliveries are signed so you can confirm they came from Dairo. Verify
the signature against the raw request body — see
[Verify the signature](/webhooks/webhooks#verify-the-signature).

## Keep your secrets safe

* **Store keys in a secret manager** — Vault, Doppler, AWS Secrets Manager, or
  your host's environment config. Never commit a key or ship one in a
  client-side bundle.
* **Keep keys out of URLs and logs.** Keys belong in the `Authorization`
  header, never a query string. Scrub them from logs, screenshots, and error
  reports.
* **Rotate after tests and on suspicion.** Revoke keys used for temporary
  tests, and rotate immediately if a key may have been exposed.
* **Use one key per workload.** A separate key per service makes revocation
  surgical and usage traceable.
