Skip to main content

The mails CLI

Send and receive email, follow events and set up AI clients from your terminal. Tables for people, JSON for scripts and agents.

Every command below comes from the CLI’s own command table (mails commands --json, version 0.1.0), so this page and mails --help list the same commands and flags. The examples act on hello, the agent a new workspace starts with; the ones that create an agent, archive one or test a forward act on a second agent, which needs a paid plan.

Installation

The CLI needs Node.js 20 or newer and installs no other packages.

npm install -g @mailsai/cli

Then run mails doctor: it checks Node, your sign-in, the connection, your clock and what your key is allowed to do.

Authentication

The CLI picks its credential in this order: --api-key, then MAILS_API_KEY, then a saved profile (--profile, MAILS_PROFILE, or the active one). On a server or in CI, use an API key in MAILS_API_KEY.

# Sign in through your browser, saved as the profile "default"
mails login

# A second workspace: choose it in the browser
mails login --profile acme

# Paste an API key instead; it is never echoed
mails login --api-key

# Which key, workspace and scopes are in use
mails whoami

The browser sign-in can do no more than your role in the workspace allows: as an owner or admin it can do everything, as a member it can send and read but cannot create API keys, agents or domains, and as a billing member it can only read.

Every command in this section works without signing in, except mails whoami and mails workspaces current.

mails login

Sign in through your browser, or with an API key.

Opens your browser to approve the CLI, then saves the sign-in as a profile (in the OS keychain when there is one, otherwise in a file only you can read). With --api-key it saves a key instead: paste it at the prompt, or pipe it in. Nothing is ever echoed, and only the first 12 characters of a key are ever shown.

FlagDescription
--scope <a,b,…>What the browser sign-in asks for (default full_access). It gets no more than your role in the workspace allows. One of: full_access, send, read.
--no-browserPrint the sign-in address instead of opening a browser. Open it in a browser on this same computer, because the sign-in comes back to 127.0.0.1; on a remote machine, use --api-key.
--timeout <n>How long to wait for the browser (default 300).
--store <value>Where to keep the credential (default auto, or MAILS_CREDENTIAL_STORE). One of: auto, keychain, file.
mails login
mails login --api-key
mails login --api-key < key.txt
mails login --profile staging --api-url https://staging.example.com

mails logout

Sign out: forget a saved profile (and end its browser sign-in).

FlagDescription
--allSign out of every profile.
mails logout
mails logout --profile staging
mails logout --all

Profiles

CommandDescription
mails whoamiShow which key, workspace and agent the CLI is using.
mails auth listList saved profiles. Also: mails auth ls.
mails auth switch <profile>Make another saved profile the active one. <profile>: Profile name (see mails auth list). Also: mails auth use.
mails auth rename <profile> <new-name>Give a saved profile a new name. <profile>: The profile's current name. <new-name>: Its new name. Also: mails auth mv.
mails auth remove <profile>Forget a saved profile. <profile>: Profile name (see mails auth list). Also: mails auth rm.
mails whoami
mails whoami --json | jq -r .workspace.slug
mails auth list
mails auth list --json
mails auth switch staging
mails auth rename default work
mails auth remove staging

Workspaces

Every key and every browser sign-in belongs to one workspace. To add another workspace, run mails login --profile <name> and choose it in the browser.

CommandDescription
mails workspaces listList the workspaces you have signed in to. Also: mails workspaces ls.
mails workspaces currentShow the workspace the CLI is using.
mails workspaces switch <workspace>Use another workspace you have signed in to. <workspace>: Workspace slug, id or profile name. Also: mails workspaces use.
mails workspaces list
mails workspaces current
mails workspaces switch acme

Scripts and AI agents

People get tables and everything else gets JSON. When stdout is not a terminal (a pipe, a file, an agent), a command prints JSON only, except mails emails raw, which prints text, mails receiving attachment, which writes the file itself, mails completion, which prints a shell script, and --help, which is always text. --json forces JSON in a terminal, and --no-json keeps the table in a pipe.

Errors are JSON on stderr, with exit code 1:

{ "error": { "code": "invalid_api_key", "message": "API key not recognized.", "request_id": "req_…", "status": 401, "hint": "…" } }
  • mails commands --json lists every command with its arguments, flags, types, allowed values, examples, the API route behind it and the key scope it needs.
  • Bodies can come from files or stdin: --text-file, --html-file, --body-file, or --data - to pipe a JSON body in. Flags given alongside override its fields.
  • Sends carry an idempotency key, so a retry after a dropped connection never sends twice. Network failures, 429s and 5xx answers on reads are retried with backoff.
  • --dry-run prints the request a command would make, without the credential and with file contents shortened, instead of sending it. Only the commands that call one API route do this; the others, such as mails login, mails logout or mails events tail, ignore it and run for real. --all follows pagination to the end.
mails receiving list --json | jq -r '.data[0].subject'

# Wait for one inbound email, then exit
mails events tail --type message.received --count 1 --json

cat batch.json | mails emails batch --data -

Emails

Send email and look up what was sent. Also available as mails messages.

mails emails list

List sent emails. Also: mails emails ls, mails messages list.

FlagDescription
--limit <n>Page size (1–100). Default: 25.
--cursor <value>Opaque cursor from a previous page's next_cursor.
--agent-id <value>Filter by agent id or name. A key tied to one agent always reads that agent's mail, and naming another agent is a 403. Also: --agent.
--thread-id <value>Filter by thread id.
--q <value>Case-insensitive substring of the subject, body or recipient addresses. For word search across all mail, use GET /v1/search.
--allFetch every page, not just the first.
mails emails list --limit 10
mails emails list --agent-id hello --all --json

Needs a key with the read scope. API: GET /v1/messages. --all follows every page.

mails emails send

Send an email. Also: mails messages send.

Sends an email from one of your agents. With a mk_test_ key the message is stored and its message.sent event reaches your webhooks (marked test_mode: true), but no email is sent and nothing is billed. Quota, suppression and rate-limit refusals are skipped, and a message the cold-email firewall would refuse comes back 201 with classifier_warning instead.

FlagDescription
--idempotency-key <value>Replays the stored response for a repeated key (kept 24 hours), so a retried request never double-sends; the replay carries Idempotent-Replay: true.
--agent <value>Which agent sends this — its name or agt_ id.
--from <value>The sender you want, as a handle or address — billing, billing@acme.com and Acme <billing@acme.com> all mean the agent called billing.
--to <a,b,…>Required. Who the message goes to: one address, or a list of 1 to 50.
--cc <a,b,…>Addresses to copy, up to 50. Every recipient sees them.
--bcc <a,b,…>Addresses to blind-copy, up to 50. They receive the message but appear in none of its headers.
--subject <value>Required. Subject line, 1 to 998 characters.
--text <value>Plain-text body, up to 1,000,000 characters. At least one of body_text and body_html is required, or their aliases text and html. Also: --body-text.
--text-file <value>Read the plain-text body from a file (- for stdin).
--html <value>HTML body, up to 1,000,000 characters. With body_text as well, the email carries a plain-text part and an HTML part. Also: --body-html.
--html-file <value>Read the HTML body from a file (- for stdin).
--reply-to <value>The address replies should go to. Left out, replies go to the agent's receiving_address.
--in-reply-to-message-id <value>Id of the message this one answers (msg_… or rcv_…), named in the email's In-Reply-To header. The send joins its thread when the same agent sent or received it.
--references <a,b,…>Message-IDs for the References header. Write one from another system with its angle brackets (<…>); a value without them is read as a mails.ai message id.
--attach <file>A local file (about 3 MB in all, sent inside the request) or an https URL that mails.ai fetches (25 MB in all). Repeat for several. Not with a future --scheduled-at. Up to 10. Also: --attachment, --attachments.
--inline <file>A file to show inside the HTML, as id=file or id=https-url: <img src="cid:id"> in --html shows it. Repeat the flag for several.
--metadata <key=value>Your own key-value strings, kept with the message and returned by GET /v1/messages/{id}; they are not part of the email. Keys up to 64 characters, values up to 500.
--scheduled-at <value>Send at this time (ISO 8601) instead of now. A time in the past sends at once. A future time cannot be combined with attachments.
--tag <name=value>Up to 10 name-value labels, kept with the message and included in its message.sent event; they are not part of the email. Also: --tags.
--list-unsubscribeAdd one-click unsubscribe (RFC 8058), for newsletters and digests. One click suppresses the address workspace-wide. Only for exactly one recipient, with no Cc or Bcc.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails emails send --to jordan@example.com --subject "Your order shipped" --text "It is on its way."
mails emails send --from hello --to a@example.com,b@example.com --subject "Hello" --html-file ./hello.html --attach ./invoice.pdf
cat message.json | mails emails send --data -

