> ## 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.

# CLI

> Send mail, verify domains, read inboxes, and tail events from your shell, CI, and coding agents.

Send mail, verify domains, read inboxes, and tail events from your terminal. The
Dairo CLI is built for scripts, CI pipelines, and coding agents. It uses the same
API key as the SDKs and keeps complaint safety on by default.

## Install

<CodeGroup>
  ```bash title="npm" theme={null}
  npm install -g @dairo/cli
  ```

  ```bash title="Homebrew" theme={null}
  brew install dairo-app/tap/dairo
  ```

  ```bash title="Install script" theme={null}
  curl -fsSL https://dairo.app/install.sh | sh
  ```

  ```powershell title="Windows" theme={null}
  irm https://dairo.app/install.ps1 | iex
  ```
</CodeGroup>

Native binaries ship for macOS (arm64/x64), Linux (arm64/x64), and Windows (x64).

## Authenticate

Sign in with your browser, or set a key directly. Either way the credential is
the same one the SDKs use — see [Authentication](/get-started/authentication).

```bash theme={null}
# Interactive: opens your browser and mints a scoped key (OAuth)
dairo login

# Headless machines (SSH, containers): shows a one-time code to enter at
# https://platform.dairo.app/activate from any device. `dairo login` also
# falls back to this automatically when no browser can be opened.
dairo login --device-code

# Recommended for CI and automation
export DAIRO_API_KEY="dairo_live_..."

# Or store a key locally — the token is read from stdin, never a shell
# argument, so it stays out of your history
printf '%s' "$DAIRO_API_KEY" | dairo auth token set

# Confirm
dairo whoami
```

The CLI checks `DAIRO_API_KEY` first, then a local config file.

<Tip>
  Add `--json` to most commands for machine-readable output — ready to pipe into
  `jq` or parse in a script or agent.
</Tip>

## Send your first email

```bash theme={null}
dairo domain add yourapp.com
dairo domain recheck yourapp.com          # repeat until the domain verifies
dairo inbox create hello --domain yourapp.com

dairo send \
  --inbox-id inbox_123 \
  --to you@example.com \
  --subject "Hello from Dairo" \
  --text "Sent from the Dairo CLI."
```

A send that is blocked because a recipient reported your mail as spam returns an
error; pass `--ignore-complaints` only to override it deliberately. See
[Land in the inbox](/webhooks/deliverability).

## Command reference

### Account and usage

```bash theme={null}
dairo whoami
dairo whoami --json
```

`dairo whoami` identifies your key and project and returns the `plan`, `limits`,
`usage`, billing `period`, `storage`, and the key's `allowedIps` (an IP allowlist,
or `null` for any IP). Check it before a large send to see your headroom.

### Domains

```bash theme={null}
dairo domain list
dairo domain add yourapp.com
dairo domain recheck yourapp.com
dairo domain delete yourapp.com
```

`dairo domain recheck` re-reads your DNS records and reports whether the domain is
verified.

### Inboxes

```bash theme={null}
dairo inbox list
dairo inbox create support --domain yourapp.com
dairo inbox delete inbox_123
```

### Sending

```bash theme={null}
dairo send \
  --inbox-id inbox_123 \
  --to ada@example.com \
  --subject "Hello from Dairo" \
  --text "Plain body" \
  --html "<p>HTML body</p>"

# Schedule for a future time instead of sending now
dairo send \
  --inbox-id inbox_123 \
  --to ada@example.com \
  --subject "Reminder" \
  --text "Don't forget." \
  --send-at 2026-07-01T09:00:00Z
```

At least one `--to` and one body (`--text`, `--html`, or `--react-source`) are
required. Add `--attachment ./file.pdf` (repeatable) and
`--attachment-delivery attachment|auto|link`. Use `--ignore-complaints` only to
deliberately override complaint suppression.

`--send-at` takes an RFC 3339 timestamp with an explicit timezone offset (for
example `2026-07-01T09:00:00Z` or `2026-07-01T11:00:00+02:00`), in the future and
at most 30 days out. A scheduled send returns `status: scheduled` with a
`scheduledAt` timestamp; cancel it with `dairo outbound cancel <messageId>`.

### Track delivery

```bash theme={null}
dairo outbound list --limit 20
dairo outbound get <messageId>

# Cancel a still-scheduled send before its fire time
dairo outbound cancel <messageId>

# Delivery-event timeline for one email
dairo outbound events --message-id <messageId>
```

Each email carries a `status` (including `scheduled` and `canceled`) plus
`scheduledAt` and `canceledAt` timestamps when set. `dairo outbound get` returns
the email with its full delivery timeline; `dairo outbound events` lists only the
events. `dairo outbound cancel` works only while the email is still `scheduled` —
once it is no longer scheduled (already sent, queued, or canceled) it returns a
`409` conflict.

### Physical mail (letters)

```bash theme={null}
# Price a letter before sending (letters:read)
dairo letter price --country CH --pages 3 --delivery economy

# Print and post a real letter (letters:send)
dairo letter send \
  --pdf ./invoice-2026-06.pdf \
  --to-name "Jane Doe" \
  --street "Hauptstrasse" --house-number 12 \
  --postal-code 8001 --city "Zürich" --country CH \
  --print-mode grayscale --sides duplex \
  --delivery economy

# List, read, and follow the delivery timeline
dairo letter list --limit 20 --status in_transit
dairo letter get let_01HZX
dairo letter events let_01HZX --limit 50

# Cancel before dispatch
dairo letter cancel let_01HZX
```

[Fairo](/letters/overview) turns a PDF into a real, posted letter. Reads use
`letters:read`; sending and canceling use `letters:send`. Cancel works only while
the letter has not been dispatched yet.

### Messages, threads, attachments

