The official CLI for Resend.
Built for humans, AI agents, and CI/CD pipelines.
██████╗ ███████╗███████╗███████╗███╗ ██╗██████╗
██╔══██╗██╔════╝██╔════╝██╔════╝████╗ ██║██╔══██╗
██████╔╝█████╗ ███████╗█████╗ ██╔██╗ ██║██║ ██║
██╔══██╗██╔══╝ ╚════██║██╔══╝ ██║╚██╗██║██║ ██║
██║ ██║███████╗███████║███████╗██║ ╚████║██████╔╝
╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝
curl -fsSL https://resend.com/install.sh | bashnpm install -g resend-clibrew install resend/cli/resendirm https://resend.com/install.ps1 | iexOr download the .exe directly from the GitHub releases page.
# 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 doctorThis 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-skillsUse this when you want to change the CLI and run your build locally.
- Node.js 20+
-
Clone the repo
git clone https://github.com/resend/resend-cli.git cd resend-cli -
Install dependencies
pnpm install
-
Build locally
pnpm build
Output:
./dist/cli.cjs
Use the dev script:
pnpm dev --versionOr run the built JS bundle:
node dist/cli.cjs --versionAfter editing source files, rebuild:
pnpm buildTo build a standalone native binary:
pnpm build:binOutput: ./dist/resend
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.
Authenticate by storing your API key locally. The key is validated against the Resend API before being saved.
resend loginWhen 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_.
When stdin is not a TTY, the --key flag is required:
resend login --key re_xxxxxxxxxxxxxOmitting --key in non-interactive mode exits with error code missing_key.
| Flag | Description |
|---|---|
--key <key> |
API key to store (required in non-interactive mode) |
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.
# JSON output
resend login --key re_xxx --json
# => {"success":true,"config_path":"/Users/you/.config/resend/credentials.json"}| Code | Cause |
|---|---|
missing_key |
No --key provided in non-interactive mode |
invalid_key_format |
Key does not start with re_ |
validation_failed |
Resend API rejected the key |
If you work across multiple Resend teams or accounts, the CLI handles that, too.
Switch between profiles without logging in and out:
resend auth switchYou can also use the global --profile (or -p) flag on any command to run it with a specific profile.
resend domains list --profile productionSend 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"| 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 |
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"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, --subjectA body (--text, --html, or --html-file) is also required — omitting all three exits with code missing_body.
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.htmlWith 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.comOverriding 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"Returns the email ID on success:
{ "id": "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794" }| 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 |
Run environment diagnostics. Verifies your CLI version, API key, domains, and detects AI agent integrations.
resend doctor| 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).
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
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.
| 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 |
Exits 0 when all checks pass or warn. Exits 1 if any check fails.
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.
| 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.
listcreategetupdatedeleterotate-signing-secretlistenevents(list,get,attempts,replay)
webhooks ls→listwebhooks rm→deletewebhooks events ls→webhooks events 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 --jsonRegisters 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.deliveredFetches 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 --jsonThe response includes the signing secret. Use resend webhooks rotate-signing-secret <id> to replace it.
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.bouncedDeletes a webhook and stops deliveries.
In non-interactive mode, --yes is required to confirm.
resend webhooks delete wh_abc123 --yesTo pause delivery temporarily, prefer resend webhooks update <id> --status disabled.
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 --jsonBuilt-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
- Deletes the webhook on exit
Your tunnel must forward to the same port as --port, e.g. ngrok http 4318.
| 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.bouncedInspects 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_1srOrx2ZWZBpBUvZwXKQmoEYga2With 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.
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)
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.
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 --jsonCreates 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" --forwardingFetches one inbox by ID or by email address.
resend inboxes get 78261eea-8f8b-4381-83c6-79fa7120f1cf
resend inboxes get support@yourdomain.com --jsonUpdates 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"Deletes an inbox. In non-interactive mode, --yes is required to confirm.
resend inboxes delete 78261eea-8f8b-4381-83c6-79fa7120f1cf --yesLists 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 --jsonFinds 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 --jsonFetches the summary of one thread. List its emails with resend inboxes threads emails list.
resend inboxes threads get <inbox-id> <thread-id>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 archiveMoves 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> --yesAdds 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>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 ""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>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>Fetches one email of a thread.
resend inboxes threads emails get <inbox-id> <thread-id> <email-id>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.pdfForwards 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"Lists every label of an inbox, oldest first. The list is not paginated.
resend inboxes labels list <inbox-id>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'Renames or recolors a label. At least one of --name or --color is required.
resend inboxes labels update <inbox-id> <label-id> --name "Billing"Deletes a label and removes it from every thread. In non-interactive mode, --yes is required.
resend inboxes labels delete <inbox-id> <label-id> --yesLists the unsent drafts of an inbox, most recently updated first. Takes --limit, --after, and --before.
resend inboxes drafts list <inbox-id>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.
Draft attachments need content, filename, and content_type, so --attachment takes a local file with ;type=<MIME type>. The files on a draft can total at most 25 MB.
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"Fetches one draft with its full body.
resend inboxes drafts get <inbox-id> <draft-id>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. --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-ccDiscards an unsent draft. In non-interactive mode, --yes is required.
resend inboxes drafts delete <inbox-id> <draft-id> --yesSends 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>Fetches the agent settings of an inbox: instructions, tone, and actions.
resend inboxes agent get <inbox-id>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 archiveThese 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 |
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'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).
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."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).
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 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.
| 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 |
MIT