Needs a key with the send scope. API: POST /v1/messages.

mails emails batch

Send up to 100 emails in one request. Also: mails messages batch.

Sends up to 100 messages in one request. Per-message failures are returned inline in data[] and do not fail the batch; the HTTP status is still 201. Each failure is the error a single send would answer: a message a send limit refused carries resets_at and retry_after_seconds, one the email service would not take for now (503 upstream_unavailable) retry_after_seconds, and a plan refusal upgrade_url.

The body is a JSON array of up to 100 messages, each with the fields of mails emails send except attachments: an item with attachments refuses the whole batch with 422.

FlagDescription
--idempotency-key <value>Replays the stored response for a repeated key (kept 24 hours), so a retried request never double-sends; the replay carries Idempotent-Replay: true.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails emails batch --body-file ./batch.json
cat batch.json | mails emails batch --data -

Needs a key with the send scope. API: POST /v1/messages/batch.

mails emails appeal

Ask for a second review of a send refused as cold outreach. Also: mails messages appeal.

Requests a second, independent review of content refused with cold_email_prohibited. Submit the SAME subject and body the refused send used.

FlagDescription
--agent <value>Agent name or id the refused send used. Optional — omitted, the workspace's single agent is assumed (the refused send itself created one if none existed).
--subject <value>The refused message's subject, verbatim.
--text <value>The refused message's body_text, verbatim. Also: --body-text.
--text-file <value>Read the plain-text body from a file (- for stdin).
--html <value>The refused message's body_html, verbatim. Also: --body-html.
--html-file <value>Read the HTML body from a file (- for stdin).
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails emails appeal --subject "Your invoice" --body-text "Hi Jordan, your invoice is attached."

Needs a key with the send scope. API: POST /v1/messages/appeal.

mails emails get

Show one sent email. Also: mails messages get.

ArgumentDescription
<id>Required. Sent message id (msg_…).
mails emails get msg_01JABC

Needs a key with the read scope. API: GET /v1/messages/{id}.

mails emails reschedule

Change when a scheduled email goes out. Also: mails emails update, mails messages reschedule.

Moves a scheduled send to a new future scheduled_at, and raises message.scheduled again with the new scheduled_at and the previous_scheduled_at it replaced. It needs the send scope as well as manage, and a live key for a live message (403 live_mode_required); the key that scheduled the send keeps answering for it.

ArgumentDescription
<id>Required. Sent message id with status scheduled, or rejected because its agent was paused or archived.
FlagDescription
--scheduled-at <value>The new send time: an ISO 8601 UTC time ending in Z, which must be in the future (400 invalid_field otherwise). Left out, the schedule stays as it is.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails emails reschedule msg_01JABC --scheduled-at 2026-12-01T09:00:00Z

Needs a key with the manage and send scope. API: PATCH /v1/messages/{id}.

mails emails reply

Reply in the same thread to a sent or received email. Also: mails messages reply.

Replies in-thread to a sent or received message. Sets In-Reply-To, References, and a Re: subject server-side. A reply cannot carry attachments (422 invalid_field, param attachments): to reply with files, send with POST /v1/messages and in_reply_to_message_id.

ArgumentDescription
<id>Required. The message being replied to (msg_… or rcv_…).
FlagDescription
--idempotency-key <value>Replays the stored response for a repeated key (kept 24 hours), so a retried request never double-sends; the replay carries Idempotent-Replay: true.
--text <value>Plain-text body of the reply, up to 1,000,000 characters, sent as written: the original is not quoted. Also: --body-text.
--text-file <value>Read the plain-text body from a file (- for stdin).
--html <value>HTML body of the reply, up to 1,000,000 characters, sent as written. With body_text as well, the email carries both parts. Also: --body-html.
--html-file <value>Read the HTML body from a file (- for stdin).
--reply-allAlso copy the original's other recipients: on a received message its To and Cc addresses, on one of your sent messages its Cc. Default false.
--cc <a,b,…>Addresses to copy, up to 50. With reply_all, the original's other recipients join these.
--bcc <a,b,…>Addresses to blind-copy, up to 50. They receive the reply but appear in none of its headers.
--tag <name=value>Up to 10 name-value labels, kept with the reply and included in its message.sent event; they are not part of the email. Also: --tags.
--scheduled-at <value>Send the reply at this time instead of now: an ISO 8601 UTC time ending in Z. A time in the past sends at once.
--list-unsubscribeAdd one-click unsubscribe (RFC 8058), for newsletters and digests. One click suppresses the address workspace-wide. Only for exactly one recipient, with no Cc or Bcc.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails emails reply rcv_01JABC --text "Thanks, that works for us."
mails emails reply rcv_01JABC --text-file ./reply.txt --reply-all

Needs a key with the send scope. API: POST /v1/messages/{id}/reply.

mails emails forward

Forward a sent or received email to new recipients. Also: mails messages forward.

Forwards a sent or received message to new recipients with a quoted body and Fwd: subject. Always sends immediately. Both parts carry the forwarded message: the HTML part is the covering note (body_html, else body_text), the forwarded header and the original (its own HTML, or its text), sent whenever there is HTML to carry.

ArgumentDescription
<id>Required. The message being forwarded (msg_… or rcv_…).
FlagDescription
--idempotency-key <value>Replays the stored response for a repeated key (kept 24 hours), so a retried request never double-sends; the replay carries Idempotent-Replay: true.
--to <a,b,…>Required. Who the forward goes to: one address, or a list of 1 to 50.
--cc <a,b,…>Addresses to copy, up to 50. Every recipient sees them.
--bcc <a,b,…>Addresses to blind-copy, up to 50. They receive the forward but appear in none of its headers.
--subject <value>Subject of the forward, 1 to 998 characters. Left out, it is Fwd: and the original's subject, kept as it is when that already starts with Fwd: or Fw:.
--text <value>Your note, up to 1,000,000 characters, placed above a ---------- Forwarded message ---------- block that quotes the original's From, To, Subject and plain text. Also: --body-text.
--text-file <value>Read the plain-text body from a file (- for stdin).
--html <value>Your note as HTML, up to 1,000,000 characters, placed above the same ---------- Forwarded message ---------- block as the plain-text part and then the original's own HTML (or its plain text when it has none). Also: --body-html.
--html-file <value>Read the HTML body from a file (- for stdin).
--list-unsubscribeAdd one-click unsubscribe (RFC 8058), for newsletters and digests. One click suppresses the address workspace-wide. Only for exactly one recipient, with no Cc or Bcc.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails emails forward rcv_01JABC --to warehouse@example.com --text "For your records."

Needs a key with the send scope. API: POST /v1/messages/{id}/forward.

mails emails cancel

Cancel a scheduled email before it goes out. Also: mails messages cancel.

Cancels a send that is still scheduled, so it is never sent. Once the send starts going out it can no longer be canceled: the cancel changes nothing and answers 409 duplicate_resource, also when the send started while the cancel was running. A test key cannot cancel a send a live key scheduled (403 live_mode_required).

ArgumentDescription
<id>Required. Sent message id with status scheduled.
mails emails cancel msg_01JABC

Needs a key with the manage scope. API: POST /v1/messages/{id}/cancel.

mails emails raw

Download an email as a raw .eml file. Also: mails messages raw.

Returns the message as a downloadable .eml attachment: the exact bytes as received when the raw message was kept (received mail up to 25 MiB), otherwise an RFC822 message rebuilt from the stored fields, with the Message-ID, From, Reply-To, In-Reply-To and References it went out or came in with.

ArgumentDescription
<id>Required. Sent (msg_…) or received (rcv_…) message id.
FlagDescription
-o, --output <value>Write the result to this file instead of stdout.
mails emails raw msg_01JABC --output message.eml
mails emails raw msg_01JABC | less

Needs a key with the read scope. API: GET /v1/messages/{id}/raw. Prints plain text, not JSON.

Receiving

Read received email, download attachments, listen for new mail.

mails receiving list

List received emails. Also: mails receiving ls.

FlagDescription
--limit <n>Page size (1–100). Default: 25.
--cursor <value>Opaque cursor from a previous page's next_cursor.
--agent-id <value>Filter by agent id or name. A key tied to one agent always reads that agent's mail, and naming another agent is a 403. Also: --agent.
--thread-id <value>Filter by thread id.
--q <value>Case-insensitive substring of the subject, body or sender address. For word search across all mail, use GET /v1/search.
--allFetch every page, not just the first.
mails receiving list
mails receiving list --agent-id hello --json | jq '.data[0].id'

