CLI

Mailhooks CLI

A command-line interface designed for AI agents and shell scripts. Every command emits JSON to stdout, so it composes cleanly with jq, pipelines, and LLM tool loops.

Overview

The Mailhooks CLI (@mailhooks/cli) is a thin wrapper over the Mailhooks SDK that makes every operation available from the terminal. It's built for agentic systems: wait for emails, extract verification codes, download attachments, and manage inboxes without writing a line of application code.

JSON-first output

Every command emits structured JSON. Auto-compact when piped.

wait-for primitive

Block until a matching email arrives. Built for agent loops.

Multi-profile auth

Named profiles for prod, multiple tenants.

Structured errors

JSON errors to stderr with exit codes. Machine-parseable.

Install

npm install -g @mailhooks/cli
# or invoke without install:
npx @mailhooks/cli 

Requires Node.js 18+. The CLI depends on @mailhooks/sdk and commander.

Quick Start

# Install globally
npm install -g @mailhooks/cli

# Or run without installing
npx @mailhooks/cli whoami

# Store your API key
mailhooks login --api-key mh_live_...

# List emails
mailhooks emails list --unread --per-page 10

# Wait for a specific email
mailhooks emails wait-for --subject "Verify your email" --timeout 60000

Configuration

Credentials are stored in named profiles — one config file can hold keys for production, or multiple tenants. Each invocation resolves credentials in this order:

Profile selection: --profile <name> flag → MAILHOOKS_PROFILE env → currentProfile in config → none

API key: --api-key flag → MAILHOOKS_API_KEY env → selected profile's apiKey → error

Base URL: --base-url flag → MAILHOOKS_API_URL env → profile baseUrl https://mailhooks.dev/api

Env varFlagPurpose
MAILHOOKS_API_KEY--api-keyAPI key
MAILHOOKS_API_URL--base-urlOverride API base URL
MAILHOOKS_PROFILE--profileWhich stored profile to use
MAILHOOKS_CONFIG_PATHOverride config file path

Login & Profiles

# Store credentials (verified against the API by default)
mailhooks login --api-key mh_live_abc123

# Multiple profiles — separate tenants or environments
mailhooks login --profile prod    --api-key mh_prod_...
mailhooks login --profile client1 --api-key mh_client1_...

# Pipe from a secret manager
op read "op://vault/mailhooks/api-key" | mailhooks login --profile prod

# Switch the active profile
mailhooks profiles use prod

Global flags

  • --pretty / --no-pretty — override JSON formatting. Default: pretty on a TTY, compact when piped. Agents get compact output automatically.
  • --help — show command help
  • --version — print CLI version

Output Contract

ConditionstdoutExit code
SuccessJSON0
API error1 (stderr: {"error","code"})
Usage error2 (stderr: code: "usage")
Timeout (wait-for)124
Login verify failed1 (code: "verify_failed")

Error objects on stderr: {"error": "<message>", "code": "<code>"}. Always check the exit code before parsing stdout — agents that assume success and try to jq an error message will get confused.

The Agent Loop

The core pattern: wait for an email, read its body, then mark it as handled.

# The canonical agent loop: wait → read → mark done
EMAIL=$(mailhooks emails wait-for --subject "Order Confirmation" --timeout 60000)
ID=$(echo "$EMAIL" | jq -r '.id')
mailhooks emails content "$ID" --text     # plain-text body
mailhooks emails mark-read "$ID" > /dev/null

Command Reference

Auth Commands

mailhooks login

Store an API key under a named profile. Verified against the API by default.

mailhooks login --api-key mh_live_... [--profile ] [--base-url ] [--skip-verify]

Without --profile, writes to a profile called default. Accepts the key via flag, stdin pipe, or a hidden TTY prompt.

mailhooks logout

mailhooks logout                    # removes the currently selected profile
mailhooks logout --profile prod     # removes a specific profile
mailhooks logout --all              # nuke the whole config file

mailhooks whoami

Shows active credentials (masked), source resolution, profile name, and available domains. When unauthenticated, returns {authenticated: false} with exit 0 — useful as a health check at agent startup.

{
  "authenticated": true,
  "apiKey": "mh_xxx…yyyy",
  "baseUrl": "https://mailhooks.dev/api",
  "profile": "default",
  "currentProfile": "default",
  "profiles": ["default", "prod"],
  "source": {
    "apiKey": "profile",
    "baseUrl": "default",
    "profile": "config"
  },
  "domains": [
    { "id": "...", "domain": "example.com", "status": "ACTIVE",
      "verified": true, "isDefault": true, "environment": "production" }
  ],
  "configPath": "/home/user/.config/mailhooks/config.json"
}