```bash theme={null}
dairo messages list --inbox-id inbox_123
dairo messages get msg_123
dairo messages download-attachments msg_123 --out ./downloads

dairo threads list --inbox-id inbox_123
dairo threads get thread_123

dairo attachments url att_123 --expiry-hours 1
dairo attachments share att_123 --expiry-hours 24
dairo attachments download att_123 --out ./file.pdf
```

### Webhooks

```bash theme={null}
dairo webhook list
dairo webhook create \
  --url https://yourapp.com/dairo/webhook \
  --event message.received \
  --event message.delivered
dairo webhook delete https://yourapp.com/dairo/webhook
```

### API keys

```bash theme={null}
dairo api-key list
dairo api-key create --name production-worker \
  --scope messages:send --scope messages:read           # prints the secret once

# Restrict the key to specific source IPs / CIDR ranges (repeatable)
dairo api-key create --name ci-runner \
  --scope messages:send \
  --allowed-ip 203.0.113.0/24 \
  --allowed-ip 198.51.100.7

dairo api-key revoke key_123
```

Pass `--scope` once per scope. Add `--allowed-ip` (repeatable, up to 50) to lock a
key to specific source IPs or ranges; omit it to allow any IP. A correct secret
presented from an unlisted IP is rejected with `403`. The allowlist shows up in
`dairo api-key list`, `dairo whoami`, and the create output.

### Audiences

```bash theme={null}
dairo audiences list
dairo audiences create "Beta announcements"
dairo audiences get aud_123
dairo audiences add aud_123 --handle ada@example.com --name "Ada"
dairo audiences import-csv aud_123 --file contacts.csv
dairo audiences send aud_123 --inbox-id inbox_123 \
  --subject "We're live" --html "<p>Hello</p>"
```

`dairo audiences add` adds or updates members; `dairo audiences import-csv`
bulk-imports from a CSV. `dairo audiences send` runs the same outbound pipeline as
`dairo send`, fanning out to every member of the list with complaint suppression
applied automatically. Listing and reading use `audiences:read`; creating a list
and adding members use `audiences:write`; `dairo audiences send` requires
`messages:send`.

### Templates

```bash theme={null}
dairo templates list
dairo templates create --slug welcome --name "Welcome" \
  --source-file ./welcome.tsx --subject "Welcome to {{appName}}"
dairo templates get welcome
dairo templates versions welcome
dairo templates publish welcome --source-file ./welcome-v2.tsx
```

Author versioned React-email templates (scopes `templates:read` and
`templates:write`), then send with one by passing a `template` reference
(`{ id, version?, variables }`) instead of an inline body.

### Wait for one-time codes

```bash theme={null}
# Attach an extraction schema to an inbox (inboxes:read / inboxes:write)
dairo inbox schema get inbox_123
dairo inbox schema set inbox_123 --schema-file schema.json \
  --on-validation-error quarantine
dairo inbox schema delete inbox_123

# Wait for an inbound one-time code
dairo inbox verification-waits register inbox_123 \
  --timeout-sec 120 --from-hint acme
dairo inbox verification-waits list inbox_123
dairo inbox verification-waits get inbox_123 vw_123
dairo inbox verification-waits cancel inbox_123 vw_123
```

Give an inbox a schema so incoming mail arrives as clean fields, or register a wait
that resolves with the extracted `code` the moment a matching email lands. See
[Wait for one-time codes](/receiving/verification-waits).

### The event stream

```bash theme={null}
dairo events list --limit 50
dairo events list --wait 12            # long-poll for new events (up to 12s)
dairo listen                           # tail the live stream
dairo events replay --since <cursor>   # re-deliver a slice to webhooks
```

Pull, tail, and replay your event stream. Reading uses `events:read`; replay uses
`events:write`. See [The event stream](/events/event-ledger).

### Agents, reputation, budgets

```bash theme={null}
dairo agents list
dairo agents get agt_123
dairo agents verify --id <rfcMessageId>   # public verdict, no scope needed
dairo reputation list                     # fleet send-health view
dairo budgets list
dairo budgets set --scope account --max-sends-per-day 5000
```

Signed agent identities, fleet send-health, and per-agent send ceilings. Reads use
`agents:read` and `budgets:read`; changes use the matching `:write` scope;
provenance verification is public.

### Compliance

```bash theme={null}
dairo compliance residency
dairo erasure-jobs create --subject-email user@example.com
dairo erasure-jobs list
dairo erasure-jobs get job_123
dairo audit-logs list --limit 50 --json
```

Erasure jobs use `compliance:read` and `compliance:write`; residency and audit-log
reads use `account:read`.

### MCP install

```bash theme={null}
# Save a token and wire up Dairo MCP for your coding agents in one step
printf '%s' "$DAIRO_API_KEY" | dairo auth token set && \
  dairo mcp install --client auto
```

See the [MCP server](/agent-first/mcp-server).

## Scripting example

A CI script that waits for a domain to verify, then sends a build notification:

```bash theme={null}
#!/usr/bin/env bash
set -euo pipefail

export DAIRO_API_KEY="${DAIRO_API_KEY:?set DAIRO_API_KEY}"

# Wait for the domain to verify
until dairo domain recheck yourapp.com --json | grep -q '"status":"verified"'; do
  echo "waiting for DNS…"; sleep 15
done

dairo send \
  --inbox-id "$DAIRO_INBOX_ID" \
  --to oncall@yourapp.com \
  --subject "Build ${GIT_SHA} deployed" \
  --text "Deploy complete on $(date -u)."
```

## Next steps

Build reliable mail agents with the patterns in
[Why Dairo is agent-native](/agent-first/agent-patterns). For the same surface as
copy-paste agent tool calls, see [MCP recipes](/agent-first/mcp-recipes).
