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.

curl https://api.dairo.app/v1/whoami \
  -H "Authorization: Bearer $DAIRO_API_KEY"

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.

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

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.

List and revoke keys whenever you need to:

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 for the full setup and lockout recovery.

Restrict a key to specific inboxes

Pass inboxIds at creation to pin a key to some of your inboxes. A pinned key sends only as those inboxes, and its mailbox reads — messages, threads, attachments, delivery events, and contact history — only see their mail. Its inbox calls see only those inboxes too: listing inboxes returns just them, and updating or deleting any other inbox of the organization, or opening a verification wait on it, answers 404. It is also refused (403) every change that spans the whole organization: adding or changing domains and webhooks, creating inboxes, creating, deleting, or binding agent passports and clearing an agent's breaker (DELETE /v1/agents/{id}/reputation), budget changes, buying, updating, or releasing phone numbers, Slack app changes, the organization's vanity slug, and creating or deleting organizations; reading or exporting the audit log and subject erasure jobs are refused too. Omit inboxIds (or pass []) for a key that works across every inbox.

curl -X POST https://api.dairo.app/v1/api-keys \
  -H "Authorization: Bearer $DAIRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"support-agent","scopes":["messages"],"inboxIds":["7c9e6679-7425-40de-944b-e07fc1f90ae7"]}'

Change the restriction later with PATCH /v1/api-keys/{id} and a new inboxIds array ([] lifts it). A pinned key only creates keys pinned to its own inboxes, and it lists, changes, and revokes only keys pinned within them: itself and any other key pinned only to inboxes it holds, whoever created that key. Every other key, including an unpinned one, answers 404, and a pinned key can never set a restriction wider than its own inboxes. Unknown body fields are rejected with a 400, so a misspelled restriction never mints an unrestricted key. whoami reports a key's inboxIds.

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:

ScopeGrants
messages:readRead messages, threads, attachments, and delivery events.
messages:sendSend, schedule, and cancel outbound messages.
inboxes:read / inboxes:writeRead / create, update, and delete inboxes.
contacts:read / contacts:writeRead / create, update, and delete contacts and their handles.
domains:read / domains:writeRead / add, verify, update, and delete domains.
webhooks:read / webhooks:writeRead / create, update, and delete webhooks.
templates:read / templates:writeRead / author and version templates.
audiences:read / audiences:writeRead / create and add members to audiences (a broadcast send needs messages:send).
keys:read / keys:writeRead / 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 for the full list and the exact scope each endpoint needs.

Grant the narrowest set a workload needs. Creating a key takes keys:write, and a key can only mint child keys whose scopes are a subset of its own, so no key can create one with broader access than it holds.

Scopes say what a key may do; the current role of the person who created it says whether it may change the organization's settings. Creating, changing, or deleting domains, webhooks, and API keys, budget changes, buying, updating, or releasing phone numbers, Slack app changes, the organization's vanity slug, reading or exporting the audit log, and listing, reading, or enqueuing erasure jobs need a key whose creator is an organization owner or admin; a member's key gets a 403 there, whatever its scopes.

The same role sets what a new key may hold. POST /v1/api-keys with a projectId mints the key into that organization with only the scopes your role there allows: a member cannot give it domains:write, webhooks:write, keys:write, account:write, phone:write, budgets:write, or compliance:read, and the request answers 403 naming the scopes to leave out.

Use your key from any surface

The Python SDK reads DAIRO_API_KEY from the environment, or takes the key explicitly:

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:

const dairo = new Dairo({ apiKey: process.env.DAIRO_API_KEY! });

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 for the full code list.

Verify webhook signatures

Webhook deliveries are signed so you can confirm they came from Dairo and are not replayed. Verify the signature over the timestamp, event ID, and raw request body, and check the timestamp — see 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.

On this page