The domains[] list is useful for agent discovery: an agent can call whoami to find out which domains it can receive mail at before constructing inbox addresses.

mailhooks profiles list / profiles use <name>

mailhooks profiles list
# → { currentProfile, profiles: [{name, apiKey (masked), baseUrl, current}] }

mailhooks profiles use prod
# → { ok: true, currentProfile: "prod" }

Email Commands

mailhooks emails list

List emails (paginated, most-recent-first by default).

mailhooks emails list --from [email protected] --unread --per-page 10

Flags: --from --to --subject --since --until --read --unread --page --per-page --sort-field --sort-order

Output: { data: Email[], currentPage, perPage, totalItems, totalPages, hasNextPage }

mailhooks emails get <id>

Fetch a single email's metadata (headers, sender, recipients, attachments — not the body).

mailhooks emails get em_123 --mark-read

mailhooks emails content <id>

Fetch the HTML + text body. Raw modes pipe cleanly into other tools.

# JSON with both bodies
mailhooks emails content em_123
# → { "html": "

...

", "text": "..." } # Raw text only (pipes cleanly into grep, LLM prompts, etc.) mailhooks emails content em_123 --text # Raw HTML only mailhooks emails content em_123 --html

mailhooks emails wait-for

Poll until a matching email arrives. The main primitive for agents that trigger on inbound mail — magic links, verification codes, confirmations.

mailhooks emails wait-for \
  --from [email protected] \
  --subject "verification" \
  --timeout 120000 \
  --poll-interval 2000 \
  --lookback 5000
FlagDefaultPurpose
--timeout <ms>30000Give up after this long. Exit 124 on timeout.
--poll-interval <ms>1000How often to check.
--initial-delay <ms>0Wait before first check.
--lookback <ms>10000Consider emails this far back on first check. Avoids matching stale emails.

mailhooks emails mark-read <id> / mark-unread <id>

Update read state. Returns the updated email.

mailhooks emails delete <id>

Permanently delete an email and its attachments. Returns { id, deleted: true }. Note: does not refund quota.

mailhooks emails download-eml <id>

Download the raw .eml source (RFC 822).

# Save to a file
mailhooks emails download-eml em_123 -o /tmp/msg.eml
# → { "path": "/tmp/msg.eml", "bytes": 12345 }

# Pipe raw bytes to stdout
mailhooks emails download-eml em_123 > msg.eml

mailhooks emails download-attachment <emailId> <attachmentId>

Download a specific attachment. Attachment IDs come from emails get .attachments[].id.

mailhooks emails download-attachment em_123 att_456 -o invoice.pdf

Parse EML

Parse an EML file into structured JSON. Useful for BYOB (bring-your-own-bucket) setups where the raw EML is in your own storage.

# Parse a raw EML file into structured JSON
mailhooks parse-eml /path/to/msg.eml

# Or from stdin
cat msg.eml | mailhooks parse-eml

Output: { from, to[], subject, body, html?, attachments[], headers, date? }

Listen Command

mailhooks listenconnects to the Mailhooks real-time SSE stream and forwards email events to a local HTTP endpoint — like stripe listen --forward-toor ngrok, but for Mailhooks.

SSE streaming

Real-time events via Server-Sent Events.

Local forwarding

POSTs each event to your local webhook route.

Signature verification

Optional HMAC-SHA256 signing with --secret.

Basic usage

# Forward all email events to localhost:3000/webhooks
mailhooks listen

# Custom endpoint
mailhooks listen --forward-to http://localhost:8080/api/mailhooks

# With webhook signature verification
mailhooks listen --secret whsec_dev_secret

# Distributed mode (load-balanced SSE)
mailhooks listen --mode distributed --forward-to http://localhost:4000/hooks

# Quiet mode (no stdout output)
mailhooks listen --no-print

How it works

  1. Connects to the Mailhooks SSE stream using your API key.
  2. On each email.received or email.updated event, fetches the full email payload and POSTs it to your local URL.
  3. If --secret is provided, each request includes an X-Webhook-Signature header containing the HMAC-SHA256 of the body. Verify it server-side with verifyWebhookSignature() from the SDK.
  4. Auto-reconnects on disconnect (disable with --no-reconnect).
  5. Runs until Ctrl+C. Prints connection status to stderr; optionally emits event JSON to stdout (on by default in a TTY).

