
Resend CLI
CLIResend 官方邮件 CLI:发送邮件,管理域名、API Key、联系人、群发与 Webhook,非终端环境自动输出 JSON,自带 agent skill。需要 Resend API Key,有永久免费套餐。
Resend CLI
The official CLI for Resend.
Built for humans, AI agents, and CI/CD pipelines.
██████╗ ███████╗███████╗███████╗███╗ ██╗██████╗
██╔══██╗██╔════╝██╔════╝██╔════╝████╗ ██║██╔══██╗
██████╔╝█████╗ ███████╗█████╗ ██╔██╗ ██║██║ ██║
██╔══██╗██╔══╝ ╚════██║██╔══╝ ██║╚██╗██║██║ ██║
██║ ██║███████╗███████║███████╗██║ ╚████║██████╔╝
╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝
Install
cURL
curl -fsSL https://resend.com/install.sh | bash
Node.js
npm install -g resend-cli
Homebrew (macOS / Linux)
brew install resend/cli/resend
PowerShell (Windows)
irm https://resend.com/install.ps1 | iex
Or download the .exe directly from the GitHub releases page.
Quickstart
# Authenticate
resend login
# Send an email
resend emails send \
--from "you@example.com" \
--to delivered@resend.dev \
--subject "Hello from Resend CLI" \
--text "Sent from my terminal."
# Check your environment
resend doctor
Agent skills
This CLI ships with an agent skill that teaches AI coding agents (Cursor, Claude Code, Windsurf, etc.) how to use the Resend CLI effectively, including non-interactive flags, output formats, and common pitfalls.
To install skills for Resend's full platform (API, CLI, React Email, email best practices) from the central skills repository:
npx skills add resend/resend-skills
Local development
Use this when you want to change the CLI and run your build locally.
Prerequisites
- Node.js 20+
Setup
-
Clone the repo
git clone https://github.com/resend/resend-cli.git cd resend-cli -
Install dependencies
pnpm install -
Build locally
pnpm buildOutput:
./dist/cli.cjs
Running the CLI locally
Use the dev script:
pnpm dev --version
Or run the built JS bundle:
node dist/cli.cjs --version
Making changes
After editing source files, rebuild:
pnpm build
Building native binaries
To build a standalone native binary:
pnpm build:bin
Output: ./dist/resend
Authentication
The CLI resolves your API key using the following priority chain:
| Priority | Source | How to set |
|---|---|---|
| 1 (highest) | --api-key flag | resend --api-key re_xxx emails send ... |
| 2 | RESEND_API_KEY env var | export RESEND_API_KEY=re_xxx |
| 3 (lowest) | Config file | resend login |
If no key is found from any source, the CLI errors with code auth_error.
Commands
resend login
Authenticate by storing your API key locally. The key is validated against the Resend API before being saved.
resend login
Interactive mode (default in terminals)
When run in a terminal, the command checks for an existing key:
- No key found: Offers to open the Resend API keys dashboard in your browser so you can create one, then prompts for the key.
- Existing key found: Shows the key source (
env,config) and prompts for a new key to replace it.
Enter the key via a masked password input. Your key must start with re_.
Non-interactive mode (CI, pipes, scripts)
The CLI is non-interactive when stdin or stdout is not a TTY, when CI or GITHUB_ACTIONS is set, or when TERM=dumb. In that mode, pass --web or --key.
--web logs in through the browser with no prompts in any mode, so AI agents can run it. The command tries to open the browser, then prints the authorize URL and the target profile to stderr. It waits up to 5 minutes for you to approve the login, then stores an OAuth grant:
resend login --web
- The browser must run on the same machine as the CLI, because Resend redirects to a local callback on
127.0.0.1. - Without
--profile, the login goes to thedefaultprofile and replaces its saved credentials. - The status text on stderr comes before any JSON error.
--key stores an API key:
resend login --key re_xxxxxxxxxxxxx
Omitting both flags in non-interactive mode exits with error code missing_key.
Options
| Flag | Description |
|---|---|
--web | Log in through the browser with no prompts |
--key <key> | API key to store (required in non-interactive mode, unless --web) |
Output
On success, credentials are saved to ~/.config/resend/credentials.json with 0600 permissions (owner read/write only). The config directory is created with 0700 permissions.
With --json, or when stdout is not a TTY, the result is JSON on stdout:
resend login --key re_xxx --json
# => {"success":true,"config_path":"...","profile":"default","storage":"...","permission":"full_access"}
resend login --web --json
# => {"success":true,"config_path":"...","profile":"default","storage":"...","scope":"full_access"}
Error codes
| Code | Cause |
|---|---|
missing_key | No --key or --web in non-interactive mode |
conflicting_flags | --web and --key are both set |
invalid_key_format | Key does not start with re_ |
validation_failed | Resend API rejected the key |
invalid_profile_name | The --profile name is not valid |
write_failed | The credentials could not be saved |
oauth_error | Browser login failed: the local callback did not start, access was denied, it timed out, or the token exchange failed |
oauth_state_mismatch | The browser callback did not match this login request |
Switch between teams and accounts
If you work across multiple Resend teams or accounts, the CLI handles that, too.
Switch between profiles without logging in and out:
resend auth switch
You can also use the global --profile (or -p) flag on any command to run it with a specific profile.
resend domains list --profile production
resend emails send
Send an email via the Resend API.
Provide all options via flags for scripting, or let the CLI prompt interactively for missing fields.
resend emails send \
--from "Name <sender@example.com>" \
--to delivered@resend.dev \
--subject "Subject line" \
--text "Plain text body"
Options
| Flag | Required | Description |
|---|---|---|
--from <address> | Yes | Sender email address (must be from a verified domain) |
--to <addresses...> | Yes | One or more recipient email addresses (space-separated) |
--subject <subject> | Yes | Email subject line |
--text <text> | One of text/html/html-file | Plain text body |
--html <html> | One of text/html/html-file | HTML body as a string |
--html-file <path> | One of text/html/html-file | Path to an HTML file to use as body |
--cc <addresses...> | No | CC recipients (space-separated) |
--bcc <addresses...> | No | BCC recipients (space-separated) |
--reply-to <address> | No | Reply-to email address |
Interactive mode
When run in a terminal without all required flags, the CLI prompts for missing fields:
# prompts for from, to, subject, and body
resend emails send
# prompts only for missing fields
resend emails send --from "you@example.com"
Non-interactive mode
When piped or run in CI, all required flags must be provided. Missing flags cause an error listing what's needed:
echo "" | resend emails send --from "you@example.com"
# Error: Missing required flags: --to, --subject
A body (--text, --html, or --html-file) is also required — omitting all three exits with code missing_body.
Examples
Multiple recipients:
resend emails send \
--from "you@example.com" \
--to delivered@resend.dev bounced@resend.dev \
--subject "Team update" \
--text "Hello everyone"
HTML from a file:
resend emails send \
--from "you@example.com" \
--to delivered@resend.dev \
--subject "Newsletter" \
--html-file ./newsletter.html
With CC, BCC, and reply-to:
resend emails send \
--from "you@example.com" \
--to delivered@resend.dev \
--subject "Meeting notes" \
--text "See attached." \
--cc manager@example.com \
--bcc delivered+1@resend.dev \
--reply-to support@example.com
Overriding the API key for one send:
resend --api-key re_other_key emails send \
--from "you@example.com" \
--to delivered@resend.dev \
--subject "Test" \
--text "Using a different key"
Output
Returns the email ID on success:
{ "id": "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794" }
Error codes
| Code | Cause |
|---|---|
auth_error | No API key found or client creation failed |
missing_body | No --text, --html, or --html-file provided |
file_read_error | Could not read the file passed to --html-file |
send_error | Resend API returned an error |
resend doctor
Run environment diagnostics. Verifies your CLI version, API key, domains, and detects AI agent integrations.
resend doctor
Checks performed
| Check | Pass | Warn | Fail |
|---|---|---|---|
| CLI Version | Running latest | Update available or registry unreachable | — |
| API Key | Key found (shows masked key and source) | — | No key found |
| Domains | Verified domains exist | No domains or all pending verification | API key invalid |
| AI Agents | Lists detected agents (or none) | — | — |
The API key is always masked in output (e.g. re_...xxxx).
Interactive mode
In a terminal, shows animated spinners for each check with colored status icons:
Resend Doctor
✔ CLI Version: v0.1.0 (latest)
✔ API Key: re_...xxxx (source: env)
✔ Domains: 2 verified, 0 pending
✔ AI Agents: Detected: Cursor, Claude Desktop
JSON mode
resend doctor --json
{
"ok": true,
"checks": [
{ "name": "CLI Version", "status": "pass", "message": "v0.1.0 (latest)" },
{
"name": "API Key",
"status": "pass",
"message": "re_...xxxx (source: env)"
},
{ "name": "Domains", "status": "pass", "message": "2 verified, 0 pending" },
{ "name": "AI Agents", "status": "pass", "message": "Detected: Cursor" }
]
}
Each check has a status of pass, warn, or fail. The top-level ok is false if any check is fail.
Detected AI agents
| Agent | Detection method |
|---|---|
| OpenClaw | ~/clawd/skills directory exists |
| Cursor | ~/.cursor directory exists |
| Claude Desktop | Platform-specific config file exists |
| VS Code | .vscode/mcp.json in current directory |
Exit code
Exits 0 when all checks pass or warn. Exits 1 if any check fails.
Webhooks
With the Resend CLI, you can manage webhook endpoints so your app receives real-time event notifications.
Payloads are signed with Svix headers (svix-id, svix-timestamp, svix-signature). Verify them in your app with the Resend SDK.
For example: resend.webhooks.verify({ payload, headers, webhookSecret })
There are many events that you can listen for in your application.
For example, you can:
- Set up a POST endpoint to unsubscribe users when an email bounces or they mark your email as spam.
- Notify yourself when you get a new subscriber using the
contact.createdevent. - Use an
email.receivedwebhook to set up an inbox for your agent and notify it when a new email is received.
Event types
| Category | Events |
|---|---|
email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.opened, email.clicked, email.failed, email.scheduled, email.suppressed, email.received | |
| Contact | contact.created, contact.updated, contact.deleted, contact.topics.updated |
| Domain | domain.created, domain.updated, domain.deleted |
| Topic | topic.created, topic.updated, topic.deleted |
| Inbox | inbox.created, inbox.updated, inbox.deleted, inbox.thread.created, inbox.email.received, inbox.email.sent, inbox.thread.folder.updated, inbox.thread.assigned, inbox.thread.unassigned, inbox.thread.labels.updated, inbox.draft.created, inbox.draft.updated, inbox.draft.sent, inbox.draft.deleted |
Use all with --events to subscribe to every event.
Subcommands
listcreategetupdatedeleterotate-signing-secretlistenevents(list,get,attempts,replay)
Aliases
webhooks ls→listwebhooks rm→deletewebhooks events ls→webhooks events list
resend webhooks list
Lists existing webhooks.
Running resend webhooks with no subcommand runs list.
| Flag | Description |
|---|---|
--limit <n> | Max webhooks to return (1–100, default 10) |
--after <cursor> | Return webhooks after this cursor (webhook ID; next page) |
--before <cursor> | Return webhooks before this cursor (previous page) |
Only one of --after or --before may be used. The API response includes has_more when more pages exist.
resend webhooks list
resend webhooks list --limit 25
resend webhooks list --after wh_abc123 --json
resend webhooks create
Registers a new endpoint.
The endpoint must use HTTPS. Use the signing_secret in the response to verify incoming payloads. resend webhooks get returns it again.
In interactive mode, the CLI can prompt for endpoint and events. In non-interactive mode (pipes, CI, --json), --endpoint and --events are required.
| Flag | Description |
|---|---|
--endpoint <url> | HTTPS URL that receives webhook POSTs |
--events <events...> | Event names (comma- or space-separated), or all |
resend webhooks create --endpoint https://app.example.com/hooks/resend --events all
resend webhooks create --endpoint https://app.example.com/hooks/resend --events email.sent email.bounced
resend webhooks create --endpoint https://app.example.com/hooks/resend --events email.sent,email.delivered
resend webhooks get
Fetches one webhook by ID.
Omit the ID in a terminal to pick from a list.
resend webhooks get wh_abc123
resend webhooks get wh_abc123 --json
The response includes the signing secret. Use resend webhooks rotate-signing-secret <id> to replace it.
resend webhooks update
Updates the webhook:
- endpoint URL
- the full event list
- delivery status
At least one of --endpoint, --events, or --status is required.
| Flag | Description |
|---|---|
--endpoint <url> | New HTTPS URL |
--events <events...> | New event list, or all |
--status <status> | enabled or disabled |
Disabled status pauses delivery without deleting the webhook.
resend webhooks update wh_abc123 --status disabled
resend webhooks update wh_abc123 --endpoint https://new-app.example.com/hooks/resend
resend webhooks update wh_abc123 --events email.sent email.bounced
resend webhooks delete
Deletes a webhook and stops deliveries.
In non-interactive mode, --yes is required to confirm.
resend webhooks delete wh_abc123 --yes
To pause delivery temporarily, prefer resend webhooks update <id> --status disabled.
resend webhooks rotate-signing-secret
Generates a new signing secret for the webhook and returns it. For 24 hours, payloads are signed with both the new and the previous secret, so either one verifies them. After that, only the new secret does.
Omit the ID in a terminal to pick from a list.
resend webhooks rotate-signing-secret wh_abc123
resend webhooks rotate-signing-secret wh_abc123 --json
resend webhooks listen
Built-in local development helper. It:
- Starts a small HTTP server
- Registers a temporary Resend webhook pointing at your public tunnel URL
- Prints events in the terminal
- Prints the temporary webhook's signing secret in the banner, so your
--forward-toapp can verify the Svix headers - Verifies Svix signatures before it prints or forwards events
- Deletes the webhook on exit
Your tunnel must forward to the same port as --port, e.g. ngrok http 4318.
Requests with missing or invalid signatures receive HTTP 400.
| Flag | Description |
|---|---|
--url <url> | Public URL (tunnel) that reaches this machine — required |
--port <port> | Local server port (default 4318) |
--events <events...> | Events to subscribe to (default: all) |
--forward-to <url> | Also POST each payload to this URL (Svix headers preserved) |
# Terminal 1: tunnel to the listen port
ngrok http 4318
# Terminal 2: use the HTTPS URL ngrok gives you
resend webhooks listen --url https://xxxx.ngrok-free.app
resend webhooks listen --url https://xxxx.ngrok-free.app --forward-to localhost:3000/api/webhooks/resend
resend webhooks listen --url https://xxxx.ngrok-free.app --port 8080 --events email.sent email.bounced
resend webhooks events
Inspects what Resend already delivered to a webhook. Use it to debug an endpoint that is missing events:
resend webhooks events list <webhook-id>— find the eventresend webhooks events get <webhook-id> <event-id>— see the payload we sentresend webhooks events attempts <webhook-id> <event-id>— see what your endpoint returnedresend webhooks events replay <webhook-id> <event-id>— queue another delivery
Running resend webhooks events <webhook-id> with no subcommand runs list.
Both list subcommands paginate forward only — there is no --before.
| Flag | Description |
|---|---|
--limit <n> | Max results to return (1–100, default 10) |
--after <cursor> | Return results after this ID (next page) |
events list reports each event's type, status (pending, attempting, success, failed), and created_at. events get adds next_attempt_at and the payload that was sent. events attempts reports the http_status_code and response body your endpoint returned for each try. events replay queues one more delivery attempt and returns {"object":"webhook_event","id":"<event-id>"}; it does not schedule automatic retries.
resend webhooks events list wh_abc123
resend webhooks events list wh_abc123 --limit 50 --json
resend webhooks events get wh_abc123 msg_1srOrx2ZWZBpBUvZwXKQmoEYga2
resend webhooks events attempts wh_abc123 msg_1srOrx2ZWZBpBUvZwXKQmoEYga2
resend webhooks events replay wh_abc123 msg_1srOrx2ZWZBpBUvZwXKQmoEYga2
Inboxes
With the Resend CLI, you can manage inboxes. An inbox is an email address at one of your verified domains that sends and receives email. Received emails are grouped into threads inside the inbox.
Most inbox commands need a full-access API key. A sending-access key can run threads emails reply, threads emails forward, drafts create, and drafts send. A key limited to one domain must match the domain of the inbox. A sending-access key must pass every ID, because the ID pickers need full access.
Subcommands
listcreategetupdatedeletethreads(list,search,get,update,delete)threads labels(add,set,remove)threads emails(list,get,reply,forward)labels(list,create,update,delete)drafts(list,create,get,update,delete,send)agent(get,update)
Aliases
inboxes ls→listinboxes rm→deletelsandrmalso work onthreads,threads emails(lsonly),labels, anddrafts
Running a group with no subcommand runs its list, or get for agent. threads labels shows its help.
The nested commands take their IDs as positional arguments, parent first: the inbox ID, then the thread, draft, or label ID, then the email ID. For example, resend inboxes threads emails get <inbox-id> <thread-id> <email-id>. Each ID also has a flag, such as --inbox-id or --thread-id. Pass an ID as an argument or as a flag, not both.
Every command that names an inbox takes its ID or its email address, such as support@yourdomain.com. The address match ignores case.
In an interactive terminal without --json or -q, omit an ID to pick from a list. The exception is the email ID of threads emails get, reply, and forward. There, these commands ask you to type it. Get it from threads emails list.
resend inboxes list
Lists the inboxes of the team, newest first. Takes --limit, --after, and --before, like resend webhooks list.
resend inboxes list
resend inboxes list --limit 25 --json
resend inboxes create
Creates an inbox at an address on one of your domains. The part before the @ can contain letters, numbers, and . _ % -, but not +. Without --forwarding, the domain must have receiving enabled. With --forwarding, Resend generates a receiving address, so you can receive mail without an MX record.
The output holds only {"object":"inbox","id":"<uuid>"}. Run resend inboxes get <id> to read the other fields, such as receiving_address.
| Flag | Description |
|---|---|
--email-address <address> | Inbox address — required in non-interactive mode |
--name <name> | Internal name, recipients do not see it |
--from-name <name> | Name recipients see when mail is sent from the inbox |
--forwarding | Receive mail without an MX record through a generated receiving address |
resend inboxes create --email-address support@yourdomain.com
resend inboxes create --email-address hello@yourdomain.com --name "Hello" --forwarding
resend inboxes get
Fetches one inbox by ID or by email address.
resend inboxes get 78261eea-8f8b-4381-83c6-79fa7120f1cf
resend inboxes get support@yourdomain.com --json
resend inboxes update
Updates the internal name or the from name. At least one of --name or --from-name is required. Pass --from-name "" to remove the from name.
resend inboxes update 78261eea-8f8b-4381-83c6-79fa7120f1cf --name "Support"
resend inboxes update 78261eea-8f8b-4381-83c6-79fa7120f1cf --from-name "Ada from Support"
resend inboxes delete
Deletes an inbox. In non-interactive mode, --yes is required to confirm.
resend inboxes delete 78261eea-8f8b-4381-83c6-79fa7120f1cf --yes
resend inboxes threads list
Lists the threads in an inbox, most recently active first.
| Flag | Description |
|---|---|
[inboxId] or --inbox-id <id> | Inbox UUID or email address |
--folders <folders...> | inbox, archive, spam, sent, trash (default inbox, or inbox archive sent with --labels) |
--labels <label-ids...> | Label UUIDs. Returns threads with any of them |
--read / --unread | Only threads where every email is read, or with at least one unread email |
--limit, --after, --before | Pagination, with thread IDs as cursors |
resend inboxes threads list <inbox-id>
resend inboxes threads list <inbox-id> --folders inbox archive --unread --json
resend inboxes threads search
Finds threads by the text of their emails, who sent or received them, attachments, and dates. Takes --query, --from, --to, --cc, --bcc, --has-attachment, --without-attachment, --start-date, and --end-date, plus the filters and pagination of threads list. Each result has a matched_email_id, the ID of the email that matched. Pass it as the email ID of threads emails get, reply, and forward. Search can be up to about a minute behind new emails.
resend inboxes threads search <inbox-id> --query invoice
resend inboxes threads search <inbox-id> --from isabella@example.com --has-attachment --start-date 2026-09-01 --json
resend inboxes threads get
Fetches the summary of one thread. List its emails with resend inboxes threads emails list.
resend inboxes threads get <inbox-id> <thread-id>
resend inboxes threads update
Marks a thread read or unread, or moves it to a folder. At least one option is required. To change the labels of a thread, use threads labels.
| Flag | Description |
|---|---|
--read / --unread | Mark every email in the thread read or unread |
--folder <folder> | inbox, archive, spam, or trash |
resend inboxes threads update <inbox-id> <thread-id> --read --folder archive
resend inboxes threads delete
Moves a thread to trash and schedules it for deletion in 30 days. To restore it, move it to another folder with threads update --folder. In non-interactive mode, --yes is required.
resend inboxes threads delete <inbox-id> <thread-id> --yes
resend inboxes threads labels add
Adds labels to a thread. Labels already on the thread stay. --labels is required and takes 1 to 10 label IDs from labels list. The output lists every label on the thread after the change.
resend inboxes threads labels add <inbox-id> <thread-id> --labels <label-id> <other-label-id>
resend inboxes threads labels set
Replaces every label on a thread in one atomic change. --labels is required and takes 0 to 100 label IDs. Pass --labels "" to remove every label. The labels themselves are kept. The output lists every label on the thread after the change.
resend inboxes threads labels set <inbox-id> <thread-id> --labels <label-id>
resend inboxes threads labels set <inbox-id> <thread-id> --labels ""
resend inboxes threads labels remove
Removes one label from a thread. The label itself is kept. The output lists every label on the thread after the change.
resend inboxes threads labels remove <inbox-id> <thread-id> <label-id>
resend inboxes threads emails list
Lists the emails of a thread, oldest first, with their bodies. Takes --limit, --after, and --before, with email IDs as cursors.
resend inboxes threads emails list <inbox-id> <thread-id>
resend inboxes threads emails get
Fetches one email of a thread.
resend inboxes threads emails get <inbox-id> <thread-id> <email-id>
resend inboxes threads emails reply
Sends a reply from the inbox address. The reply goes to the Reply-To addresses of the email, or to its sender. If the inbox sent the email, the reply goes to its to recipients. --reply-all also copies the other recipients, without the inbox address and without duplicates. With --reply-all, a reply to an email the inbox sent goes to its Reply-To addresses when they are not the inbox address. At least one of --text or --html is required. The output holds the ID of the sent email.
| Flag | Description |
|---|---|
--text <text> | Plain-text body |
--html <html> | HTML body |
--cc, --bcc <addresses...> | Extra recipients, one or more |
--subject <subject> | Override the subject |
--reply-all | Reply to everyone on the original email |
--attachment <specs...> | File path or URL to attach, as on resend emails send. The content of all files can total at most 25 MB |
--attachments-file <path> | JSON array of attachment objects |
--headers <key=value...> | Custom headers. In-Reply-To, References and Reply-To are reserved. Names and values cannot contain line breaks |
--idempotency-key <key> | Deduplicate the send |
resend inboxes threads emails reply <inbox-id> <thread-id> <email-id> --text "Thanks!"
resend inboxes threads emails reply <inbox-id> <thread-id> <email-id> --text "Invoice attached." --attachment ./invoice.pdf
resend inboxes threads emails forward
Forwards an email from the inbox address. --to is required. The attachments of the original email are sent with the forward. Together with the files you add, they can have at most 25 MB of content. An original attachment that was sent from a URL cannot be forwarded. Takes --cc, --bcc, --text, --html, --subject, --attachment, --attachments-file, --headers, and --idempotency-key. The output holds the ID of the sent email.
resend inboxes threads emails forward <inbox-id> <thread-id> <email-id> --to teammate@example.com --text "FYI"
resend inboxes labels list
Lists every label of an inbox, oldest first. The list is not paginated.
resend inboxes labels list <inbox-id>
resend inboxes labels create
Creates a label. --name is required. --color takes a #RRGGBB hex color, random when omitted. Quote it so the shell keeps the #.
resend inboxes labels create <inbox-id> --name "Urgent" --color '#E93D82'
resend inboxes labels update
Renames or recolors a label. At least one of --name or --color is required.
resend inboxes labels update <inbox-id> <label-id> --name "Billing"
resend inboxes labels delete
Deletes a label and removes it from every thread. In non-interactive mode, --yes is required.
resend inboxes labels delete <inbox-id> <label-id> --yes
resend inboxes drafts list
Lists the unsent drafts of an inbox, most recently updated first. Takes --limit, --after, and --before.
resend inboxes drafts list <inbox-id>
resend inboxes drafts create
Creates a draft that starts a new thread. Pass --thread-id and --reply-to-email-id together for a reply draft. A reply draft without --subject is sent with the subject of the thread, prefixed with Re: when it does not already start with it. Takes --to, --cc, --bcc, --subject, --text, --html, --attachment, and --attachments-file. At least one of them is required. --idempotency-key deduplicates the request.
--attachment takes a local file with ;type=<MIME type>, or an http:// or https:// URL. A URL's filename defaults to the decoded last segment of its path and its MIME type to the type of the downloaded file. The URLs are downloaded when the draft is saved, so a URL that cannot be downloaded returns an error. The files on a draft can total at most 25 MB, downloads included.
resend inboxes drafts create <inbox-id> --to user@example.com --subject "Hello" --text "Draft body"
resend inboxes drafts create <inbox-id> --thread-id <thread-id> --reply-to-email-id <email-id> --text "Reply draft"
resend inboxes drafts create <inbox-id> --to user@example.com --text "Report attached" --attachment "./report.pdf;type=application/pdf"
resend inboxes drafts create <inbox-id> --to user@example.com --text "Invoice attached" --attachment "https://example.com/invoice.pdf"
resend inboxes drafts get
Fetches one draft with its full body.
resend inboxes drafts get <inbox-id> <draft-id>
resend inboxes drafts update
Updates the recipients, subject, body, or attachments of an unsent draft. Setting --text or --html replaces the whole body. --attachment and --attachments-file replace every attachment on the draft, and --clear-attachments removes them all. --attachment and --attachments-file take the same local files and URLs as drafts create. --clear-to, --clear-cc, --clear-bcc, --clear-subject, --clear-text, and --clear-html remove the value of that field. A clear flag and a value for the same field return an error. The output holds only {"object":"inbox_draft","id":"<uuid>"}. Run drafts get to read the draft.
resend inboxes drafts update <inbox-id> <draft-id> --subject "Updated subject"
resend inboxes drafts update <inbox-id> <draft-id> --clear-attachments
resend inboxes drafts update <inbox-id> <draft-id> --to ada@example.com grace@example.com --clear-cc
resend inboxes drafts delete
Discards an unsent draft. In non-interactive mode, --yes is required.
resend inboxes drafts delete <inbox-id> <draft-id> --yes
resend inboxes drafts send
Sends a draft from the inbox address. The draft needs at least one to recipient and html or text. A standalone draft also needs a non-empty subject. A reply draft without a subject is sent with the subject of the thread, prefixed with Re: when it does not already start with it. --headers adds custom headers, except the reserved In-Reply-To, References and Reply-To. --idempotency-key deduplicates the send. The output holds the ID of the sent email.
resend inboxes drafts send <inbox-id> <draft-id>
resend inboxes agent get
Fetches the agent settings of an inbox: instructions, tone, and actions.
resend inboxes agent get <inbox-id>
resend inboxes agent update
Updates the agent settings. At least one of --instructions, --tone, or --actions is required. Omitted fields keep their current value. --actions replaces the whole set. It takes draft_reply, forward_thread, add_labels, assign_thread, mark_as_spam, archive, and move_to_trash.
--tone takes one of the presets concise_professional, concise_casual, expanded_professional, or expanded_casual. Pass --tone "" to clear it. Other text returns invalid_tone.
resend inboxes agent update <inbox-id> --tone concise_professional
resend inboxes agent update <inbox-id> --actions draft_reply add_labels archive
Global options
These flags work on every command and are passed before the subcommand:
resend [global options] <command> [command options]
| Flag | Description |
|---|---|
--api-key <key> | Override API key for this invocation (takes highest priority) |
-p, --profile <name> | Profile to use (overrides RESEND_PROFILE env var) |
--json | Force JSON output even in interactive terminals |
-q, --quiet | Suppress spinners and status output (implies --json) |
--version | Print version and exit |
--help | Show help text |
Output behavior
The CLI has two output modes:
| Mode | When | Stdout | Stderr |
|---|---|---|---|
| Interactive | Terminal (TTY) | Formatted text | Spinners, prompts, human-readable errors |
| Machine | Piped, CI, or --json | Success JSON only | JSON errors; optional warnings (e.g. flags) |
Switching is automatic — pipe to another command and JSON output activates:
resend doctor | jq '.checks[].name'
resend emails send --from ... --to ... --subject ... --text ... | jq '.id'
Error output
Errors always exit with code 1. The format on stderr depends on output mode (same rules as the table above):
- Machine (piped stdout, CI,
--json, or-q): structured JSON so stdout stays success-only for scripting (jq, etc.):
{ "error": { "message": "No API key found", "code": "auth_error" } }
- Interactive (TTY without
--json/-q): a human-readable line such asError: No API key found(still on stderr).
Agent & CI/CD usage
CI/CD
Set RESEND_API_KEY as an environment variable — no resend login needed:
# GitHub Actions
env:
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
steps:
- run: |
resend emails send \
--from "deploy@example.com" \
--to "delivered@resend.dev" \
--subject "Deploy complete" \
--text "Version ${{ github.sha }} deployed."
AI agents
Agents calling the CLI as a subprocess automatically get JSON output (non-TTY detection). The contract:
- Input: All required flags must be provided (no interactive prompts)
- Output: Success JSON on stdout; error JSON on stderr (use
2>or combined capture if you need both) - Exit code:
0success,1error - Errors: Always include
messageandcodefields - Discovery:
resend commandsprints the full command tree as JSON (subcommands, options, descriptions).
resend commands
Prints the CLI command tree as JSON for scripting and AI agents. In an interactive terminal, pass global --json if you need machine output; when stdout is piped, JSON is used automatically.
Dry-run (no API call)
--dry-run validates a payload and prints it without calling the API. These commands support it:
resend emails send ... --dry-run— validates inputs and prints{ "dryRun": true, "request": { ... } }without sending. Attachments appear asfilenameandbyteLengthonly.resend broadcasts create ... --dry-run— same for the broadcast create payload.resend emails batch --file ./emails.json --dry-run— parses and normalises the JSON array, resolves any--react-emailtemplate, then prints the request payload without sending. No API key required.
Other commands do not support --dry-run.
Configuration
| Item | Path | Notes |
|---|---|---|
| Config directory | ~/.config/resend/ | Respects $XDG_CONFIG_HOME on Linux, %APPDATA% on Windows |
| Credentials | ~/.config/resend/credentials.json | 0600 permissions (owner read/write) |
| Install directory | ~/.resend/bin/ | Respects $RESEND_INSTALL |
License
MIT
相關資源