Needs a key with the read scope. API: GET /v1/messages/received. --all follows every page.

mails receiving get

Show one received email.

ArgumentDescription
<id>Required. Received message id (rcv_…).
mails receiving get rcv_01JABC
mails receiving get rcv_01JABC --json | jq -r .extracted_text

Needs a key with the read scope. API: GET /v1/messages/received/{id}.

mails receiving attachments

List the attachments of a received email.

Every file in the message, embedded images included (inline: true). Files that could not be kept are listed under omitted.

ArgumentDescription
<id>Required. Received message id (rcv_…).
mails receiving attachments rcv_01JABC

Needs a key with the read scope. API: GET /v1/messages/received/{id}/attachments.

mails receiving attachment

Download one attachment of a received email.

The file's bytes, always as a download (Content-Disposition: attachment) with X-Content-Type-Options: nosniff. Content-Type is the declared type when it is on a safe list, else application/octet-stream; HTML and SVG are never served as themselves. ?inline=1 asks for inline display, granted only for PNG, JPEG, GIF, WebP, AVIF and BMP images.

ArgumentDescription
<id>Required. Received message id (rcv_…).
<attachment-id>Required. Attachment id (att_…).
FlagDescription
-o, --output <value>Write the result to this file instead of stdout.
mails receiving attachment rcv_01JABC att_01JABC --output invoice.pdf

Needs a key with the read scope. API: GET /v1/messages/received/{id}/attachments/{attachment_id}.

mails receiving simulate

Deliver a made-up inbound email to an agent (test key only).

Drives the receive/classify half of the product with a hand-supplied envelope: runs the real classifier, threads the message, persists a received row, and emits the same typed event + webhook a real inbound would. Set in_reply_to_message_id to a prior test-send id to simulate a reply.received.

FlagDescription
--agent <value>Required. The agent that receives the message: its name or agt_ id.
--from <value>Sender address. Required unless raw_base64 is sent, in which case it defaults to the raw email's From.
--from-name <value>Sender display name, up to 256 characters. Left out, it is the raw email's From name when raw_base64 is sent; otherwise the message has none.
--subject <value>Subject line, up to 2,000 characters. Left out, it is the raw email's Subject when raw_base64 is sent; otherwise the message has none.
--text <value>Message text. Required unless raw_base64 is sent, in which case it defaults to the raw email's text. Also: --body-text.
--text-file <value>Read the plain-text body from a file (- for stdin).
--raw-base64 <value>A complete raw email (RFC 822), base64-encoded, up to 3 MB decoded.
--in-reply-to-message-id <value>Id (msg_…) of one of this agent's sent messages, to simulate a reply to it.
--spf <value>Pretend SPF result, taken as given (default pass). One of: pass, fail, unknown.
--dkim <value>Pretend DKIM result, taken as given (default pass). With spf, both fail simulates message.received.unauthenticated. One of: pass, fail, unknown.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails receiving simulate --agent hello --from jordan@example.com --subject "Where is my order?" --body-text "Order 10473 has not arrived."

Needs a key with the send scope. Works only with a test key (mk_test_…). API: POST /v1/test/inbound.

mails receiving listen

Print new mail as it arrives. Also: mails receiving tail, mails receiving watch.