Forwarded request format

Each forwarded request contains the full email payload (matching real Mailhooks webhook format), with headers:

  • Content-Type: application/json
  • X-Mailhooks-Event: email.received (or email.updated)
  • X-Webhook-Signature (if --secret is set)
{
  "id": "em_abc123",
  "from": "[email protected]",
  "to": ["[email protected]"],
  "subject": "Hello",
  "body": "Plain text body...",
  "html": "

HTML body...

", "attachments": [], "receivedAt": "2025-01-15T10:30:00.000Z" }

Flags

FlagDefaultPurpose
-f, --forward-to <url>http://localhost:3000/webhooksLocal URL to forward events to
--mode <mode>broadcastSSE mode: broadcast (all events) or distributed (load-balanced)
--no-reconnect(reconnect on)Disable automatic reconnection
--reconnect-delay <ms>5000Delay between reconnection attempts
--secret <secret>Webhook signing secret. Adds X-Webhook-Signature header
--printauto (TTY=on)Print event JSON to stdout
--no-printSuppress event JSON output

Local dev with Next.js

The most common pattern: run your app in one terminal, forward Mailhooks events in another. Your webhook route receives real email events locally.

# Terminal 1: your app
npm run dev

# Terminal 2: forward Mailhooks events
mailhooks listen --forward-to http://localhost:3000/api/mailhooks/webhook --secret whsec_dev_secret

# Your /api/mailhooks/webhook route receives real Mailhooks events locally

Patterns

Extract a one-time code

# Wait for a one-time code, extract it, move on
EMAIL=$(mailhooks emails wait-for --subject "Your login code" --timeout 60000) || exit 1
ID=$(echo "$EMAIL" | jq -r '.id')
CODE=$(mailhooks emails content "$ID" --text | grep -oE '[0-9]{6}' | head -1)
mailhooks emails mark-read "$ID" > /dev/null
echo "$CODE"

Drain an inbox by sender

# Mark all unread emails from a sender as read
mailhooks emails list --from [email protected] --unread --per-page 100 \
  | jq -r '.data[].id' \
  | while read id; do mailhooks emails mark-read "$id" > /dev/null; done

Multi-tenant agents

# An agent operating on behalf of different tenants
for tenant in acme globex initech; do
  mailhooks --profile "$tenant" emails list --unread --per-page 50
done

Discover available domains

# Discover available inbox domains before constructing an address
DOMAIN=$(mailhooks whoami | jq -r '.domains[] | select(.verified and .isDefault) | .domain' | head -1)
echo "agent+$(uuidgen)@$DOMAIN"

Stream events to a local dev server

# Terminal 1: your app
npm run dev

# Terminal 2: forward Mailhooks events
mailhooks listen --forward-to http://localhost:3000/api/mailhooks/webhook --secret whsec_dev_secret

# Your /api/mailhooks/webhook route receives real Mailhooks events locally

Use --secret to match your production webhook verification in dev. Works with any framework — Next.js, Express, FastAPI, etc.

CLI vs SDK vs MCP

FeatureCLISDKMCP Server
Use caseAgents, scripts, terminalApplication codeLLM tool-calling agents
Installnpm i -g @mailhooks/clinpm i @mailhooks/sdkmcp-mailhooks
LanguageAny (JSON CLI)TypeScript / NodeAny (MCP protocol)
Output format JSON to stdout Typed objects Tool responses
wait-for Built-in Built-in Built-in
listen (SSE → local) Built-in Via RealtimeResource No
Multi-profile auth Yes Manual Single key
No code required Shell only Import + code Config only

Troubleshooting

Exit code 2 with "missing_api_key"

No API key found. Run mailhooks login, set MAILHOOKS_API_KEY, or pass --api-key.

Exit code 124 from wait-for

Timeout — no matching email arrived. Increase --timeout, widen --lookback, or check that the email was actually sent to a verified domain.

Exit code 1 with "verify_failed"

The API key you passed to login was rejected. Check the key is correct and hasn't been revoked. Use --skip-verify to store it anyway (not recommended).

JSON looks wrong when piped

The CLI detects whether stdout is a TTY and switches between pretty and compact JSON. If something in between (e.g. a pseudo-TTY in a container runner) tricks the detection, pass --no-pretty explicitly.

Get Started