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

npm install -g @dairo/cli

The package installs the native binary for your platform: macOS (arm64/x64), Linux (arm64/x64), or Windows (arm64/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.

# 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

# Sign out: revokes the stored key and removes it from the config file
dairo logout

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

Without --json the CLI prints for people: lists as tables (a further page is announced as More results: --cursor <cursor>), everything else as key: value lines. Add --json to any command to print the API response exactly as returned — the object itself, or the list envelope with data and pagination — ready to pipe into jq or parse in a script or agent.

Send your first email

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.

Command reference

Account and usage

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 [] for any IP). Check it before a large send to see your headroom.

Domains

dairo domain list
dairo domain add yourapp.com
dairo domain recheck yourapp.com
dairo domain delete yourapp.com
dairo domain delete yourapp.com --cascade   # also delete its inboxes and their mail

dairo domain recheck re-reads your DNS records and reports whether the domain is verified. A domain that still has inboxes is only deleted with --cascade, which deletes those inboxes and their messages too.

Inboxes

dairo inbox list
dairo inbox create support --domain yourapp.com
dairo inbox delete inbox_123

Sending

dairo send \
  --inbox-id inbox_123 \
  --to ada@example.com \
  --subject "Hello from Dairo" \
  --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 "tomorrow at 9am"

Name the sending inbox with --inbox-id, or by address with --from. At least one --to and exactly one body (--text, --text-file, --html, --html-file, or --react-source) are required. Add --attachment ./file.pdf (repeatable); attachments travel inline in the email, 8 MiB in total. 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) or a natural-language time such as "in 1 hour" or "tomorrow at 9am"; the time must be in the future and at most 30 days out. A scheduled send returns status: scheduled with a scheduledAt timestamp; cancel it with dairo messages cancel <messageId>.

Track delivery

dairo messages list --direction outbound --limit 20
dairo messages get <messageId>

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

# Delivery-event timeline for one email (or only its bounces or complaints)
dairo messages events <messageId>
dairo messages events <messageId> --type bounce

Each email carries a status (including scheduled and canceled) plus scheduledAt and canceledAt timestamps when set. dairo messages get returns the email with its delivery events inline; dairo messages events lists only the events. dairo messages 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, and an id your key cannot see returns 404. See Track delivery.

Physical mail (letters)

# Price a letter before sending (letters:read)
dairo letter price --country CH --page-count 3 --delivery economy

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

# 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 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. Without --confirm, dairo letter send creates a draft instead of posting it. --delivery is economy (the default), priority, or registered.

Messages, threads, attachments

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

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

# Check a delivery you received: the raw body on stdin, the headers as flags
DAIRO_WEBHOOK_SECRET=whsec_... dairo webhook verify \
  --signature "$X_DAIRO_SIGNATURE" \
  --timestamp "$X_DAIRO_TIMESTAMP" \
  --event-id "$X_DAIRO_EVENT_ID" < body.json

dairo webhook verify checks a delivery offline, without an API call, using the same rules as the verification snippets: the v2 signature over the timestamp, event id, and body, any entry of a rotation header, and a timestamp no older than 5 minutes. It exits non-zero when the delivery does not verify.

API keys

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

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 with the member's email address in the first column and an optional name in the second (an optional handle,name header row is allowed; any other first row is refused with its line number). dairo audiences send runs the same outbound pipeline as dairo send, fanning out to every member of the list with complaint suppression applied automatically. A broadcast always sends now, one email per member, so it takes no --to, --send-at, or --channel. Listing and reading use audiences:read; creating a list and adding members use audiences:write; dairo audiences send requires messages:send.

Contacts

dairo contacts list --limit 50
dairo contacts list --limit 50 --cursor <pagination.nextCursor>
dairo contacts create --display-name "Ada Lovelace" --alias ada \
  --handle email=ada@example.com
dairo contacts get ctc_123
dairo contacts messages ctc_123 --limit 25

dairo contacts list prints one page (--limit 1–100, default 50); pass the returned pagination.nextCursor as --cursor while pagination.hasMore is true. Reads use contacts:read; changes use contacts:write. See Contacts.

Templates

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

# 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 from acme.com (or any of its subdomains)
dairo inbox verification-waits register inbox_123 \
  --timeout-sec 120 --from-hint acme.com \
  --instruction "the 6-digit sign-in code from 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 result the moment a matching email lands. See Wait for one-time codes.

The event stream

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.

Agents, reputation, budgets

dairo agents list
dairo agents get agt_123
dairo agents verify --id <messageId>      # 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
dairo budgets set --scope account --max-spend-per-day-cents 2000   # $20 a day

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. --max-spend-per-day-cents caps the account's paid holds (calls, letters, AI generations) over the last 24 hours and applies to the account budget only.

Compliance

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

# 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

--client auto configures every installed client (Claude Code, Codex, Cursor, Hermes). The entries are written to the clients' config files in your home directory — Claude Code's is ~/.claude.json — never into the current project, because they hold your API key. See the MCP server.

Scripting example

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

#!/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 | jq -e '.status == "verified"' >/dev/null; 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. For the same surface as copy-paste agent tool calls, see MCP recipes.

On this page