Waits for inbound email and prints who it is from, the subject and the start of the body (one JSON object per line when piped or with --json; add --full for the whole message). Mail from a forged sender (no DKIM signature aligned with the From domain verifies, and that domain's DMARC policy is quarantine or reject) is left out unless you add --include-unauthenticated. Stop it with Ctrl-C, --count or --timeout.

FlagDescription
--agent <value>Only mail for this agent. Also: --agent-id.
--fullFetch and print the whole message, not just the event.
--include-unauthenticatedAlso show mail from a forged sender (message.received.unauthenticated).
--since <value>Start after this event id, or from the position a stopped run printed, instead of from now.
--from-startReplay the workspace's whole event history first.
--position-file <value>Start where the last run that used this file stopped, and keep the place saved in it: runs one after another miss no event and repeat none.
-n, --count <n>Stop after this many events.
--timeout <n>Stop after this long (exit 0).
mails receiving listen
mails receiving listen --agent hello --json
mails receiving listen --count 1 --json | jq -r ".data.subject"

Agents

The addresses that send and receive.

mails agents list

List agents. Also: mails agents ls.

Lists the workspace's agents, newest first. A key tied to one agent lists that agent and no other.

FlagDescription
--limit <n>Page size (1–100). Default: 25.
--cursor <value>Opaque cursor from a previous page's next_cursor.
--status <value>Which agents to list. One of: live, all, active, paused, archived. Default: live.
--allFetch every page, not just the first.
mails agents list
mails agents list --status all --json

Needs a key with the read scope. API: GET /v1/agents. --all follows every page.

mails agents create

Create an agent: an address that sends and receives.

Creates an agent (a send/receive identity) at name@<workspace>.mails.ai, or at an address on a domain your workspace has verified for sending. Add forwarding: true when the mail service that already hosts that address will forward its mail to the agent's forwarding_address (no MX change), then test it with POST /v1/agents/{id}/forwarding/verify.

A new workspace starts with one agent, hello. The Free plan allows one agent, so another needs a paid plan: mails billing portal prints the link to upgrade.

ArgumentDescription
[name]The agent's name and the part of its address before the @: 1 to 32 lowercase letters, digits, ., _ or -.
FlagDescription
--domain <value>The domain of the agent's address. Left out, it is your workspace's <slug>.mails.ai, or the domain of address when that is given.
--address <value>The agent's address, on a domain your workspace has verified for sending (support@acme.com). The agent is named after its local part, so name may be left out.
--forwardingForwarding mode, for an address on your own domain: the mail service hosting it forwards its mail to the agent's forwarding_address, so no MX record changes.
--allowlist-domains <a,b,…>When set, this agent sends only to these domains and their subdomains (acme.com covers mail.acme.com). Up to 100.
--blocklist-domains <a,b,…>This agent never sends to these domains or their subdomains. Up to 100.
--daily-send-limit <n>This agent's own cap on recipients in any 24 hours, on top of your plan's cap: each live send counts once per to, cc and bcc address.
--hourly-send-limit <n>This agent's own cap on recipients in any hour, on top of your plan's cap: each live send counts once per to, cc and bcc address.
--classify-inboundAlso read this agent's new mail for intent, entities and urgency (never a reply in a thread). Needs a paid plan (402 feature_not_enabled). Off unless set.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails agents create support
mails agents create billing --domain mail.acme.com --daily-send-limit 5000
mails agents create --address support@acme.com --forwarding

Needs a key with the manage scope. API: POST /v1/agents.

mails agents get

Show one agent.

Returns an agent by its id or its name. A key tied to one agent gets any other agent as 404 agent_not_found, as if it did not exist, and a name other agents share is not reported to it as a 409.

ArgumentDescription
<id>Required. Agent id or name.
mails agents get hello

Needs a key with the read scope. API: GET /v1/agents/{id}.

mails agents update

Change an agent's settings, or pause it.

Changes an agent's status, recipient lists, send limits, or whether its new mail is read for intent. A pause by our abuse-prevention system or for the agent's own bounce or complaint rate cannot be lifted here (403 agent_suspended), and pausing such an agent again keeps that reason.

ArgumentDescription
<id>Required. Agent id or name.
FlagDescription
--status <value>paused makes the agent's send requests fail with 422 agent_paused; active resumes it. One of: active, paused.
--allowlist-domains <a,b,…>When set, this agent sends only to these domains and their subdomains (acme.com covers mail.acme.com). Up to 100.
--blocklist-domains <a,b,…>This agent never sends to these domains or their subdomains. Up to 100.
--daily-send-limit <n>This agent's own cap on recipients in any 24 hours, on top of your plan's cap: each live send counts once per to, cc and bcc address.
--hourly-send-limit <n>This agent's own cap on recipients in any hour, on top of your plan's cap: each live send counts once per to, cc and bcc address.
--classify-inboundTurns intent classification of new mail on or off (never replies in a thread). Turning it on needs a paid plan (402 feature_not_enabled); left out, it stays as it is.
--address <value>The agent's own address, which never changes: any other address answers 400. Accepted so a body that names it alongside forwarding works.
--forwardingForwarding mode, for an address on your own domain: the mail service hosting it forwards its mail to the agent's forwarding_address, so no MX record changes.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails agents update hello --status paused
mails agents update hello --daily-send-limit null

Needs a key with the manage scope. API: PATCH /v1/agents/{id}.

mails agents archive

Archive an agent (its address is kept and cannot be reused). Also: mails agents delete, mails agents rm.

Soft-deletes the agent (sets status to archived). The address is retained and cannot be reused, and archiving cannot be undone. The agent sends nothing more; mail that reaches it is still received, stored and sent to your webhooks as before, so none is lost.

ArgumentDescription
<id>Required. Agent id or name.
mails agents archive support --yes

Needs a key with the manage scope. API: DELETE /v1/agents/{id}. Asks first in a terminal, and needs --yes anywhere else.

mails agents verify-forwarding

Send a test through an agent's forward, to mark it verified.

For an agent in forwarding mode: sends one test email from mails.ai to the agent's own address. When your mail service forwards it to the agent's forwarding_address, it is dropped there (the agent never sees it) and forwarding_status becomes verified; if it has not arrived 15 minutes after it was sent, failing. Answers the agent at once: poll GET /v1/agents/{id}.

ArgumentDescription
<id>Required. Agent id or name.
mails agents verify-forwarding support

Needs a key with the manage scope. API: POST /v1/agents/{id}/forwarding/verify.

mails agents inbox

Show an agent's inbox summary and unread count.

Unread count, thread and unread counts per folder and per label, and the time of the latest message.

ArgumentDescription
<id>Required. Agent id or name.
mails agents inbox hello

Needs a key with the read scope. API: GET /v1/agents/{id}/inbox.

Threads

Conversations: status, labels, read state, folders.

mails threads list

List conversation threads. Also: mails threads ls.

FlagDescription
--limit <n>Page size (1–100). Default: 25.
--cursor <value>Opaque cursor from a previous page's next_cursor.
--agent-id <value>Filter by agent id or name. A key tied to one agent always reads that agent's mail, and naming another agent is a 403. Also: --agent.
--status <value>Filter by thread status (open|closed|archived).
--folder <value>Which folder's threads to return, or all for every folder. Without it: every folder except trash. One of: inbox, archive, spam, trash, sent, all.
--readfalse for unread threads only, true for read ones.
--label <value>Only threads carrying this label (a name, or a label id lbl_…).
--mode <value>live for threads that hold live mail, test for threads of test mail only (test_mode: true), all for both. One of: live, test, all.
--q <value>Case-insensitive substring of the subject or participant addresses. For word search across all mail, use GET /v1/search.
--allFetch every page, not just the first.
mails threads list --agent-id hello
mails threads list --status open --json

Needs a key with the read scope. API: GET /v1/threads. --all follows every page.

mails threads get

Show a thread with its messages.

ArgumentDescription
<id>Required. Thread id (thrd_…).
mails threads get thrd_01JABC

Needs a key with the read scope. API: GET /v1/threads/{id}.

mails threads update

Change a thread's status, labels, read state or folder.

Changes read, folder, status, labels and label_ids; emits thread.updated when something changed. folder: "archive" and status: "archived" move together, and moving an archived thread anywhere else reopens it. folder: "sent" is a 422: a thread is filed there while every message in it is one the agent sent. So is status: "archived" with any folder but archive.

ArgumentDescription
<id>Required. Thread id.
FlagDescription
--labels <a,b,…>Label names, up to 50, each at most 64 characters. Replaces the thread's labels. Any such name works; a name that matches a label object picks up its id and colour.
--label-ids <a,b,…>Label object ids (lbl_…). Replaces the thread's labels with those labels' names. Sent together with labels, the thread gets both. Up to 50.
--status <value>"archived" also files the thread in archive; "open" or "closed" on an archived thread moves it back to the inbox. One of: open, closed, archived.
--readMark the thread read (true) or unread (false).
--folder <value>Move the thread to inbox, archive, spam or trash. archive also sets status "archived", and moving an archived thread anywhere else reopens it. One of: inbox, archive, spam, trash.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails threads update thrd_01JABC --status closed --labels resolved,shipping

Needs a key with the manage scope. API: PATCH /v1/threads/{id}.

mails threads mark-read

Mark a thread as read.

ArgumentDescription
<id>Required. Thread id.
mails threads mark-read thrd_01JABC

Needs a key with the manage scope. API: PATCH /v1/threads/{id}.

mails threads mark-unread

Mark a thread as unread.

ArgumentDescription
<id>Required. Thread id.
mails threads mark-unread thrd_01JABC

Needs a key with the manage scope. API: PATCH /v1/threads/{id}.

mails threads restore

Move a thread back to the inbox.

ArgumentDescription
<id>Required. Thread id.
mails threads restore thrd_01JABC

Needs a key with the manage scope. API: PATCH /v1/threads/{id}.

mails threads spam

Move a thread to spam.

ArgumentDescription
<id>Required. Thread id.
mails threads spam thrd_01JABC

Needs a key with the manage scope. API: PATCH /v1/threads/{id}.

mails threads delete

Move a thread to trash, or delete it for good with --permanent. Also: mails threads rm, mails threads trash.

Moves the thread to trash (deleted: false, the thread returned). Sent again for a thread already in trash it changes nothing, so a retried request never destroys mail. With permanent=true it deletes the thread for good, from any folder: its messages' content, raw email and attachments are removed, and thread.deleted is emitted. Trash is also emptied automatically 30 days after a thread was moved there.

ArgumentDescription
<id>Required. Thread id.
FlagDescription
--permanenttrue deletes the thread for good instead of moving it to trash.
mails threads delete thrd_01JABC
mails threads delete thrd_01JABC --permanent --yes

Needs a key with the manage scope. API: DELETE /v1/threads/{id}. With --permanent it asks first in a terminal, and needs --yes anywhere else.

Drafts

Compose, edit, schedule and send drafts.

mails drafts list

List drafts. Also: mails drafts ls.

FlagDescription
--limit <n>Page size (1–100). Default: 25.
--cursor <value>Opaque cursor from a previous page's next_cursor.
--agent-id <value>Filter by agent id or name. A key tied to one agent always reads that agent's mail, and naming another agent is a 403. Also: --agent.
--status <value>Filter by draft status.
--allFetch every page, not just the first.
mails drafts list

Needs a key with the read or draft scope. API: GET /v1/drafts. --all follows every page.

mails drafts create

Save a draft, or schedule it with --send-at.

Creates a draft, or a scheduled draft if send_at is in the future. Scheduling sends the draft at that time, so a send_at needs the send scope: a key with only draft gets 403 insufficient_scope. A draft cannot carry attachments (422 invalid_field, param attachments); send files with POST /v1/messages.

FlagDescription
--agent <value>Which agent sends the draft: its name or agt_ id.
--to <a,b,…>Required. Who the draft goes to: one address, or a list of 1 to 50. It is stored and returned as a list.
--cc <a,b,…>Addresses to copy when the draft is sent, up to 50.
--bcc <a,b,…>Addresses to blind-copy when the draft is sent, up to 50. They appear in none of the email's headers.
--subject <value>Subject line, 1 to 998 characters. A draft without one is sent with an empty subject.
--text <value>Plain-text body, up to 1,000,000 characters. A draft can be saved without a body. Also: --body-text.
--text-file <value>Read the plain-text body from a file (- for stdin).
--html <value>HTML body, up to 1,000,000 characters. A draft can be saved without a body. Also: --body-html.
--html-file <value>Read the HTML body from a file (- for stdin).
--reply-to <value>The address replies to the sent draft should go to. Left out, replies go to the agent's receiving_address.
--in-reply-to-message-id <value>Id of the message this one answers (msg_… or rcv_…), named in the email's In-Reply-To header.
--send-at <value>Schedule the draft to send at this time: an ISO 8601 UTC time ending in Z, in the future (400 invalid_field otherwise). It needs the send scope.
--tag <name=value>Up to 10 name-value labels, copied onto the message when the draft is sent; they are not part of the email. Also: --tags.
--list-unsubscribeAdd one-click unsubscribe (RFC 8058), for newsletters and digests. One click suppresses the address workspace-wide. Only for exactly one recipient, with no Cc or Bcc.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails drafts create --to jordan@example.com --subject "Following up" --text "Just checking in."

Needs a key with the send or draft scope. API: POST /v1/drafts.

mails drafts get

Show one draft.

ArgumentDescription
<id>Required. Draft id (dft_…).
mails drafts get dft_01JABC

Needs a key with the read or draft scope. API: GET /v1/drafts/{id}.

mails drafts update

Edit a draft.

Edits a draft or scheduled draft. Setting a future send_at schedules it; clearing it returns to draft. A draft that starts sending while it is being changed answers 409 duplicate_resource. Without the send scope a key can neither schedule a draft nor change a scheduled one, except to unschedule it with send_at: null (403 insufficient_scope).

ArgumentDescription
<id>Required. Draft id.
FlagDescription
--to <a,b,…>Replaces the recipients: one address, or a list of up to 50. Left out, they stay as they are.
--cc <a,b,…>Replaces the Cc list, up to 50 addresses; [] empties it. Left out, it stays as it is.
--bcc <a,b,…>Replaces the Bcc list, up to 50 addresses; [] empties it. Left out, it stays as it is.
--subject <value>Replaces the subject, 1 to 998 characters. Left out, it stays as it is.
--text <value>Replaces the plain-text body, up to 1,000,000 characters. Left out, it stays as it is. Also: --body-text.
--text-file <value>Read the plain-text body from a file (- for stdin).
--html <value>Replaces the HTML body, up to 1,000,000 characters. Left out, it stays as it is. Also: --body-html.
--html-file <value>Read the HTML body from a file (- for stdin).
--send-at <value>A future time (ISO 8601, UTC, ending in Z) schedules the draft, and null unschedules it back to draft; left out, the schedule stays as it is.
--list-unsubscribeAdd one-click unsubscribe (RFC 8058), for newsletters and digests. One click suppresses the address workspace-wide. Only for exactly one recipient, with no Cc or Bcc.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails drafts update dft_01JABC --subject "Quick follow-up"

Needs a key with the manage or draft scope. API: PATCH /v1/drafts/{id}.

mails drafts delete

Delete a draft. Also: mails drafts rm.

Deletes a draft. A draft that is being sent cannot be deleted (409 duplicate_resource), and a test key cannot delete one a live key made (403 live_mode_required).

ArgumentDescription
<id>Required. Draft id.
mails drafts delete dft_01JABC --yes

Needs a key with the manage or draft scope. API: DELETE /v1/drafts/{id}. Asks first in a terminal, and needs --yes anywhere else.

mails drafts send

Send a draft now, or schedule it with --send-at.

Sends a draft immediately, or schedules it when the request body supplies a future send_at. A test key cannot send or schedule a draft a live key made (403 live_mode_required).

ArgumentDescription
<id>Required. Draft id.
FlagDescription
--idempotency-key <value>Replays the stored response for a repeated key (kept 24 hours), so a retried request never double-sends; the replay carries Idempotent-Replay: true.
--send-at <value>Send the draft at this time instead of now: an ISO 8601 UTC time ending in Z, in the future (400 invalid_field otherwise). Left out, the draft is sent at once.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails drafts send dft_01JABC
mails drafts send dft_01JABC --send-at 2026-12-01T09:00:00Z

Needs a key with the send scope. API: POST /v1/drafts/{id}/send.

Labels

Labels for threads.

mails labels list

List labels. Also: mails labels ls.

Every label in the workspace (at most 200), by name.

mails labels list

Needs a key with the read scope. API: GET /v1/labels.

mails labels create

Create a label.

A name (unique in the workspace, ignoring case) and a colour from a fixed palette of 12.

ArgumentDescription
<name>Required. Unique within the workspace, ignoring case. 1-64 characters.
FlagDescription
--color <value>The label's colour, one of 12 palette names. Left out, it is gray. One of: gray, red, orange, amber, yellow, green, teal, cyan, blue, indigo, purple, pink.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails labels create billing --color blue

Needs a key with the manage scope. API: POST /v1/labels.

mails labels get

Show one label.

ArgumentDescription
<id>Required. Label id (lbl_…).
mails labels get lbl_01JABC

Needs a key with the read scope. API: GET /v1/labels/{id}.

mails labels update

Rename or recolor a label.

Renaming a label renames it on every thread that carries it.

ArgumentDescription
<id>Required. Label id (lbl_…).
FlagDescription
--name <value>Renaming a label renames it on every thread that carries it.
--color <value>A new colour from the same palette of 12. Left out, the colour stays as it is; a request must carry name, color or both. One of: gray, red, orange, amber, yellow, green, teal, cyan, blue, indigo, purple, pink.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails labels update lbl_01JABC --name invoices

Needs a key with the manage scope. API: PATCH /v1/labels/{id}.

mails labels delete

Delete a label. Also: mails labels rm.

Deletes the label and takes it off every thread that carries it.

ArgumentDescription
<id>Required. Label id (lbl_…).
mails labels delete lbl_01JABC --yes

Needs a key with the manage scope. API: DELETE /v1/labels/{id}. Asks first in a terminal, and needs --yes anywhere else.

Search received and sent email.

Search received and sent email.

Full-text search over received and sent mail (subject, body text and addresses), newest first. Each hit says whether it was received or sent and carries a snippet around the match. Never leaves the workspace; a key tied to one agent searches that agent's mail; a test key searches test mail only and a live key live mail only.

ArgumentDescription
[q]Full-text search: every word must appear in the subject, body or addresses, as a whole word or, from three letters up, the start of one (inv finds invoice).
FlagDescription
--agent-id <value>Filter by agent id or name. A key tied to one agent always reads that agent's mail, and naming another agent is a 403. Also: --agent.
--from <value>The sender's address contains this text (the display name, which the sender chooses, is not matched).
--to <value>A recipient address contains this text.
--after <value>Only mail at or after this time (ISO 8601, or YYYY-MM-DD).
--before <value>Only mail before this time (ISO 8601, or YYYY-MM-DD).
--has-attachmenttrue for mail with a file attachment, false for mail without.
--folder <value>Which folder's mail to return, or all for every folder. Without it: every folder except trash. One of: inbox, archive, spam, trash, sent, all.
--label <value>Only mail in threads carrying this label (a name, or a label id lbl_…).
--type <value>Only received or only sent mail. One of: received, sent.
--limit <n>Page size (1–100). Default: 25.
--cursor <value>Opaque cursor from a previous page's next_cursor.
--allFetch every page, not just the first.
mails search "refund"
mails search invoice --from billing@acme.com --has-attachment --json

Needs a key with the read scope. API: GET /v1/search. --all follows every page.

Events

Every event, and a live tail.

mails events list

List events. Also: mails events ls.

FlagDescription
--limit <n>Page size (1–100). Default: 25.
--cursor <value>Opaque cursor from a previous page's next_cursor.
--event-type <value>Filter by exact event type.
--agent-id <value>Filter by agent id or name. Also: --agent.
--since <value>Only events created at or after this time: an ISO 8601 time such as 2026-10-05T07:00:00Z or 2026-10-05T16:00:00+09:00, or a date (2026-10-05, midnight UTC).
--allFetch every page, not just the first.
mails events list --event-type message.received --limit 5

Needs a key with the read scope. API: GET /v1/events. --all follows every page.

mails events get

Show one event.

ArgumentDescription
<id>Required. Event id (evt_…).
mails events get evt_01JABC

Needs a key with the read scope. API: GET /v1/events/{id}.

mails events redeliver

Send an event to your webhooks again.

Re-dispatches a stored event to all matching webhook endpoints. Needs a key that is not tied to an agent, since an endpoint can receive every agent's mail (403 insufficient_scope).

ArgumentDescription
<id>Required. Event id.
mails events redeliver evt_01JABC

Needs a key with the manage scope. API: POST /v1/events/{id}/redeliver.

mails events tail

Print events as they happen. Also: mails events listen, mails events stream.

Follows the workspace's live event stream and prints one line per event (one JSON object per line when piped or with --json). It reconnects by itself and never misses or repeats an event. Stop it with Ctrl-C, --count or --timeout.

FlagDescription
--type <a,b,…>Only these event types. A trailing * matches a family, e.g. message.* Also: --event-types, --types.
--since <value>Start after this event id, or from the position a stopped run printed, instead of from now.
--from-startReplay the workspace's whole event history first.
--position-file <value>Start where the last run that used this file stopped, and keep the place saved in it: runs one after another miss no event and repeat none.
-n, --count <n>Stop after this many events.
--timeout <n>Stop after this long (exit 0).
mails events tail
mails events tail --type message.received,reply.received
mails events tail --json --count 1 --timeout 60 | jq .type

Webhooks

Endpoints, deliveries, and forwarding events to localhost.

mails webhooks list

List webhook endpoints. Also: mails webhooks ls.

FlagDescription
--limit <n>Page size (1–100). Default: 25.
--cursor <value>Opaque cursor from a previous page's next_cursor.
--allFetch every page, not just the first.
mails webhooks list

Needs a key with the read scope. API: GET /v1/webhooks. --all follows every page.

mails webhooks create

Register an HTTPS endpoint to receive events.

Registers an HTTPS endpoint to receive events. The signing_secret is returned ONCE — use it to verify signatures. An empty event_types list is refused with 422 invalid_param_value: an endpoint with no event types would never be sent anything. Closed to apps connected by sign-in (403 connected_app_not_allowed): use the dashboard, an API key or the mails CLI.

The signing secret is shown once, in this result. Keep it: it is how your handler verifies deliveries.

ArgumentDescription
<url>Required. The https:// URL events are POSTed to. Its host must resolve to public IP addresses only.
FlagDescription
--event-types <a,b,…>Events to deliver, at least one. "*" is every event except thread.created, thread.updated, thread.deleted and message.delayed, sent only to an endpoint that names them. Up to 50.
--description <value>A note of your own to tell endpoints apart, up to 255 characters. Left out, description is null.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails webhooks create https://api.acme.com/hooks/mails --event-types message.received,reply.received

Needs a key with the manage scope. API: POST /v1/webhooks.

mails webhooks get

Show one webhook endpoint.

ArgumentDescription
<id>Required. Webhook endpoint id (whe_…).
mails webhooks get whe_01JABC

Needs a key with the read scope. API: GET /v1/webhooks/{id}.

mails webhooks update

Change a webhook endpoint, or switch it off.

Changes an endpoint's URL, event types, description or active state. An empty event_types list is refused with 422 invalid_param_value, as on create. An app connected by sign-in may change everything except url (403 connected_app_not_allowed).

ArgumentDescription
<id>Required. Webhook endpoint id (whe_…).
FlagDescription
--url <value>A new https:// URL for the endpoint, checked as on create. The signing secret stays the same, and retries still due go to the new URL; left out, the URL is unchanged.
--event-types <a,b,…>Events to deliver, at least one. "*" is every event except thread.created, thread.updated, thread.deleted and message.delayed, sent only to an endpoint that names them. Up to 50.
--description <value>Replaces the endpoint's note, up to 255 characters; left out, it is unchanged. null is refused: clear it with an empty string, which is returned as it is.
--activefalse pauses the endpoint: nothing is delivered to it, events raised meanwhile are not sent when it resumes, and a retry that comes due is dead-lettered.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails webhooks update whe_01JABC --active false

Needs a key with the manage scope. API: PATCH /v1/webhooks/{id}.

mails webhooks delete

Delete a webhook endpoint and its delivery history. Also: mails webhooks rm.

Deletes the endpoint and its delivery history.

ArgumentDescription
<id>Required. Webhook endpoint id.
mails webhooks delete whe_01JABC --yes

Needs a key with the manage scope. API: DELETE /v1/webhooks/{id}. Asks first in a terminal, and needs --yes anywhere else.

mails webhooks rotate-secret

Issue a new signing secret (the old one keeps working for 24 hours).

Issues a new signing secret, returned once. For the next 24 hours every delivery's X-Mails-Signature carries a v1 signature from the new secret and one from the previous secret, so a receiver can switch over without failing a delivery. Verify by accepting the header when any v1 matches. Rotating again within the 24 hours retires the oldest secret.

ArgumentDescription
<id>Required. Webhook endpoint id.
mails webhooks rotate-secret whe_01JABC

Needs a key with the manage scope. API: POST /v1/webhooks/{id}/rotate-secret.

mails webhooks deliveries

List delivery attempts for a webhook endpoint.

ArgumentDescription
<id>Required. Webhook endpoint id.
FlagDescription
--limit <n>Page size (1–100). Default: 25.
--cursor <value>Opaque cursor from a previous page's next_cursor.
--allFetch every page, not just the first.
mails webhooks deliveries whe_01JABC --limit 20

Needs a key with the read scope. API: GET /v1/webhooks/{id}/deliveries. --all follows every page.

mails webhooks test

Send a test event to a webhook endpoint.

Fires a synthetic webhook.test event at the endpoint and reports the delivery result.

ArgumentDescription
<id>Required. Webhook endpoint id.
mails webhooks test whe_01JABC

Needs a key with the manage scope. API: POST /v1/webhooks/{id}/test.

mails webhooks replay

Send the event behind a past delivery again.

Re-sends the event behind a previous delivery attempt to that delivery's endpoint only. Other endpoints subscribed to the event are not sent it again; to re-send an event to every subscribed endpoint, use POST /v1/events/{id}/redeliver.

ArgumentDescription
<delivery-id>Required. Webhook delivery id (whd_…).
mails webhooks replay whd_01JABC

Needs a key with the manage scope. API: POST /v1/webhook-deliveries/{id}/replay.

mails webhooks listen

Forward live events to a local address, signed like real deliveries. Also: mails webhooks forward.

Posts every event to the address you give (for example your handler on localhost), with the same body and the same X-Mails-Signature, X-Mails-Event-Id and X-Mails-Event-Type headers a real webhook delivery carries. No tunnel and no public address needed. Sign with your endpoint's real secret via --secret-env, or let the CLI make one for this session and print it for your handler to use.

FlagDescription
--forward-to <value>Required. Where to post each event, e.g. http://localhost:3000/webhooks/mails Also: --to, --url.
--events <a,b,…>Only these event types: without it, what an endpoint on * is sent. A trailing * matches a family (message.*), which a real endpoint's event_types cannot. Also: --type, --event-types.
--secret-env <value>Sign with the secret held in this environment variable (default MAILS_WEBHOOK_SECRET when it is set).
-H, --header <key=value>Extra header to send with each forward. Repeat for several.
--since <value>Start after this event id, or from the position a stopped run printed, instead of from now.
--from-startReplay the workspace's whole event history first.
--position-file <value>Start where the last run that used this file stopped, and keep the place saved in it: runs one after another miss no event and repeat none.
-n, --count <n>Stop after this many events.
--timeout <n>Stop after this long (exit 0).
mails webhooks listen --forward-to http://localhost:3000/webhooks/mails
mails webhooks listen --forward-to http://localhost:3000/hook --events message.received,reply.received
MAILS_WEBHOOK_SECRET=… mails webhooks listen --forward-to http://localhost:3000/hook   # reuse your endpoint's secret
Unless MAILS_WEBHOOK_SECRET is set (or --secret-env names another variable), each forwarded event is signed with a secret the CLI makes for this session and prints. Set MAILS_WEBHOOK_SECRET to your endpoint’s real signing secret instead, so the handler you test here verifies exactly as it will in production.

Domains

Your own sending domains and their DNS records.

mails domains list

List your domains. Also: mails domains ls.

mails domains list

Needs a key with the read scope. API: GET /v1/domains.

mails domains create

Add your own sending domain and get the DNS records to create. Also: mails domains add.

Registers a bring-your-own sending domain (paid plans). Returns the DNS records to create at your registrar — records-only verification, no nameserver changes. Agents whose address is on a VERIFIED custom domain send under their own identity instead of the shared workspace subdomain. A domain that is already added, to this workspace or to another, is refused with 400 duplicate_resource.

ArgumentDescription
<domain>Required. The domain to send from, such as mail.acme.com, 4 to 253 characters, lowercased and without a trailing dot.
FlagDescription
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails domains create mail.acme.com

Needs a key with the manage scope. API: POST /v1/domains.

mails domains get

Show a domain and its DNS records.

ArgumentDescription
<id>Required. Domain id (dom_…).
mails domains get dom_01JABC

Needs a key with the read scope. API: GET /v1/domains/{id}.

mails domains delete

Remove a domain. Also: mails domains rm.

Agents with addresses on this domain stop sending: every send from them is refused with 422 domain_not_verified until the domain is added and verified again, or the agent is recreated on your workspace's mails.ai subdomain. It is never sent from another domain instead.

ArgumentDescription
<id>Required. Domain id (dom_…).
mails domains delete dom_01JABC --yes

Needs a key with the manage scope. API: DELETE /v1/domains/{id}. Asks first in a terminal, and needs --yes anywhere else.

mails domains verify

Check a domain's DNS records now.

Re-checks every DNS record and updates per-record + overall status. Each call is an attempt: one that does not pass counts toward failed, 20 in a row mark the domain failed, and the count starts over on success or after a day without a call. Polling is rarely needed.

ArgumentDescription
<id>Required. Domain id (dom_…).
mails domains verify dom_01JABC

Needs a key with the manage scope. API: POST /v1/domains/{id}/verify.

API keys

Create, list and revoke API keys.

mails api-keys list

List API keys (prefixes only). Also: mails api-keys ls.

Lists the workspace's API keys, newest first, without their plaintext. A key tied to one agent lists only the keys tied to that same agent.

FlagDescription
--limit <n>Page size (1–100). Default: 25.
--cursor <value>Opaque cursor from a previous page's next_cursor.
--allFetch every page, not just the first.
mails api-keys list

Needs a key with the read scope. API: GET /v1/api-keys. --all follows every page.

mails api-keys create

Create an API key.

Creates a new API key. The plaintext key is returned ONCE in this response and never again. Closed to apps connected by sign-in (403 connected_app_not_allowed): use the dashboard, an API key or the mails CLI.

The API shows a new key only once, so say where it goes: --save-as <profile> stores it as a profile, --key-file <path> writes it to a private file, --show-key prints it. Without one of them nothing is created.

FlagDescription
--name <value>A label for the key, 1 to 80 characters, returned as name wherever the key is listed. It need not be unique; left out, name is null.
--agent-id <value>Tie the key to one agent.
--scopes <a,b,…>send sends mail, read reads it, and draft writes drafts but can neither send nor schedule one. One of: send, read, manage, draft.
--mode <value>live makes an mk_live_… key that sends real mail, and test an mk_test_… key whose sends are stored and raise events but are never transmitted or billed. Default live. One of: live, test.
--expires-at <value>When the key stops working: an ISO 8601 UTC time ending in Z. From then on it answers 401 expired_api_key; left out, the key never expires.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
--save-as <value>Store the new key as this saved profile (only its prefix is shown).
--key-file <value>Write the new key to this file, readable only by you (only its prefix is shown).
--show-keyPrint the whole new key, once.
mails api-keys create --mode test --save-as test
mails api-keys create --name "Production server" --scopes send,read --key-file ./mails.key
mails api-keys create --name "CI" --show-key

Needs a key with the manage scope. API: POST /v1/api-keys.

mails api-keys revoke

Revoke an API key at once. Also: mails api-keys delete, mails api-keys rm.

Revokes a key immediately. Idempotent — revoking an already-revoked key returns its existing revoked_at. A test key cannot revoke a live key (403 live_mode_required): revoke it with a live key or in the dashboard.

ArgumentDescription
<id>Required. API key id.
mails api-keys revoke key_01JABC --yes

Needs a key with the manage scope. API: DELETE /v1/api-keys/{id}. Asks first in a terminal, and needs --yes anywhere else.

Connected apps

List and revoke the apps connected by signing in.

mails connected-apps list

List the apps connected by signing in, this CLI among them. Also: mails connected-apps ls.

The apps connected to this workspace by signing in, whoever in it connected them: the mails CLI and apps such as Claude, ChatGPT or Cursor. Only those that can still act are listed, newest first: one revoked, or whose tokens have all expired or been signed out, is not. Settings › Connected apps in the dashboard shows the same apps to everyone in the workspace.

FlagDescription
--limit <n>Page size (1–100). Default: 25.
--cursor <value>Opaque cursor from a previous page's next_cursor.
--allFetch every page, not just the first.
mails connected-apps list

Needs a key with the manage scope. API: GET /v1/oauth/grants. --all follows every page.

mails connected-apps revoke

Revoke a connected app at once. Also: mails connected-apps delete, mails connected-apps rm.

Ends the sign-in, as Revoke in Settings › Connected apps does: every access and refresh token it was issued stops working at once, and the app has to be approved again to get back in. The API keys and webhook endpoints it created, and the webhook signing secrets it was shown, stay; the response counts them, so revoke and rotate those yourself.

API keys and webhooks the app created, and webhook signing secrets it was shown, stay: the result counts them, so revoke and rotate those too.

ArgumentDescription
<id>Required. The connected app's id (oag_…), from List connected apps.
mails connected-apps revoke oag_01JABC --yes

Needs a key with the manage scope. API: DELETE /v1/oauth/grants/{id}. Asks first in a terminal, and needs --yes anywhere else.

Suppressions

Addresses that must not be mailed, and the allow list.

mails suppressions list

List allow entries, or check one address with --address. Also: mails suppressions ls.

Three answers from one route. With ?address=, the suppression status of that address. With ?view=suppressed, which of your own recent recipients are suppressed for you, and why (up to 500, in one page; addresses are stored hashed, so the list itself cannot be browsed). Without either, the workspace allowlist (paginated).

FlagDescription
--address <value>Look up suppression status for this address instead of listing the allowlist.
--view <value>suppressed: list which of your own recent recipients are suppressed for you, and why, instead of the allowlist. One of: suppressed.
--limit <n>Page size (1–100). Default: 25.
--cursor <value>Opaque cursor from a previous page's next_cursor.
--allFetch every page, not just the first.
mails suppressions list
mails suppressions list --address jordan@example.com

Needs a key with the read scope. API: GET /v1/suppression. --all follows every page.

mails suppressions allow

Record that a suppressed recipient has agreed to receive mail again.

Attests that a recipient consents, overriding non-hard-bounce suppression for that address.

ArgumentDescription
<address>Required. The recipient to send to again despite a complaint or unsubscribe; a hard bounce is not lifted.
FlagDescription
--attestation <value>Required. Your statement that the recipient agreed to receive your mail, 20 to 2,000 characters. It is stored with the entry and returned with it.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails suppressions allow jordan@example.com --attestation "Reconfirmed opt-in in account settings on 2026-06-20."

Needs a key with the manage scope. API: POST /v1/suppression/allow.

mails suppressions revoke

Remove an allow entry. Also: mails suppressions delete, mails suppressions rm.

ArgumentDescription
<id>Required. Allowlist entry id (sup_…).
mails suppressions revoke sup_01JABC --yes

Needs a key with the manage scope. API: DELETE /v1/suppression/allow/{id}. Asks first in a terminal, and needs --yes anywhere else.

Metrics

Counts over time.

mails metrics

Sent, delivered, bounced, complained, received and reply counts over time.

Counts of sent, delivered, bounced, complained, received and reply messages per day or hour (UTC). Defaults to the last 30 days by day, or the last 48 hours by hour; at most 366 days or 744 hours per call. A test key counts test mail only and a live key live mail only.

FlagDescription
--from <value>Start of the range (ISO 8601, or YYYY-MM-DD).
--to <value>End of the range, exclusive (ISO 8601, or YYYY-MM-DD).
--agent-id <value>Filter by agent id or name. A key tied to one agent always reads that agent's mail, and naming another agent is a 403. Also: --agent.
--interval <value>Bucket size. One of: day, hour. Default: day.
mails metrics --from 2026-10-01 --to 2026-10-08 --interval day

Needs a key with the read scope. API: GET /v1/metrics.

Logs

Audit log.

mails logs

Show the audit log of changes made in the workspace.

Read-only audit trail of state-changing operations, and of requests the API refused with a 4xx once it knew the workspace (category api, action request.refused). A refusal before that, such as a missing or unknown key, is not recorded. The same refusal again (the same key, method, path and code) is recorded at most once a minute, a 429 once a minute per code, and a workspace records at most 30 refusals a minute.

FlagDescription
--limit <n>Page size (1–100). Default: 25.
--cursor <value>Opaque cursor from a previous page's next_cursor.
--category <value>Filter by audit category.
--action <value>Filter by audit action.
--allFetch every page, not just the first.
mails logs --limit 20
mails logs --category api_key --json

Needs a key with the read scope. API: GET /v1/logs. --all follows every page.

Usage

Usage this billing period.

mails usage

Show this billing period's usage against the plan's limits.

Returns metered usage for the current billing period plus the workspace's tier caps. A key tied to one agent cannot read it (403 insufficient_scope): usage and costs are counted for the whole workspace, and GET /v1/metrics counts that agent's mail.

mails usage

Needs a key with the read scope. API: GET /v1/billing/usage.

Reputation

Sender reputation.

mails reputation

Show sender reputation for the workspace or one agent.

Workspace-wide reputation when called without agent_id, or per-agent stats when an agent id/name is supplied.

FlagDescription
--agent-id <value>Agent id or name. Omit for the workspace aggregate. Also: --agent.
mails reputation
mails reputation --agent-id hello

Needs a key with the read scope. API: GET /v1/reputation.

Billing

Subscription.

mails billing portal

Get a link to manage or start the subscription.

Returns a Stripe Billing Portal URL when already subscribed, otherwise a Checkout URL to subscribe. A workspace on an active paid plan with no Stripe subscription on file gets 409 billing_state_inconsistent instead of a Checkout URL, since subscribing again would bill it twice: contact support@mails.ai.

FlagDescription
--return-url <value>Where the customer lands after leaving the billing portal, or after finishing or cancelling checkout: an absolute URL. Left out, it is the dashboard's /billing page.
--price-lookup-key <value>The plan checkout subscribes to: mailsai_pro_monthly (the default), mailsai_scale_monthly, mailsai_pro_yearly or mailsai_scale_yearly.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails billing portal

Needs a key with the manage scope. API: POST /v1/billing/portal.

Health

API status.

mails health

Check that the API is up.

Liveness probe. Without authentication it returns status (healthy when the database answers and accepts writes, else degraded) and timestamp. The dependency detail (database, sender, classifier) is returned only to internal callers.

mails health

Works without signing in. API: GET /api/health.

Utility

Checks, shortcuts, and setting up AI clients.

mails send

Send an email.

Sends an email from one of your agents. With a mk_test_ key the message is stored and its message.sent event reaches your webhooks (marked test_mode: true), but no email is sent and nothing is billed. Quota, suppression and rate-limit refusals are skipped, and a message the cold-email firewall would refuse comes back 201 with classifier_warning instead.

FlagDescription
--idempotency-key <value>Replays the stored response for a repeated key (kept 24 hours), so a retried request never double-sends; the replay carries Idempotent-Replay: true.
--agent <value>Which agent sends this — its name or agt_ id.
--from <value>The sender you want, as a handle or address — billing, billing@acme.com and Acme <billing@acme.com> all mean the agent called billing.
--to <a,b,…>Required. Who the message goes to: one address, or a list of 1 to 50.
--cc <a,b,…>Addresses to copy, up to 50. Every recipient sees them.
--bcc <a,b,…>Addresses to blind-copy, up to 50. They receive the message but appear in none of its headers.
--subject <value>Required. Subject line, 1 to 998 characters.
--text <value>Plain-text body, up to 1,000,000 characters. At least one of body_text and body_html is required, or their aliases text and html. Also: --body-text.
--text-file <value>Read the plain-text body from a file (- for stdin).
--html <value>HTML body, up to 1,000,000 characters. With body_text as well, the email carries a plain-text part and an HTML part. Also: --body-html.
--html-file <value>Read the HTML body from a file (- for stdin).
--reply-to <value>The address replies should go to. Left out, replies go to the agent's receiving_address.
--in-reply-to-message-id <value>Id of the message this one answers (msg_… or rcv_…), named in the email's In-Reply-To header. The send joins its thread when the same agent sent or received it.
--references <a,b,…>Message-IDs for the References header. Write one from another system with its angle brackets (<…>); a value without them is read as a mails.ai message id.
--attach <file>A local file (about 3 MB in all, sent inside the request) or an https URL that mails.ai fetches (25 MB in all). Repeat for several. Not with a future --scheduled-at. Up to 10. Also: --attachment, --attachments.
--inline <file>A file to show inside the HTML, as id=file or id=https-url: <img src="cid:id"> in --html shows it. Repeat the flag for several.
--metadata <key=value>Your own key-value strings, kept with the message and returned by GET /v1/messages/{id}; they are not part of the email. Keys up to 64 characters, values up to 500.
--scheduled-at <value>Send at this time (ISO 8601) instead of now. A time in the past sends at once. A future time cannot be combined with attachments.
--tag <name=value>Up to 10 name-value labels, kept with the message and included in its message.sent event; they are not part of the email. Also: --tags.
--list-unsubscribeAdd one-click unsubscribe (RFC 8058), for newsletters and digests. One click suppresses the address workspace-wide. Only for exactly one recipient, with no Cc or Bcc.
-d, --data <value>The request body as JSON (- reads it from stdin). Flags given alongside override its fields.
--body-file <value>Read the JSON request body from a file (- for stdin).
mails send --to jordan@example.com --subject "Your order shipped" --text "It is on its way."
mails send --from hello --to a@example.com,b@example.com --subject "Hello" --html-file ./hello.html --attach ./invoice.pdf
cat message.json | mails send --data -

Needs a key with the send scope. API: POST /v1/messages.

mails doctor

Check the CLI, your sign-in and the connection, and say how to fix what is wrong.

Checks the Node version, the CLI version, where your credential comes from, whether the API answers, your clock, what your key is allowed to do, and whether your custom domains are verified. Exits 1 when something is broken.

mails doctor
mails doctor --json

mails mcp install

Add the mails.ai MCP server to Claude, Claude Code, Cursor, Codex or VS Code. Also: mails mcp add.

Connects an AI client to the hosted mails.ai MCP server, so it can send and read email with your workspace. By default the client signs in through your browser; with --auth key the config names the MAILS_API_KEY environment variable instead. A key is never written into a config file. Any file this changes is copied to a .bak-<time> file first. --print shows what would be done and changes nothing.

FlagDescription
-c, --client <value>Required. Which client to set up. One of: claude, claude-code, cursor, codex, vscode.
--scope <value>user: for you, everywhere (default). project: a file in this folder that the team can share. One of: user, project.
--auth <value>oauth: sign in through the browser. key: read MAILS_API_KEY from the environment. auto (default) uses the browser when the server offers it. One of: auto, oauth, key.
--with-keySame as --auth key.
--printShow the config, the command and the next steps; change nothing.
--name <value>What to call the server in the client (default mails).
--url <value>The MCP address (default: the hosted server for your API address).
mails mcp install --client claude-code
mails mcp install --client cursor --scope project
mails mcp install --client codex --auth key
mails mcp install --client vscode --print

Works without signing in.

mails open

Open the dashboard (or one of its pages) in your browser.

ArgumentDescription
[page]One of: dashboard, agents, emails, messages, threads, drafts, events, webhooks, domains, api-keys, keys, suppressions, logs, billing, settings, mcp. Default: dashboard.
FlagDescription
--no-browserOnly print the address.
mails open
mails open api-keys
mails open webhooks --no-browser

Works without signing in.

mails docs

Open the documentation, or the API reference.

ArgumentDescription
[topic]api for the API reference, openapi for the OpenAPI document. Default: the docs.
FlagDescription
--no-browserOnly print the address.
mails docs
mails docs api
mails docs openapi --no-browser

Works without signing in.

mails update

Check for a newer CLI and print the command that installs it. Also: mails upgrade.

Asks npm for the newest version. It only prints the command to run: the CLI never changes its own files.

mails update

Works without signing in.

mails completion

Print a shell completion script (bash, zsh or fish).

The script asks the CLI itself for the words that fit, so it always matches the installed version: commands, flags, allowed values and your profile names.

ArgumentDescription
<shell>Required. bash, zsh or fish.
source <(mails completion zsh)
mails completion bash >> ~/.bashrc
mails completion fish > ~/.config/fish/completions/mails.fish

Works without signing in.

mails commands

Print every command (--json: the whole tree with flags and examples, for agents).

With --json (or when piped) this is the machine-readable map of the CLI: every command with its arguments, flags, types, allowed values, examples, the API route behind it and the key scope it needs. Give a few words to see only that part, e.g. mails commands emails.

ArgumentDescription
[prefix…]Only commands under these words.
mails commands
mails commands --json
mails commands webhooks --json | jq '.commands[].command'

Works without signing in.

Global options

Every command accepts these. --dry-run acts only on the commands that call one API route.

FlagDescription
--jsonPrint JSON. Automatic when stdout is not a terminal (--no-json keeps the table).
-q, --quietNo status lines or notes; implies --json.
-p, --profile <value>Use this saved profile (default: the active one, or MAILS_PROFILE).
--api-key <value>API key for this one command (beats MAILS_API_KEY and profiles).
--api-url <value>API address (default https://api.mails.ai, or MAILS_API_URL).
-y, --yesConfirm a destructive command without asking.
--dry-runPrint the HTTP request instead of sending it.
--debugLog every HTTP request and response status to stderr (credentials masked).
--colorForce color on (--no-color turns it off; NO_COLOR is honoured).
-h, --helpShow help for the command.
-v, --versionPrint the CLI version.

Environment variables

VariableWhat it does
MAILS_API_KEYAPI key to use (beats saved profiles).
MAILS_API_URLAPI address (default https://api.mails.ai).
MAILS_BASE_URLThe same, under the SDKs' name for it, read when MAILS_API_URL is not set.
MAILS_PROFILESaved profile to use.
MAILS_CONFIG_DIRWhere profiles and the credentials file live. Without it: %APPDATA%\mails on Windows, else $XDG_CONFIG_HOME/mails when that is set (to an absolute path), else ~/.config/mails.
MAILS_CREDENTIAL_STOREauto, keychain or file.
MAILS_WEBHOOK_SECRETSigning secret for mails webhooks listen.
MAILS_HTTP_TIMEOUTSeconds before a request gives up (default 60).
NO_COLORTurns color off.
Using the CLI with AI agentsThe mails agent skill teaches an agent this CLI, the SDKs and the MCP server.

Was this page helpful?