Resend MCP Server
MCPResend 官方 MCP,让 agent 收发邮件,管理联系人、群发(Broadcasts)与发信域名。提供免安装的远程服务(OAuth 登录),也可用 npm 包本地运行,本地运行需要 Resend API Key。
resend/resend-mcp580
Himalaya
CLI命令行邮件客户端,支持 IMAP、SMTP、JMAP、Gmail 与 Microsoft Graph,多账户、可用 --json 输出,适合让 agent 查收、搜索、撰写与发送邮件;首次运行时向导会自动探测邮箱配置。
pimalaya/himalaya7.6k
mcp-email-server
MCP通过 IMAP/SMTP 让 agent 读取、搜索、整理和发送邮件,任何支持这两种协议的邮箱都能接入,支持多个账号,自带本地网页配置界面。需要邮箱账号的密码或授权码。
Wh1isper/mcp-email-server376
gws(Google Workspace CLI)
CLI用一个命令行调用 Gmail、Drive、日历、Docs、Sheets 等 Google Workspace API,命令按 Discovery 动态生成、输出 JSON,附 100 多个 Agent Skill。googleworkspace 组织开源但声明非官方支持产品;需 Google Cloud 项目 OAuth 授权。
googleworkspace/cli32.1k
gog (gogcli)
CLI面向脚本与 agent 的 Google Workspace 命令行,覆盖 Gmail、日历、Drive、Docs、Sheets、联系人与任务,支持 JSON 输出、只读模式与命令白名单,可作只读默认的 MCP 服务;需自备 Google OAuth 客户端。
openclaw/gogcli8.6k
Firecrawl MCP Server
MCP把网页抓取成 Markdown 或结构化数据,支持整站爬取、站点地图、网页搜索与动态页面交互。Firecrawl 官方维护,云端需要 API Key(也可自托管);托管端免密钥时仅开放抓取、搜索与解析。
firecrawl/firecrawl-mcp-server7.8k