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@yourdomain.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@yourdomain.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@yourdomain.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@yourdomain.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@yourdomain.com" \
--to delivered@resend.dev bounced@resend.dev \
--subject "Team update" \
--text "Hello everyone"HTML from a file:
resend emails send \
--from "you@yourdomain.com" \
--to delivered@resend.dev \
--subject "Newsletter" \
--html-file ./newsletter.htmlWith CC, BCC, and reply-to:
resend emails send \
--from "you@yourdomain.com" \
--to delivered@resend.dev \
--subject "Meeting notes" \
--text "See attached." \
--cc manager@example.com \
--bcc delivered+1@resend.dev \
--reply-to noreply@example.comOverriding the API key for one send:
resend --api-key re_other_key emails send \
--from "you@yourdomain.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 |
| Domain | domain.created, domain.updated, domain.deleted |
Use all with --events to subscribe to every event.
listcreategetupdatedeletelisten
webhooks ls→listwebhooks rm→delete
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. The signing_secret in the response is shown once. Store it immediately to verify incoming payloads.
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 signing secret is not returned from get. To rotate secrets, delete the webhook and create a new one.
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.
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
- 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.bouncedThese 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@yourdomain.com" \
--to "delivered@resend.com" \
--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 is only implemented where agents most often need to validate a complex payload before a high-impact send:
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.
Other write commands (batch, broadcasts send, webhooks, contacts, etc.) do not support --dry-run yet. If that would help your workflow, open an issue — likely next candidates are emails batch (large JSON files) and broadcasts send (confirm id + schedule before delivery).
| 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