
Resend Cli
- 48 installs
- 3 repo stars
- Updated June 29, 2026
- tristanmanchester/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
resend-cli is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- resend-cli
- AI & Agent Building
- AI-coding skill
Resend Cli by the numbers
- 48 all-time installs (skills.sh)
- Ranked #7,473 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tristanmanchester/agent-skills --skill resend-cliAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 48 |
|---|---|
| repo stars | ★ 3 |
| Last updated | June 29, 2026 |
| Repository | tristanmanchester/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Resend CLI
This skill is for agents that should operate Resend through the official CLI first, not by dropping straight to raw REST.
The goal is not just “know the commands”. The goal is to make an agent:
1. choose the right Resend primitive, 2. choose the right CLI command, 3. run it in a deterministic non-interactive way, 4. detect the important CLI coverage gaps before it gets stuck, and 5. fall back to MCP/API only when the CLI genuinely does not cover the job.
Start here
Load only the files that match the task:
references/agent-operating-model.md— the default decision process for live Resend workreferences/install-auth-and-profiles.md— install methods, auth priority, profiles, config pathsreferences/subprocess-contract.md— how agents should invokeresendsafely and parse outputreferences/command-selection.md— fast routing from user intent to the right command(s)references/sending-scheduling-and-batch.md— transactional sends, schedules, tags, attachments, batch limitsreferences/domains-dns-and-deliverability.md— domain creation, verification, receiving, TLS, tracking, 403/domain mismatchreferences/webhooks-and-listeners.md— webhook creation, update, signature handling, temporary local listenersreferences/inbound-receiving-and-threading.md— inbound list/get/attachments/forward/listen flowsreferences/contacts-topics-segments-and-broadcasts.md— subscription modelling, targeting, campaignsreferences/templates-and-coverage-gaps.md— template lifecycle and the important current CLI gapsreferences/diagnostics-and-fallbacks.md— debug order, CLI quirks, when to fall back to MCP/APIreferences/recipes.md— short end-to-end playbooksreferences/sources.md— first-party source manifest and refresh notes
Machine-readable assets:
assets/command-catalog.json— command index with detail levels (source_inspected,readme_confirmed,tree_confirmed)assets/task-router.json— route common tasks to command sequencesassets/error-map.json— fast-fail diagnosis hintsassets/coverage-gaps.json— current CLI limitations and ambiguities that matter to agentsassets/subprocess-contract.json— deterministic invocation defaultsassets/scaffold-index.json— reusable command/file scaffoldsassets/source-manifest.json— authoritative URLs used to build this skill
Bundled helper:
scripts/resend_cli.py— agent wrapper for probing, routing, scaffolding, batch linting, diagnosis, and safe subprocess execution
Core operating rules
1) Prefer the official CLI for live Resend work
Default order of preference:
1. Official Resend CLI for live terminal/CI/agent operations 2. Official Resend MCP server if the environment already exposes it and the CLI is unavailable 3. Official SDK when editing app code inside an existing integration 4. Raw REST only for stack-neutral examples, protocol debugging, or feature gaps
Do not choose raw REST just because it is familiar.
2) For agents, stay non-interactive by default
For bounded commands:
- pass all required flags explicitly
- use global
--json -q - prefer
RESEND_API_KEYor a stored profile over typing secrets interactively - set
RESEND_NO_UPDATE_NOTIFIER=1for deterministic output - capture both stdout and stderr defensively
3) Run doctor early when the environment is unknown
When you do not know whether the CLI is installed, authenticated, or pointed at the right account:
resend --json -q doctorThis is usually the fastest first read on:
- CLI availability/version
- whether an API key is being resolved
- whether verified domains exist
- whether the machine looks like an AI-agent environment
4) Choose the primitive before the command
- One logical transactional email →
emails send - Up to 100 distinct transactional emails in one request →
emails batch - Scheduled transactional email mutation →
emails update/emails cancel - Campaign to a segment →
broadcasts create/broadcasts send - Reusable hosted content →
templates * - Sender or receiving setup →
domains * - Inbound processing →
emails receiving *+webhooks * - Scoped credentials →
api-keys * - Recipient data and preferences →
contacts,contact-properties,topics,segments - Local dev event loop →
webhooks listenoremails receiving listen
5) Know the current CLI gaps
This version of the skill treats these as especially important:
- Template send gap: the CLI manages templates, but the current
emails sendcommand surface does not expose a direct--template-id/template-vars flow. - Domain capability update gap:
domains updateexposes TLS/open/click tracking, but not an explicit sending/receiving capability toggle, while inbound help text references such a toggle. - Stream commands are special:
webhooks listenandemails receiving listenare long-running and should be treated as NDJSON/event streams in agent mode. - JSON error channel discrepancy: the README promises machine JSON on stdout only, but the current source writes JSON errors with
console.error, so wrappers must parse stderr too.
6) Keep IDs and file paths
Most multi-step flows become much easier if the agent persists:
- domain IDs
- email IDs
- webhook IDs
- topic IDs
- segment IDs
- template IDs/aliases
- API key IDs
- the file paths it generated for HTML or batch JSON
The mutation ladder
For state-changing live operations:
1. classify the task 2. confirm the command sequence 3. make any needed file assets (.html, batch JSON) 4. run with --json -q 5. verify with get, list, or a follow-up check 6. persist returned IDs and next-step context 7. only then continue to the next mutation
Bundled helper script
scripts/resend_cli.py is intentionally agent-oriented.
Commands:
probe— find the CLI, report install hints, and show environment basicscatalog— list known commands from the bundled catalogueinfo— inspect one command and its notes/gapsrecommend— route a free-text task to the best CLI sequencescaffold— print or materialise sample commands/fileslint-batch— statically validate anemails batchJSON filedoctor— explain likely causes of common CLI/API failuresrun— execute the official CLI with deterministic defaults and tolerant JSON parsing
Examples:
python3 scripts/resend_cli.py probe
python3 scripts/resend_cli.py catalog --resource emails
python3 scripts/resend_cli.py info "emails send"
python3 scripts/resend_cli.py recommend "send 70 different shipment notifications"
python3 scripts/resend_cli.py scaffold batch-send --write-dir ./tmp
python3 scripts/resend_cli.py lint-batch ./tmp/batch-emails.json
python3 scripts/resend_cli.py doctor --command "emails send" --status 403 --message "1010 forbidden"
python3 scripts/resend_cli.py run -- emails list --limit 5Response shape this skill should produce
A strong answer usually includes:
1. the exact Resend primitive, 2. the exact CLI command or command sequence, 3. any file scaffolding the user needs, 4. the operational caveats that matter here, 5. the verification step, 6. the fallback path if the CLI does not currently cover the flow.
Example prompts this skill should handle
- “Use the Resend CLI to send a scheduled password reset email”
- “Should I use
emails send,emails batch, orbroadcasts create?” - “Create a sending + receiving domain in
eu-west-1” - “Set up local webhook listening with ngrok”
- “Why is my batch file failing?”
- “How do I manage multiple Resend accounts from one agent?”
- “Can the CLI send a hosted template directly?”
- “How should my agent parse
resendoutput safely?”
[
{
"from": "Acme <alerts@example.com>",
"to": [
"alice@example.com"
],
"subject": "Shipment 1001",
"text": "Your order 1001 has shipped."
},
{
"from": "Acme <alerts@example.com>",
"to": [
"bob@example.com"
],
"subject": "Shipment 1002",
"html": "<p>Your order <strong>1002</strong> has shipped.</p>"
}
]
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Weekly Update</title>
</head>
<body>
<h1>Hello {{{FIRST_NAME|there}}}</h1>
<p>Here is your weekly update from Acme.</p>
</body>
</html>
{
"meta": {
"skill": "resend-cli",
"version": "3.0.0",
"cli_version": "1.4.1",
"last_reviewed": "2026-03-14",
"detail_levels": {
"source_inspected": "The exact command implementation or raw source file was inspected.",
"readme_confirmed": "The behaviour is directly documented in the official README/changelog.",
"tree_confirmed": "The command exists in the public CLI tree, but this skill did not inspect every option in source."
}
},
"resource_index": {
"api-keys": [
"api-keys create",
"api-keys delete",
"api-keys list"
],
"auth": [
"auth list",
"auth remove",
"auth rename",
"auth switch"
],
"broadcasts": [
"broadcasts create",
"broadcasts delete",
"broadcasts get",
"broadcasts list",
"broadcasts send",
"broadcasts update"
],
"contact-properties": [
"contact-properties create",
"contact-properties delete",
"contact-properties get",
"contact-properties list",
"contact-properties update"
],
"contacts": [
"contacts add-segment",
"contacts create",
"contacts delete",
"contacts get",
"contacts list",
"contacts remove-segment",
"contacts update",
"contacts update-topics"
],
"domains": [
"domains create",
"domains delete",
"domains get",
"domains list",
"domains update",
"domains verify"
],
"emails": [
"emails batch",
"emails cancel",
"emails get",
"emails list",
"emails send",
"emails update"
],
"emails receiving": [
"emails receiving attachment",
"emails receiving attachments",
"emails receiving forward",
"emails receiving get",
"emails receiving list",
"emails receiving listen"
],
"root": [
"doctor",
"login",
"logout",
"whoami"
],
"segments": [
"segments create",
"segments delete",
"segments get",
"segments list"
],
"templates": [
"templates create",
"templates delete",
"templates duplicate",
"templates get",
"templates list",
"templates publish",
"templates update"
],
"topics": [
"topics create",
"topics delete",
"topics get",
"topics list",
"topics update"
],
"webhooks": [
"webhooks create",
"webhooks delete",
"webhooks get",
"webhooks list",
"webhooks listen",
"webhooks update"
]
},
"commands": [
{
"name": "doctor",
"resource": "root",
"category": "diagnostics",
"detail_level": "readme_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [
"--json",
"-q"
],
"returns": {
"kind": "object",
"shape_hint": {
"ok": true,
"checks": [
{
"name": "CLI Version",
"status": "pass"
}
]
}
},
"notes": [
"Best first command when auth or environment is unclear.",
"Checks CLI version, API key resolution, domains, and AI-agent detection."
]
},
{
"name": "login",
"resource": "root",
"category": "auth",
"detail_level": "readme_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [
"--key",
"--json",
"-q"
],
"returns": {
"kind": "object",
"shape_hint": {
"success": true,
"config_path": "~/.config/resend/credentials.json"
}
},
"notes": [
"Use mostly for human bootstrap.",
"In non-interactive mode, --key is required."
]
},
{
"name": "auth switch",
"resource": "auth",
"category": "auth",
"detail_level": "readme_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "text_or_json"
},
"notes": [
"Prefer explicit --profile on live mutations for agents."
]
},
{
"name": "auth list",
"resource": "auth",
"category": "auth",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "list"
},
"notes": [
"Lists stored profiles."
]
},
{
"name": "auth rename",
"resource": "auth",
"category": "auth",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "object_or_text"
},
"notes": [
"Use carefully in shared automation setups."
]
},
{
"name": "auth remove",
"resource": "auth",
"category": "auth",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "object_or_text"
},
"notes": [
"Removes one stored profile."
]
},
{
"name": "logout",
"resource": "root",
"category": "auth",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "object_or_text"
},
"notes": [
"Clears stored credentials."
]
},
{
"name": "whoami",
"resource": "root",
"category": "auth",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "object_or_text"
},
"notes": [
"Useful for fast account identity checks if installed CLI exposes it as expected."
]
},
{
"name": "emails send",
"resource": "emails",
"category": "transactional",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"--from",
"--to",
"--subject",
"one of --text|--html|--html-file"
],
"high_value_optional": [
"--cc",
"--bcc",
"--reply-to",
"--scheduled-at",
"--attachment",
"--headers",
"--tags",
"--idempotency-key"
],
"returns": {
"kind": "object",
"shape_hint": {
"id": "email-id"
}
},
"notes": [
"For agents, pass all required flags explicitly.",
"Current flag surface does not show a direct hosted-template send path."
]
},
{
"name": "emails batch",
"resource": "emails",
"category": "transactional",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"--file"
],
"high_value_optional": [
"--idempotency-key",
"--batch-validation strict|permissive"
],
"returns": {
"kind": "list_or_partial",
"shape_hint": [
{
"id": "email-id"
}
]
},
"notes": [
"Hard limit 100 emails per request.",
"Unsupported per-email fields: attachments, scheduled_at."
]
},
{
"name": "emails list",
"resource": "emails",
"category": "transactional",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [
"--limit",
"--after",
"--before"
],
"returns": {
"kind": "list",
"shape_hint": {
"object": "list",
"has_more": false,
"data": []
}
},
"notes": [
"List sent email summaries. Use emails get for full details."
]
},
{
"name": "emails get",
"resource": "emails",
"category": "transactional",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<email-id>"
],
"high_value_optional": [],
"returns": {
"kind": "object",
"shape_hint": {
"object": "email",
"id": "email-id"
}
},
"notes": [
"Good verification step after send/update."
]
},
{
"name": "emails update",
"resource": "emails",
"category": "transactional",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<email-id>",
"--scheduled-at"
],
"high_value_optional": [],
"returns": {
"kind": "object",
"shape_hint": {
"object": "email",
"id": "email-id"
}
},
"notes": [
"Updates scheduled transactional email timing."
]
},
{
"name": "emails cancel",
"resource": "emails",
"category": "transactional",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<email-id>"
],
"high_value_optional": [],
"returns": {
"kind": "object",
"shape_hint": {
"object": "email",
"id": "email-id"
}
},
"notes": [
"Cancels a scheduled email."
]
},
{
"name": "emails receiving list",
"resource": "emails receiving",
"category": "inbound",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [
"--limit",
"--after",
"--before"
],
"returns": {
"kind": "list"
},
"notes": [
"Lists received emails. Requires receiving to be enabled on the domain."
]
},
{
"name": "emails receiving get",
"resource": "emails receiving",
"category": "inbound",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<received-email-id>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Fetches full inbound email data, including raw download URL and body fields."
]
},
{
"name": "emails receiving attachments",
"resource": "emails receiving",
"category": "inbound",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<received-email-id>"
],
"high_value_optional": [],
"returns": {
"kind": "list"
},
"notes": [
"Lists inbound attachment metadata and download URLs."
]
},
{
"name": "emails receiving attachment",
"resource": "emails receiving",
"category": "inbound",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<received-email-id>",
"<attachment-id>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Fetches one inbound attachment record."
]
},
{
"name": "emails receiving forward",
"resource": "emails receiving",
"category": "inbound",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<received-email-id>",
"--to",
"--from"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Forwards an inbound email; this is different from replying in-thread."
]
},
{
"name": "emails receiving listen",
"resource": "emails receiving",
"category": "inbound",
"detail_level": "source_inspected",
"bounded": false,
"streaming": true,
"destructive": false,
"required": [],
"high_value_optional": [
"--interval",
"--json"
],
"returns": {
"kind": "ndjson_stream"
},
"notes": [
"Long-running poller.",
"JSON mode emits one JSON object per line.",
"Exits after 5 consecutive API failures."
]
},
{
"name": "domains create",
"resource": "domains",
"category": "domains",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"--name"
],
"high_value_optional": [
"--region",
"--tls",
"--sending",
"--receiving"
],
"returns": {
"kind": "object",
"shape_hint": {
"object": "domain",
"records": []
}
},
"notes": [
"Returns DNS records to configure externally."
]
},
{
"name": "domains list",
"resource": "domains",
"category": "domains",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [
"--limit",
"--after",
"--before"
],
"returns": {
"kind": "list"
},
"notes": [
"Useful for checking status across multiple domains."
]
},
{
"name": "domains get",
"resource": "domains",
"category": "domains",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<domain-id>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Use after verification attempts to poll current status."
]
},
{
"name": "domains update",
"resource": "domains",
"category": "domains",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<domain-id>",
"one of --tls|--open-tracking|--no-open-tracking|--click-tracking|--no-click-tracking"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Updates TLS and tracking settings.",
"Does not currently expose a sending/receiving capability toggle in the inspected source."
]
},
{
"name": "domains verify",
"resource": "domains",
"category": "domains",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<domain-id>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Verification is async; follow with domains get."
]
},
{
"name": "domains delete",
"resource": "domains",
"category": "domains",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<domain-id>"
],
"high_value_optional": [
"--yes"
],
"returns": {
"kind": "object_or_text"
},
"notes": [
"Non-interactive usage should include --yes."
]
},
{
"name": "webhooks create",
"resource": "webhooks",
"category": "webhooks",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"--endpoint",
"--events"
],
"high_value_optional": [],
"returns": {
"kind": "object",
"shape_hint": {
"object": "webhook",
"id": "webhook-id",
"signing_secret": "..."
}
},
"notes": [
"Save the signing secret immediately; it is shown once."
]
},
{
"name": "webhooks list",
"resource": "webhooks",
"category": "webhooks",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [
"--limit",
"--after",
"--before"
],
"returns": {
"kind": "list"
},
"notes": [
"Lists registered webhooks."
]
},
{
"name": "webhooks get",
"resource": "webhooks",
"category": "webhooks",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<webhook-id>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Fetches one webhook."
]
},
{
"name": "webhooks update",
"resource": "webhooks",
"category": "webhooks",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<webhook-id>",
"at least one of --endpoint|--events|--status"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"--events replaces the full event list; it is not additive."
]
},
{
"name": "webhooks delete",
"resource": "webhooks",
"category": "webhooks",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<webhook-id>"
],
"high_value_optional": [
"--yes"
],
"returns": {
"kind": "object_or_text"
},
"notes": [
"Deletes a webhook."
]
},
{
"name": "webhooks listen",
"resource": "webhooks",
"category": "webhooks",
"detail_level": "source_inspected",
"bounded": false,
"streaming": true,
"destructive": false,
"required": [
"--url"
],
"high_value_optional": [
"--forward-to",
"--events",
"--port",
"--json"
],
"returns": {
"kind": "ndjson_stream"
},
"notes": [
"Starts local server and creates a temporary webhook.",
"Deletes the temporary webhook on clean exit.",
"JSON mode emits one JSON object per event."
]
},
{
"name": "api-keys create",
"resource": "api-keys",
"category": "auth",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"--name"
],
"high_value_optional": [
"--permission",
"--domain-id"
],
"returns": {
"kind": "object",
"shape_hint": {
"id": "api-key-id",
"token": "re_xxx"
}
},
"notes": [
"Token is returned once only."
]
},
{
"name": "api-keys list",
"resource": "api-keys",
"category": "auth",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "list"
},
"notes": [
"Lists API keys or summaries thereof."
]
},
{
"name": "api-keys delete",
"resource": "api-keys",
"category": "auth",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<api-key-id>"
],
"high_value_optional": [
"--yes"
],
"returns": {
"kind": "object_or_text"
},
"notes": [
"Deletes an API key."
]
},
{
"name": "contacts create",
"resource": "contacts",
"category": "audience",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"--email"
],
"high_value_optional": [
"--first-name",
"--last-name",
"--unsubscribed",
"--properties",
"--segment-id"
],
"returns": {
"kind": "object",
"shape_hint": {
"object": "contact",
"id": "contact-id"
}
},
"notes": [
"Properties are passed as a JSON string.",
"first-name/last-name map to standard properties."
]
},
{
"name": "contacts list",
"resource": "contacts",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [
"--limit",
"--after",
"--before"
],
"returns": {
"kind": "list"
},
"notes": [
"Lists contacts."
]
},
{
"name": "contacts get",
"resource": "contacts",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<contact-id-or-email>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Fetches one contact."
]
},
{
"name": "contacts update",
"resource": "contacts",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<contact-id-or-email>"
],
"high_value_optional": [
"--properties",
"--unsubscribed",
"--segment-id",
"--topic-id"
],
"returns": {
"kind": "object"
},
"notes": [
"Use with care; inspect local help for exact flags."
]
},
{
"name": "contacts delete",
"resource": "contacts",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<contact-id-or-email>"
],
"high_value_optional": [
"--yes"
],
"returns": {
"kind": "object_or_text"
},
"notes": [
"Deletes a contact."
]
},
{
"name": "contacts add-segment",
"resource": "contacts",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Adds a contact to a segment."
]
},
{
"name": "contacts remove-segment",
"resource": "contacts",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Removes a contact from a segment."
]
},
{
"name": "contacts update-topics",
"resource": "contacts",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Updates topic subscriptions for a contact."
]
},
{
"name": "contact-properties create",
"resource": "contact-properties",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Creates a custom contact property."
]
},
{
"name": "contact-properties list",
"resource": "contact-properties",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "list"
},
"notes": [
"Lists contact property definitions."
]
},
{
"name": "contact-properties get",
"resource": "contact-properties",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Fetches one contact property definition."
]
},
{
"name": "contact-properties update",
"resource": "contact-properties",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Updates a contact property definition."
]
},
{
"name": "contact-properties delete",
"resource": "contact-properties",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [],
"high_value_optional": [
"--yes"
],
"returns": {
"kind": "object_or_text"
},
"notes": [
"Deletes a contact property definition."
]
},
{
"name": "topics create",
"resource": "topics",
"category": "audience",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"--name"
],
"high_value_optional": [
"--description",
"--default-subscription opt_in|opt_out"
],
"returns": {
"kind": "object",
"shape_hint": {
"id": "topic-id"
}
},
"notes": [
"Topics control recipient-facing preference categories."
]
},
{
"name": "topics list",
"resource": "topics",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "list"
},
"notes": [
"Lists topics."
]
},
{
"name": "topics get",
"resource": "topics",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<topic-id>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Fetches one topic."
]
},
{
"name": "topics update",
"resource": "topics",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<topic-id>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Updates a topic."
]
},
{
"name": "topics delete",
"resource": "topics",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<topic-id>"
],
"high_value_optional": [
"--yes"
],
"returns": {
"kind": "object_or_text"
},
"notes": [
"Deletes a topic."
]
},
{
"name": "segments create",
"resource": "segments",
"category": "audience",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"--name"
],
"high_value_optional": [],
"returns": {
"kind": "object",
"shape_hint": {
"object": "segment",
"id": "segment-id",
"name": "..."
}
},
"notes": [
"Segments are sender-controlled targeting groups. Prefer them over deprecated audiences."
]
},
{
"name": "segments list",
"resource": "segments",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [],
"returns": {
"kind": "list"
},
"notes": [
"Lists segments."
]
},
{
"name": "segments get",
"resource": "segments",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<segment-id>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Fetches one segment."
]
},
{
"name": "segments delete",
"resource": "segments",
"category": "audience",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<segment-id>"
],
"high_value_optional": [
"--yes"
],
"returns": {
"kind": "object_or_text"
},
"notes": [
"Deletes a segment."
]
},
{
"name": "broadcasts create",
"resource": "broadcasts",
"category": "campaigns",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"--from",
"--subject",
"--segment-id",
"one of --html|--html-file|--text"
],
"high_value_optional": [
"--name",
"--reply-to",
"--preview-text",
"--topic-id",
"--send",
"--scheduled-at"
],
"returns": {
"kind": "object",
"shape_hint": {
"id": "broadcast-id"
}
},
"notes": [
"Creates a draft unless --send is given.",
"Supports natural-language scheduling when paired with --send."
]
},
{
"name": "broadcasts list",
"resource": "broadcasts",
"category": "campaigns",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [
"--limit",
"--after",
"--before"
],
"returns": {
"kind": "list"
},
"notes": [
"Lists broadcast summaries."
]
},
{
"name": "broadcasts get",
"resource": "broadcasts",
"category": "campaigns",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<broadcast-id>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Fetches full broadcast details."
]
},
{
"name": "broadcasts update",
"resource": "broadcasts",
"category": "campaigns",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<broadcast-id>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Updates a draft broadcast."
]
},
{
"name": "broadcasts send",
"resource": "broadcasts",
"category": "campaigns",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<broadcast-id>"
],
"high_value_optional": [
"--scheduled-at"
],
"returns": {
"kind": "object"
},
"notes": [
"Only API/CLI-created drafts can be sent this way.",
"Supports natural-language scheduling."
]
},
{
"name": "broadcasts delete",
"resource": "broadcasts",
"category": "campaigns",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<broadcast-id>"
],
"high_value_optional": [
"--yes"
],
"returns": {
"kind": "object_or_text"
},
"notes": [
"Deletes a broadcast."
]
},
{
"name": "templates create",
"resource": "templates",
"category": "templates",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"--name",
"one of --html|--html-file"
],
"high_value_optional": [
"--subject",
"--text",
"--from",
"--reply-to",
"--alias",
"--var"
],
"returns": {
"kind": "object",
"shape_hint": {
"object": "template",
"id": "template-id"
}
},
"notes": [
"Creates a draft template."
]
},
{
"name": "templates list",
"resource": "templates",
"category": "templates",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [],
"high_value_optional": [
"--limit",
"--after",
"--before"
],
"returns": {
"kind": "list"
},
"notes": [
"Lists templates."
]
},
{
"name": "templates get",
"resource": "templates",
"category": "templates",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<template-id-or-alias>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Fetches one template."
]
},
{
"name": "templates update",
"resource": "templates",
"category": "templates",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<template-id-or-alias>"
],
"high_value_optional": [
"--subject",
"--html",
"--html-file",
"--var"
],
"returns": {
"kind": "object"
},
"notes": [
"Updates a draft or draft portion of a template."
]
},
{
"name": "templates publish",
"resource": "templates",
"category": "templates",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<template-id-or-alias>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Publishes a draft template or re-publishes with latest draft changes."
]
},
{
"name": "templates duplicate",
"resource": "templates",
"category": "templates",
"detail_level": "source_inspected",
"bounded": true,
"streaming": false,
"destructive": false,
"required": [
"<template-id-or-alias>"
],
"high_value_optional": [],
"returns": {
"kind": "object"
},
"notes": [
"Creates a new draft copy."
]
},
{
"name": "templates delete",
"resource": "templates",
"category": "templates",
"detail_level": "tree_confirmed",
"bounded": true,
"streaming": false,
"destructive": true,
"required": [
"<template-id-or-alias>"
],
"high_value_optional": [
"--yes"
],
"returns": {
"kind": "object_or_text"
},
"notes": [
"Deletes a template."
]
}
]
}
{
"meta": {
"skill": "resend-cli",
"version": "3.0.0",
"last_reviewed": "2026-03-14"
},
"gaps": [
{
"id": "template-send-gap",
"title": "Hosted template lifecycle is present, but direct send-by-template is not clearly exposed in emails send",
"severity": "high",
"evidence_level": "source_inspected",
"applies_to": [
"templates *",
"emails send"
],
"what_to_do": [
"Use templates create/update/publish in the CLI as normal",
"For real hosted-template sends, inspect local emails send help",
"If the installed CLI still lacks template flags, fall back to MCP/API or render local HTML and use --html-file"
]
},
{
"id": "domain-capability-toggle-gap",
"title": "Receiving/sending capability toggle is ambiguous after creation",
"severity": "high",
"evidence_level": "source_inspected",
"applies_to": [
"domains create",
"domains update",
"emails receiving"
],
"what_to_do": [
"Prefer setting --receiving at domains create time",
"If an existing domain needs receiving enabled, inspect local help",
"Be ready to fall back to MCP/API if domains update still lacks the toggle"
]
},
{
"id": "stderr-json-discrepancy",
"title": "Machine-mode JSON errors may be written to stderr",
"severity": "high",
"evidence_level": "source_inspected",
"applies_to": [
"all bounded commands"
],
"what_to_do": [
"Parse both stdout and stderr",
"Preserve raw text channels in wrapper output"
]
},
{
"id": "streaming-commands-are-special",
"title": "Listen commands are streams, not request/response calls",
"severity": "medium",
"evidence_level": "source_inspected",
"applies_to": [
"webhooks listen",
"emails receiving listen"
],
"what_to_do": [
"Treat output as NDJSON in JSON mode",
"Use timeouts or supervised process control",
"Do not block forever unless the user explicitly wants a stream"
]
},
{
"id": "transactional-schedule-style",
"title": "Transactional email help documents ISO timestamps; broadcasts explicitly support natural language",
"severity": "medium",
"evidence_level": "source_inspected",
"applies_to": [
"emails send",
"emails update",
"broadcasts create",
"broadcasts send"
],
"what_to_do": [
"Prefer ISO 8601 for agent-generated transactional schedules",
"Natural language is fine for broadcast scheduling when that helps the user"
]
}
]
}
{
"meta": {
"skill": "resend-cli",
"version": "3.0.0"
},
"errors": [
{
"signal": "auth_error",
"applies_to": [
"all"
],
"likely_causes": [
"No API key resolved from flag, env, or config",
"Wrong --profile selected",
"Profile does not exist",
"CLI not authenticated in the intended account"
],
"first_checks": [
"resend --json -q doctor",
"Check RESEND_API_KEY",
"Check --profile / RESEND_PROFILE"
]
},
{
"signal": "missing_body",
"applies_to": [
"emails send",
"templates create",
"broadcasts create"
],
"likely_causes": [
"No --text, --html, or --html-file provided",
"HTML file path is wrong or unreadable"
],
"first_checks": [
"Provide one of the required body flags",
"Prefer --html-file for large content"
]
},
{
"signal": "batch_error",
"applies_to": [
"emails batch"
],
"likely_causes": [
"Input file is not valid JSON",
"Input is not a JSON array",
"More than 100 emails",
"Per-email attachments present",
"Per-email scheduled_at present"
],
"first_checks": [
"python3 scripts/resend_cli.py lint-batch <path>",
"Chunk to <=100 items",
"Remove attachments and scheduled_at"
]
},
{
"signal": "403 / 1010",
"applies_to": [
"emails send",
"emails batch",
"broadcasts create",
"raw REST fallback"
],
"likely_causes": [
"Exact from-domain mismatch",
"Using resend.dev outside testing rules",
"Wrong account / wrong key",
"If using raw REST fallback: missing User-Agent"
],
"first_checks": [
"Check verified domain and exact from address",
"Check account/profile",
"If not using the CLI, add User-Agent"
]
},
{
"signal": "invalid_limit",
"applies_to": [
"list commands"
],
"likely_causes": [
"--limit outside 1-100"
],
"first_checks": [
"Use an integer between 1 and 100"
]
},
{
"signal": "create_error / update_error / send_error",
"applies_to": [
"many"
],
"likely_causes": [
"Underlying Resend API rejected the request",
"Wrong resource IDs",
"Invalid domain or subscription model state"
],
"first_checks": [
"Re-run with the same deterministic flags",
"Inspect parsed JSON error payload",
"Verify IDs and prerequisite resources"
]
},
{
"signal": "poll_error",
"applies_to": [
"emails receiving listen"
],
"likely_causes": [
"Repeated receiving API failures",
"Bad auth or wrong account",
"Receiving not enabled"
],
"first_checks": [
"Run resend --json -q doctor",
"Check receiving-capable domain setup",
"Check account/profile"
]
},
{
"signal": "webhook verification failure",
"applies_to": [
"webhooks"
],
"likely_causes": [
"Parsed JSON used instead of raw body",
"Wrong signing secret",
"Svix headers not preserved",
"Replay or duplicate event handling missing"
],
"first_checks": [
"Verify raw body",
"Use svix-id, svix-timestamp, svix-signature",
"Dedupe on svix-id"
]
}
]
}
name: resend-notification
on:
workflow_dispatch:
jobs:
notify:
runs-on: ubuntu-latest
env:
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
steps:
- name: Install Resend CLI
run: curl -fsSL https://resend.com/install.sh | bash
- name: Add CLI to PATH
run: echo "$HOME/.resend/bin" >> "$GITHUB_PATH"
- name: Sanity-check environment
run: resend --json -q doctor
- name: Send notification email
run: |
resend --json -q emails send --from "deploy@example.com" --to "team@example.com" --subject "Deploy complete" --text "Version ${{ github.sha }} deployed." --idempotency-key "deploy-${{ github.run_id }}"
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Welcome</title>
</head>
<body>
<h1>Welcome to Acme</h1>
<p>Your account is ready.</p>
<p>Thanks for joining us.</p>
</body>
</html>
{
"send-basic": {
"description": "Single transactional send using an HTML file and idempotency.",
"command": "resend --json -q emails send --from \"Acme <alerts@example.com>\" --to user@example.com --subject \"Welcome\" --html-file ./newsletter.html --idempotency-key welcome-001",
"files": [
"newsletter.html"
]
},
"batch-send": {
"description": "Batch send from a JSON file.",
"command": "resend --json -q emails batch --file ./batch-emails.json --idempotency-key batch-001",
"files": [
"batch-emails.json"
]
},
"domain-sending-receiving": {
"description": "Create a sending + receiving domain in EU region.",
"command": "resend --json -q domains create --name eu.example.com --region eu-west-1 --receiving",
"files": []
},
"webhook-dev-listen": {
"description": "Local webhook loop using a tunnel and forward-to.",
"command": "resend --json webhooks listen --url https://example.ngrok-free.app --forward-to localhost:3000/webhooks/resend",
"files": []
},
"receiving-listen": {
"description": "Observe inbound emails as NDJSON.",
"command": "resend --json emails receiving listen --interval 5",
"files": []
},
"broadcast-draft": {
"description": "Create a draft broadcast backed by an HTML file.",
"command": "resend --json -q broadcasts create --from hello@example.com --subject \"Weekly Update\" --segment-id <segment-id> --html-file ./broadcast.html --topic-id <topic-id>",
"files": [
"broadcast.html"
]
},
"template-create": {
"description": "Create a hosted template draft and then publish it.",
"command": "resend --json -q templates create --name \"Welcome\" --html-file ./template.html --subject \"Welcome\" --var NAME:string && resend --json -q templates publish <template-id>",
"files": [
"template.html"
]
},
"api-key-domain-scope": {
"description": "Create a sending-only API key scoped to one domain.",
"command": "resend --json -q api-keys create --name \"ci-token\" --permission sending_access --domain-id <domain-id>",
"files": []
},
"github-actions-email": {
"description": "GitHub Actions job that sends a notification using RESEND_API_KEY.",
"command": "cat ./github-actions-resend.yml",
"files": [
"github-actions-resend.yml"
]
}
}
{
"generated_for": "resend-cli skill v3.0.0",
"last_reviewed": "2026-03-14",
"sources": [
{
"url": "https://resend.com/changelog/cli",
"kind": "official_changelog",
"used_for": [
"CLI announcement",
"install methods",
"53 commands across 13 resources",
"agent-friendly positioning",
"broadcast natural-language scheduling"
]
},
{
"url": "https://github.com/resend/resend-cli",
"kind": "official_repo_readme",
"used_for": [
"auth priority",
"config paths",
"permissions",
"doctor",
"global flags",
"machine-mode contract",
"Node 20+ local-dev requirement"
]
},
{
"url": "https://github.com/resend/resend-cli/releases",
"kind": "official_release_page",
"used_for": [
"current release version"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/lib/output.ts",
"kind": "source_file",
"used_for": [
"stdout/stderr JSON behaviour"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/lib/client.ts",
"kind": "source_file",
"used_for": [
"CLI sets RESEND_USER_AGENT",
"auth error path"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/lib/config.ts",
"kind": "source_file",
"used_for": [
"profile resolution",
"credentials schema",
"config path details"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/lib/pagination.ts",
"kind": "source_file",
"used_for": [
"limit validation",
"pagination hints"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/emails/send.ts",
"kind": "source_file",
"used_for": [
"emails send flags",
"idempotency flag",
"attachment/header/tag support"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/emails/batch.ts",
"kind": "source_file",
"used_for": [
"emails batch file format",
"100 cap",
"unsupported fields"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/emails/receiving/listen.ts",
"kind": "source_file",
"used_for": [
"NDJSON stream behaviour",
"5 consecutive failures exit"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/webhooks/listen.ts",
"kind": "source_file",
"used_for": [
"temporary webhook local dev loop",
"forwarding with Svix headers"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/domains/create.ts",
"kind": "source_file",
"used_for": [
"receiving capability at creation",
"regions",
"TLS choices"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/domains/update.ts",
"kind": "source_file",
"used_for": [
"current update surface and receiving-toggle ambiguity"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/broadcasts/create.ts",
"kind": "source_file",
"used_for": [
"broadcast scheduling",
"topic/segment fields",
"triple-brace interpolation"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/broadcasts/send.ts",
"kind": "source_file",
"used_for": [
"draft-send semantics",
"natural-language scheduling"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/templates/index.ts",
"kind": "source_file",
"used_for": [
"template lifecycle",
"published templates note"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/templates/create.ts",
"kind": "source_file",
"used_for": [
"template flags",
"variable declarations"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/templates/publish.ts",
"kind": "source_file",
"used_for": [
"publish semantics"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/topics/create.ts",
"kind": "source_file",
"used_for": [
"topic default_subscription"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/segments/create.ts",
"kind": "source_file",
"used_for": [
"segments over audiences guidance"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/contacts/create.ts",
"kind": "source_file",
"used_for": [
"contact properties JSON",
"segment assignment",
"global unsubscribed"
]
},
{
"url": "https://raw.githubusercontent.com/resend/resend-cli/main/src/commands/api-keys/create.ts",
"kind": "source_file",
"used_for": [
"sending_access vs full_access",
"domain-scoped keys"
]
},
{
"url": "https://resend.com/docs/webhooks/verify-webhooks-requests",
"kind": "official_docs",
"used_for": [
"Svix verification and raw-body guidance"
]
},
{
"url": "https://resend.com/docs/knowledge-base/403-error-domain-mismatch",
"kind": "official_docs",
"used_for": [
"exact-domain mismatch guidance"
]
},
{
"url": "https://resend.com/docs/dashboard/receiving/introduction",
"kind": "official_docs",
"used_for": [
"underlying inbound/receiving semantics"
]
},
{
"url": "https://resend.com/docs/mcp-server",
"kind": "official_docs",
"used_for": [
"MCP fallback surface"
]
},
{
"url": "https://resend.com/changelog",
"kind": "official_changelog",
"used_for": [
"30-day scheduling support"
]
},
{
"url": "https://resend.com/docs/api-reference/introduction",
"kind": "official_docs",
"used_for": [
"underlying API semantics relevant to CLI users"
]
}
]
}
{
"default_global_flags": [
"--json",
"-q"
],
"default_env": {
"RESEND_NO_UPDATE_NOTIFIER": "1"
},
"auth_priority": [
"--api-key",
"RESEND_API_KEY",
"stored config/profile"
],
"secret_recommendation": "Prefer RESEND_API_KEY or stored profiles instead of embedding secrets in arguments.",
"parse_order": [
"stdout full JSON",
"stdout NDJSON",
"stderr full JSON",
"stderr NDJSON",
"raw text fallback"
],
"streaming_commands": [
"webhooks listen",
"emails receiving listen"
],
"notes": [
"Global flags belong before the command: resend --json -q emails send ...",
"Use explicit flags in agent mode; avoid interactive prompts.",
"Keep stderr for diagnostics because JSON errors may be emitted there."
]
}
{
"meta": {
"skill": "resend-cli",
"version": "3.0.0",
"last_reviewed": "2026-03-14"
},
"routes": [
{
"id": "doctor",
"match_any": [
"doctor",
"debug",
"not working",
"ci",
"api key",
"auth",
"profile",
"which account",
"why is resend",
"broken"
],
"preferred_surface": "cli",
"recommended_sequence": [
"resend --json -q doctor"
],
"notes": [
"Start with environment diagnosis before mutation."
]
},
{
"id": "single-transactional-send",
"match_any": [
"send email",
"transactional",
"password reset",
"receipt",
"notification",
"welcome email",
"reply in thread"
],
"match_none": [
"100",
"250",
"70",
"batch",
"broadcast",
"bulk",
"campaign",
"different",
"many",
"newsletter"
],
"preferred_surface": "cli",
"recommended_sequence": [
"resend --json -q emails send --from ... --to ... --subject ... --text ..."
],
"notes": [
"Use emails send for one logical email.",
"Use --idempotency-key if retries are possible."
]
},
{
"id": "scheduled-transactional-send",
"match_any": [
"schedule email",
"scheduled email",
"tomorrow",
"later today",
"reschedule email",
"cancel scheduled"
],
"match_none": [
"broadcast",
"campaign"
],
"preferred_surface": "cli",
"recommended_sequence": [
"resend --json -q emails send --from ... --to ... --subject ... --text ... --scheduled-at 2026-03-15T08:00:00Z",
"resend --json -q emails update <email-id> --scheduled-at 2026-03-15T09:00:00Z",
"resend --json -q emails cancel <email-id>"
],
"notes": [
"Prefer ISO 8601 for transactional scheduling."
]
},
{
"id": "batch-send",
"match_any": [
"100",
"100 emails",
"200",
"250",
"70",
"70 emails",
"batch",
"bulk transactional",
"different",
"distinct notifications",
"many",
"many different emails",
"shipment notifications"
],
"preferred_surface": "cli",
"recommended_sequence": [
"python3 scripts/resend_cli.py lint-batch ./batch-emails.json",
"resend --json -q emails batch --file ./batch-emails.json --idempotency-key batch-001"
],
"notes": [
"Use when there are many distinct transactional emails.",
"Hard cap is 100 emails per request."
]
},
{
"id": "domains",
"match_any": [
"verify domain",
"dns",
"spf",
"dkim",
"sending domain",
"receiving domain",
"open tracking",
"click tracking",
"tls enforced"
],
"preferred_surface": "cli",
"recommended_sequence": [
"resend --json -q domains create --name example.com --region eu-west-1 --receiving",
"resend --json -q domains verify <domain-id>",
"resend --json -q domains get <domain-id>"
],
"notes": [
"CLI returns DNS records but cannot edit DNS on your provider."
]
},
{
"id": "webhooks",
"match_any": [
"webhook",
"svix",
"signature",
"event delivery",
"listen locally",
"ngrok"
],
"preferred_surface": "cli",
"recommended_sequence": [
"resend --json -q webhooks create --endpoint https://app.example.com/hooks/resend --events all",
"resend --json webhooks listen --url https://example.ngrok-free.app --forward-to localhost:3000/webhooks/resend"
],
"notes": [
"Verify raw body in the app.",
"webhooks listen is a long-running dev command."
]
},
{
"id": "inbound",
"match_any": [
"inbound",
"received email",
"receiving",
"support mailbox",
"email.received",
"forward inbound",
"attachment download"
],
"preferred_surface": "cli",
"recommended_sequence": [
"resend --json -q emails receiving list --limit 10",
"resend --json -q emails receiving get <received-email-id>",
"resend --json emails receiving listen --interval 5"
],
"notes": [
"Treat webhook events as triggers, then fetch the full message."
]
},
{
"id": "audience-model",
"match_any": [
"topics",
"segments",
"contacts",
"newsletter subscriptions",
"broadcast",
"audience",
"opt in",
"opt out",
"subscription management"
],
"preferred_surface": "cli",
"recommended_sequence": [
"resend --json -q topics create --name \"Product Updates\" --default-subscription opt_in",
"resend --json -q segments create --name \"Beta Users\"",
"resend --json -q contacts create --email jane@example.com --segment-id <segment-id>",
"resend --json -q broadcasts create --from hello@example.com --subject \"Weekly Update\" --segment-id <segment-id> --html-file ./broadcast.html"
],
"notes": [
"Prefer segments + topics over deprecated audiences."
]
},
{
"id": "templates",
"match_any": [
"template",
"hosted template",
"publish template",
"template variables",
"duplicate template"
],
"preferred_surface": "cli_with_possible_fallback",
"recommended_sequence": [
"resend --json -q templates create --name \"Welcome\" --html-file ./template.html --subject \"Welcome\" --var NAME:string",
"resend --json -q templates publish <template-id>"
],
"notes": [
"CLI clearly covers template lifecycle.",
"Direct hosted-template sending may require fallback; inspect gap notes."
]
},
{
"id": "api-keys",
"match_any": [
"api key",
"sending_access",
"domain-scoped key",
"ci token",
"scoped token"
],
"preferred_surface": "cli",
"recommended_sequence": [
"resend --json -q api-keys create --name \"ci-token\" --permission sending_access --domain-id <domain-id>"
],
"notes": [
"Token is shown once only."
]
},
{
"id": "multi-account",
"match_any": [
"multiple accounts",
"multiple teams",
"switch profile",
"staging and production",
"profile"
],
"preferred_surface": "cli",
"recommended_sequence": [
"resend auth switch",
"resend --profile staging --json -q doctor",
"resend --profile production --json -q domains list"
],
"notes": [
"Prefer explicit --profile on live mutations."
]
}
]
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Template</title>
</head>
<body>
<h1>Welcome {{{NAME}}}</h1>
<p>We are glad you are here.</p>
</body>
</html>
Changelog
3.0.0 — 2026-03-14
This is a CLI-first rewrite of the skill.
What changed
- Reoriented the skill around the official
resendCLI rather than raw API calls. - Added an explicit agent subprocess contract for deterministic invocation and parsing.
- Added a coverage-gap model so agents know when to fall back to MCP/API instead of guessing.
- Added a new
scripts/resend_cli.pywrapper for: - CLI probing
- task routing
- command lookup
- scaffold generation
- batch linting
- static diagnosis
- safe subprocess execution with stdout/stderr JSON tolerance
- Added richer machine-readable assets:
command-catalog.jsontask-router.jsoncoverage-gaps.jsonsubprocess-contract.jsonsource-manifest.json- Added CLI-native playbooks for:
- transactional sends
- batch sends
- domains and DNS
- webhooks and local listeners
- inbound receiving
- contacts, topics, segments, broadcasts
- templates and current template-send caveats
- multi-profile usage
- CI/CD usage
Important behavioural changes from v2
- The skill now defaults to CLI → MCP/API fallback, not SDK/API-first.
- The skill treats long-running
listencommands as event streams, not ordinary request/response commands. - The skill now explicitly models the current
stderrJSON discrepancy, the template-send gap, and the domains capability-toggle ambiguity.
{
"skill_name": "resend-cli",
"version": "3.0.0",
"evals": [
{
"id": 1,
"prompt": "Use the Resend CLI to send a scheduled password reset email tomorrow at 09:00 Berlin time.",
"expected_output": "A CLI-first answer using emails send with explicit flags, an ISO timestamp, and idempotency guidance.",
"assertions": [
"Chooses resend emails send rather than batch or broadcasts",
"Uses a concrete scheduled-at example",
"Prefers ISO 8601 for the transactional schedule",
"Mentions idempotency for retries",
"Does not claim the user must use raw REST"
]
},
{
"id": 2,
"prompt": "I need to send 70 different shipment notifications. Which Resend CLI command should my agent use?",
"expected_output": "A recommendation to use emails batch with a JSON file and the right limits.",
"assertions": [
"Chooses resend emails batch",
"Mentions the 100-email limit",
"Mentions a JSON file input",
"Warns that attachments are not supported",
"Warns that scheduled_at is not supported"
]
},
{
"id": 3,
"prompt": "I have 250 distinct notifications to send. Can the Resend CLI do that in one call?",
"expected_output": "A chunking plan that keeps batches at 100 or fewer and uses multiple invocations.",
"assertions": [
"Says one batch is not enough",
"Recommends chunking across multiple emails batch calls",
"Mentions 100 as the cap",
"Mentions throttling or pacing"
]
},
{
"id": 4,
"prompt": "Create a sending and receiving domain for eu.example.com in the EU region using the Resend CLI.",
"expected_output": "A domain-create workflow with --region eu-west-1, --receiving, DNS, and verify polling.",
"assertions": [
"Chooses resend domains create",
"Uses eu-west-1",
"Includes receiving capability",
"Mentions DNS record configuration outside the CLI",
"Includes resend domains verify and domains get as follow-up"
]
},
{
"id": 5,
"prompt": "Set up local webhook development with ngrok and forward payloads to localhost:3000.",
"expected_output": "A webhooks listen answer that treats the command as a long-running stream and mentions the temporary webhook lifecycle.",
"assertions": [
"Chooses resend webhooks listen",
"Includes --url and --forward-to",
"Describes it as a long-running listener",
"Mentions the temporary webhook cleanup behaviour",
"Mentions Svix headers or webhook verification"
]
},
{
"id": 6,
"prompt": "I want my agent to watch for new inbound emails from Resend.",
"expected_output": "A receiving-listen answer that treats output as NDJSON and does not confuse it with a bounded request.",
"assertions": [
"Chooses resend emails receiving listen",
"Mentions NDJSON or one JSON object per line",
"Mentions that the command is long-running",
"Does not describe it as a one-shot response"
]
},
{
"id": 7,
"prompt": "Can the Resend CLI send a hosted template by template ID?",
"expected_output": "A careful answer that distinguishes the template lifecycle from direct template sending and surfaces the current CLI gap.",
"assertions": [
"Mentions templates create/update/publish lifecycle",
"Notes that the current emails send flags do not clearly expose template-id sending",
"Suggests inspecting local help or falling back to MCP/API",
"Does not confidently invent a non-existent --template-id flag"
]
},
{
"id": 8,
"prompt": "Use the Resend CLI to create a topic, a segment, a contact, and then a broadcast.",
"expected_output": "A coherent subscription architecture that uses topics and segments correctly.",
"assertions": [
"Uses resend topics create",
"Uses resend segments create",
"Uses resend contacts create",
"Uses resend broadcasts create",
"Distinguishes topics from segments",
"Prefers segments over deprecated audiences"
]
},
{
"id": 9,
"prompt": "How should my agent authenticate to the Resend CLI in CI?",
"expected_output": "An auth answer that prefers RESEND_API_KEY or profiles over interactive login.",
"assertions": [
"Mentions RESEND_API_KEY",
"Mentions auth priority or profiles",
"Avoids relying on interactive resend login in CI",
"Mentions resend doctor as a quick verification step"
]
},
{
"id": 10,
"prompt": "My wrapper assumes the Resend CLI always writes JSON errors to stdout. Is that safe?",
"expected_output": "A defensive parsing answer that notes the current stderr discrepancy.",
"assertions": [
"Says wrappers should parse both stdout and stderr",
"References the current JSON error channel discrepancy",
"Keeps the return code",
"Does not claim stdout-only parsing is always safe"
]
},
{
"id": 11,
"prompt": "I need a domain-scoped send-only token for my CI pipeline.",
"expected_output": "A CLI-first api-keys create answer using sending_access and domain-id.",
"assertions": [
"Chooses resend api-keys create",
"Uses --permission sending_access",
"Uses --domain-id",
"Mentions that the token is shown only once"
]
},
{
"id": 12,
"prompt": "The user already created a Resend domain, but now wants to enable receiving on it via the CLI.",
"expected_output": "A careful answer that surfaces the current domains update ambiguity instead of overpromising.",
"assertions": [
"Mentions that domains update currently exposes TLS/open/click tracking",
"Notes the ambiguity around receiving toggles",
"Suggests inspecting local help or falling back if necessary",
"Does not claim a definitely-supported receiving toggle without caveat"
]
},
{
"id": 13,
"prompt": "What should my agent run first when Resend commands fail in an unknown environment?",
"expected_output": "A concise answer recommending resend doctor with JSON mode.",
"assertions": [
"Chooses resend --json -q doctor",
"Mentions version/auth/domain checks",
"Uses CLI-first diagnosis rather than raw API debugging"
]
},
{
"id": 14,
"prompt": "Use the Resend CLI to schedule a broadcast for tomorrow morning.",
"expected_output": "A broadcasts answer using natural-language scheduling or a valid explicit schedule string.",
"assertions": [
"Chooses broadcasts create --send or broadcasts send",
"Mentions natural-language scheduling is supported for broadcasts",
"Includes segment-id and message body requirements",
"Does not use emails batch for a campaign"
]
},
{
"id": 15,
"prompt": "How do I reply to an inbound email in the same thread if I am using the Resend CLI?",
"expected_output": "A response that uses emails send with threading headers after retrieving the inbound message.",
"assertions": [
"Uses emails receiving get to fetch inbound context",
"Uses emails send for the reply",
"Mentions In-Reply-To and References",
"Does not confuse forward with reply-threading"
]
},
{
"id": 16,
"prompt": "Should my agent use the Resend CLI, MCP, SDK, or raw REST?",
"expected_output": "A surface-selection answer that prefers the CLI for live terminal/CI operations and falls back deliberately.",
"assertions": [
"Prefers the official CLI for live terminal or CI work",
"Mentions MCP as a fallback or alternative live surface",
"Mentions SDK for app-code integration",
"Treats raw REST as low-level/debug/fallback"
]
},
{
"id": 17,
"prompt": "I want my agent to parse the output of resend emails receiving listen.",
"expected_output": "An answer that treats the command as a streaming NDJSON source.",
"assertions": [
"Mentions NDJSON or one JSON object per line",
"Mentions it is long-running",
"Recommends timeout/supervision or explicit stop conditions"
]
},
{
"id": 18,
"prompt": "Why does sending from onboarding@resend.dev to a customer fail, even when I use the CLI?",
"expected_output": "An explanation of resend.dev testing limits and the need for a verified custom domain.",
"assertions": [
"Explains that resend.dev is testing-only",
"Recommends verifying a custom domain",
"Does not imply the CLI bypasses domain restrictions"
]
}
]
}
Agent operating model
This file explains how to behave like a careful Resend CLI operator instead of a generic shell scripter.
1) Classify the task first
Put every request into one of these buckets before you touch the terminal:
| Task shape | Typical examples | First move |
|---|---|---|
| Environment diagnosis | “Why is Resend not working in CI?” | resend --json -q doctor |
| Bounded live mutation | send email, create domain, create webhook | choose command, prepare files, run with --json -q |
| Long-running dev loop | local webhook or inbound listener | choose a listen command and treat output as a stream |
| Architecture / modelling | subscription model, domains, template strategy | choose primitive before command |
| Coverage-gap handling | hosted template send, receiving toggle on existing domain | surface the gap early and fall back deliberately |
2) Prefer CLI-first, but do not become CLI-dogmatic
Use the official CLI when:
- the user wants a real account operation from an agent, shell, or CI environment
- structured subprocess output is useful
- you want the CLI to set Resend-specific client behaviour for you
Use MCP/API/SDK only when:
- the CLI is unavailable and cannot be installed
- the CLI does not expose the feature cleanly enough
- the user needs idiomatic application code, not an operational command
3) Use the right command class
Bounded commands
These return and exit:
doctoremails send,emails list,emails get,emails batch,emails update,emails canceldomains create,domains get,domains list,domains update,domains verifywebhooks create,webhooks get,webhooks list,webhooks updatecontacts *,topics *,segments *,broadcasts *,templates *,api-keys *
For these, use:
resend --json -q <command> ...Streaming commands
These stay alive until interrupted:
resend webhooks listenresend emails receiving listen
For these:
- do not pretend they are normal request/response calls
- expect line-oriented event output in JSON/piped mode
- manage them like supervised background processes with explicit stop conditions
- keep stderr for status and operational context
4) Default mutation workflow
For live mutations:
1. choose the correct primitive 2. choose the exact CLI command 3. build any file input first (--html-file, batch JSON) 4. run with deterministic flags 5. verify with a read/list/get command 6. save the returned IDs
5) Default safety rules
- Prefer environment variables or stored profiles over putting secrets in command arguments.
- Add an idempotency key for
emails sendandemails batchwhen retries are possible. - Prefer
--html-fileover inlining large HTML strings into shell commands. - Prefer explicit ISO 8601 timestamps for agent-generated transactional schedules.
- For any webhook plan, include signature verification, dedupe, and out-of-order handling.
- For inbound plans, treat the webhook as a trigger and then fetch the full message/attachments.
6) How to answer well
A good Resend CLI answer usually contains:
1. the primitive, 2. the command sequence, 3. any file content the user needs, 4. the caveats that actually matter, 5. the verification step, 6. the fallback path if the CLI surface is incomplete.
Command selection
Pick the primitive first. Then pick the CLI command sequence.
Quick routing matrix
| Need | Use | Main commands | Notes |
|---|---|---|---|
| One logical transactional email | Emails | emails send | One email can still have multiple recipients |
| Update or cancel a scheduled transactional email | Emails | emails update, emails cancel | Keep the returned email ID |
| Up to 100 distinct transactional emails | Batch emails | emails batch | Requires a JSON file; no attachments or scheduled_at |
| Campaign/newsletter to a group | Broadcasts | broadcasts create, broadcasts send | Pair with segments and usually topics |
| Reusable hosted template lifecycle | Templates | templates create, templates update, templates publish | Current CLI has an important direct-send caveat |
| Verified sender or receiving domain | Domains | domains create, domains verify, domains get, domains update | DNS changes still happen outside the CLI |
| Scoped credentials | API keys | api-keys create, api-keys list, api-keys delete | Prefer narrow sending_access keys |
| Recipient profile data and targeting | Contacts / Topics / Segments / Contact properties | contacts *, topics *, segments *, contact-properties * | Use segments over deprecated audiences |
| Real-time app notifications | Webhooks | webhooks create, webhooks update, webhooks listen | Save signing secrets immediately |
| Inbound message processing | Receiving + Webhooks | emails receiving * plus webhooks * | Fetch full message and attachments separately |
| Environment diagnosis | Doctor | doctor | Usually the fastest first check |
Decision notes
emails send vs emails batch
Use emails send when it is one logical message, even if --to contains multiple recipients.
Use emails batch when you truly have many distinct messages and you want to submit up to 100 of them in one request.
Transactional vs campaign
Use emails send / emails batch for user-specific, operational, or application-driven messages.
Use broadcasts create / broadcasts send when the message is a campaign to a defined segment and should respect marketing-style subscription state.
Topics vs segments
- Topics are recipient-facing preference categories.
- Segments are sender-controlled targeting groups.
They complement each other rather than replacing each other.
Webhook local dev
Use webhooks listen when the user needs a live local loop with a public tunnel URL and optional forwarding to a local app.
Inbound dev loop
Use emails receiving listen when the user wants to observe inbound mail arrival over time.
Templates
Use templates * for the hosted draft/publish lifecycle, but check references/templates-and-coverage-gaps.md before promising a pure CLI send-by-template workflow.
Contacts, topics, segments, and broadcasts
This file covers Resend's CLI surface for subscription modelling and campaign delivery.
Model the data correctly
Contact properties
Use contact properties for profile data and segmentation inputs.
Examples:
- plan
- company
- country
- signup source
Topics
Topics are recipient-facing preference categories.
Examples:
- Product Updates
- Security Alerts
- Weekly Digest
A topic's default_subscription matters. Choose it intentionally.
Segments
Segments are sender-controlled targeting groups.
Examples:
- beta users
- German customers
- enterprise plan
- trial users from the last 14 days
Global unsubscribe vs topic subscriptions
These are different layers:
- global unsubscribed = no broadcasts at all
- topic subscription = whether a contact wants a specific category
Do not collapse them into the same boolean.
topics create
Good example:
resend --json -q topics create \
--name "Product Updates" \
--description "Feature launches and important product news" \
--default-subscription opt_insegments create
Good example:
resend --json -q segments create --name "Beta Users"contacts create
Good example:
resend --json -q contacts create \
--email jane@example.com \
--first-name Jane \
--last-name Smith \
--properties '{"plan":"pro","company":"Acme"}' \
--segment-id <segment-id>Notes:
first-nameandlast-namemap to standard properties--propertiesexpects a JSON string--unsubscribedis a team-wide opt-out from broadcasts
Broadcasts
Use broadcasts for campaign-style sends to a segment.
broadcasts create
This creates a draft, or sends immediately with --send.
Key options:
--from--subject--segment-id- one of
--html,--html-file,--text - optional
--topic-id - optional
--send - optional
--scheduled-at
Sample draft:
resend --json -q broadcasts create \
--from hello@example.com \
--subject "Weekly Update" \
--segment-id <segment-id> \
--html-file ./broadcast.html \
--topic-id <topic-id>Scheduling broadcasts
Broadcast scheduling explicitly accepts both:
- ISO 8601
- natural language (for example
tomorrow at 9am ET,in 1 hour)
That makes broadcasts a good CLI-native place to use human-friendly schedule strings.
broadcasts send
Use this to send or schedule a draft broadcast later.
Important caveat:
- only broadcasts created via the API/CLI can be sent this way; dashboard-created broadcasts are not
currently sendable programmatically through this command
Audiences
For new designs, prefer segments + topics + contacts over deprecated audiences.
Diagnostics and fallbacks
This file covers the fastest debug order and the deliberate fallback rules.
Start with doctor
When the environment is unclear:
resend --json -q doctorRead it in this order:
1. did the CLI run at all? 2. is a key resolved? 3. is the key coming from the expected source? 4. are there verified domains? 5. is this obviously an agent environment?
Common failure order
1. CLI missing
Check install first. Do not assume resend exists on the machine.
2. Auth resolution failure
If you see auth_error:
- check
RESEND_API_KEY - check
--profile - check whether the expected profile actually exists
- run
doctor
3. Domain mismatch / sending prerequisites
If sending fails:
- check the exact
fromdomain/subdomain - check whether the domain is verified
- check whether the account has verified domains at all
- remember that
resend.devis not for real customer sending
4. Batch shape failure
If emails batch fails:
- ensure the input is valid JSON
- ensure it is an array
- ensure it has 100 items or fewer
- ensure no item includes
attachments - ensure no item includes
scheduled_at
5. Webhook/inbound design mistakes
If webhook handling is flaky:
- verify raw body, not parsed JSON
- dedupe on
svix-id - assume retries
- assume out-of-order delivery
CLI-specific quirks to remember
Output channel discrepancy
Use tolerant parsing because JSON errors may be written to stderr.
Update notices
Set RESEND_NO_UPDATE_NOTIFIER=1 for deterministic output in wrappers.
Long-running commands
Do not run listen commands as if they were bounded commands. Supervise them explicitly.
When to fall back to MCP/API
Fallback is appropriate when:
- the CLI is not installed and cannot be installed
- the installed CLI version does not expose the required surface
- the flow is currently better represented through MCP/API
Current likely fallback cases:
- sending via hosted
template_idfrom the CLI - enabling receiving on an already-created domain if local
domains update --helpstill lacks the toggle - any future feature that exists in docs/API but is not exposed in the installed CLI
How to phrase a fallback well
Good fallback phrasing:
1. explain exactly what the CLI covers, 2. identify the missing or ambiguous piece, 3. name the fallback surface, 4. preserve the user's goal.
Example:
The CLI clearly covers template creation/publish, but the current emails send flags do not showa direct hosted-template send path. If you want true hosted-template sends today, the safest next
step is MCP/API for that final send operation.
Domains, DNS, and deliverability
This file covers the domain setup issues that most often break Resend operations.
Domain creation
Use domains create to bootstrap a sending or receiving domain and obtain the DNS records to add in your DNS provider.
Sample:
resend --json -q domains create --name eu.example.com --region eu-west-1 --receivingImportant creation options
--name--region(us-east-1,eu-west-1,sa-east-1,ap-northeast-1)--tls(opportunisticorenforced)--sending--receiving
Verification flow
A correct domain flow is:
1. create the domain 2. read the returned DNS records 3. add those records in the DNS provider 4. run domains verify 5. poll with domains get / domains list 6. only then send from that domain
The CLI cannot finish DNS work by itself.
from address rule
The from address must match the exact verified domain.
Examples:
- verified
mail.example.com→ send fromalerts@mail.example.com - verified
example.com→ send fromalerts@example.com
Do not assume a verified subdomain automatically authorises the parent domain, or vice versa.
resend.dev
resend.dev is testing-only. It is not the answer for real customer delivery.
If the user wants real sending, point them toward verifying a domain they control.
Receiving and MX planning
If the user already uses another provider’s MX records on the root domain, recommend a dedicated subdomain for Resend receiving, such as:
support.mail.example.cominbound.example.com
That avoids root-domain MX conflicts.
domains update
Current CLI domains update exposes:
--tls--open-tracking/--no-open-tracking--click-tracking/--no-click-tracking
Important ambiguity
The receiving command help text references enabling receiving via domains update, but the current domains update implementation does not expose a receiving/sending capability flag. Treat this as a real CLI ambiguity.
Practical guidance:
- if you know you need receiving, prefer setting
--receivingatdomains createtime - if the user wants to enable receiving on an already-created domain, inspect local help and be
ready to fall back to API/MCP if the toggle is not exposed
Domain verification polling
Use:
resend --json -q domains verify <domain-id>
resend --json -q domains get <domain-id>Verification is async. Do not assume the domain becomes verified immediately after verify.
API keys
For automation, use the narrowest scope that fits.
Good example:
resend --json -q api-keys create \
--name "ci-pipeline" \
--permission sending_access \
--domain-id <domain-id>Store the returned token immediately; it is shown once.
Inbound receiving and threading
This file covers the receiving side of the Resend CLI.
Recommended inbound architecture
For a production inbound mailbox:
1. enable receiving on a domain/subdomain 2. register a webhook that includes email.received 3. treat the webhook as a trigger only 4. use emails receiving get to fetch the full inbound message 5. use emails receiving attachments / attachment for files 6. only then process, store, or reply
Receiving command group
The CLI exposes:
emails receiving listemails receiving getemails receiving listenemails receiving attachmentsemails receiving attachmentemails receiving forward
emails receiving list
Use this for bounded polling or debugging:
resend --json -q emails receiving list --limit 10emails receiving get
Use this when you need the full content and metadata of one inbound message.
Key point: the returned data includes the raw-message download URL and full body fields. Use this instead of assuming the webhook already contains everything you need.
Attachments
Use:
resend --json -q emails receiving attachments <email-id>
resend --json -q emails receiving attachment <email-id> <attachment-id>Attachment download URLs are signed and short-lived. Fetch them when needed instead of storing them as if they were permanent URLs.
emails receiving forward
Use this when the task is “forward an inbound email to another address” rather than “reply in the same thread”.
emails receiving listen
This is a polling stream for inbound mail.
Properties that matter to agents:
- minimum interval is 2 seconds
- in JSON/piped mode, output is NDJSON
- the command exits after 5 consecutive API failures
- it is for observation/automation loops, not a bounded one-shot call
Sample:
resend --json emails receiving listen --interval 5Reply threading
If the user wants to reply into the same thread, use emails send, not emails receiving forward.
Include appropriate threading headers derived from the inbound message, such as:
In-Reply-ToReferences
Also consider prefixing the subject with Re: when appropriate.
Install, auth, and profiles
This file covers the setup decisions that most often decide whether a Resend CLI flow is smooth or painful.
Install methods
Preferred install methods depend on the environment:
| Environment | Good install choice |
|---|---|
| Generic macOS/Linux shell | `curl -fsSL https://resend.com/install.sh \ |
| Node-heavy dev machine | npm install -g resend-cli |
| Homebrew-managed machine | brew install resend/cli/resend |
| Windows PowerShell | `irm https://resend.com/install.ps1 \ |
The published CLI release is currently v1.4.1.
Local development vs operational use
The repo README documents Node.js 20+ for local development/building the CLI from source. That is not the same thing as requiring Node for every normal user; native install paths also exist.
Authentication priority
The CLI resolves credentials in this order:
1. --api-key 2. RESEND_API_KEY 3. stored config from resend login
For agents and CI, prefer:
1. RESEND_API_KEY for one-off automation, or 2. a stored named profile plus --profile / RESEND_PROFILE for multi-account setups.
Why not default to --api-key?
It has highest priority, but environment variables or stored profiles are usually safer than putting secrets into command-line arguments that may appear in process listings, shell history, or logs.
resend login
Use resend login mainly for human setup.
In non-interactive mode, --key is required:
resend login --key re_xxxxxxxxxxxxxThe README documents that login validates the key before saving. If you are scripting, still prefer RESEND_API_KEY or pre-seeded profiles so you do not need an interactive auth step at all.
Profiles
Profiles are the CLI-native way to switch between Resend accounts or teams.
Useful patterns:
resend auth switch
resend --profile production --json -q domains list
RESEND_PROFILE=staging resend --json -q doctorImportant profile notes
- The config layer prefers
RESEND_PROFILE. RESEND_TEAMis legacy compatibility, not the preferred new name.- Profile names are validated and should stay simple (
letters,numbers,_,-).
Config paths and permissions
The CLI stores config under:
- Linux/macOS default:
~/.config/resend/ - Linux override:
$XDG_CONFIG_HOME/resend - Windows:
%APPDATA%/resend
Credentials go in credentials.json. The README and source both indicate restrictive permissions:
- config directory:
0700 - credentials file:
0600
Practical agent guidance
Single account CI job
export RESEND_API_KEY=re_xxxxxxxxx
resend --json -q doctor
resend --json -q emails send --from deploy@example.com --to team@example.com --subject "Deploy complete" --text "Done"Multiple accounts
Use stored profiles and pass --profile explicitly on every live mutation that must hit a specific account. Do not rely on whichever profile was active last unless the user explicitly wants that.
When auth looks wrong
Run:
resend --json -q doctorThen check:
- whether a key is resolved at all
- whether it came from the expected source
- whether verified domains exist in that account
Recipes
These are concise end-to-end CLI-native playbooks.
1. Transactional send with retry safety
1. prepare HTML file if needed 2. run emails send 3. include --idempotency-key 4. verify with emails get
resend --json -q emails send \
--from "Acme <alerts@example.com>" \
--to alice@example.com \
--subject "Welcome" \
--html-file ./newsletter.html \
--idempotency-key welcome-0012. Scheduled password reset
1. choose emails send 2. use explicit ISO timestamp 3. keep returned email ID 4. use emails update or emails cancel if needed
3. Batch shipment notifications
1. build batch-emails.json 2. lint the file 3. run emails batch 4. chunk if more than 100 items
python3 scripts/resend_cli.py lint-batch ./batch-emails.json
resend --json -q emails batch --file ./batch-emails.json --idempotency-key shipments-0014. Sending + receiving domain
1. domains create --receiving 2. add DNS records in the DNS provider 3. domains verify 4. poll with domains get 5. only then send or receive
5. Local webhook loop
1. open a tunnel to the local port 2. run webhooks listen --url ... --forward-to ... 3. collect events for a bounded time 4. stop the process cleanly
6. Inbound mailbox processor
1. create receiving-capable domain/subdomain 2. create webhook including email.received 3. on each event, call emails receiving get 4. fetch attachments when needed 5. reply using emails send with threading headers
7. Subscription model for newsletters
1. topics create 2. segments create 3. contacts create with properties and segment IDs 4. broadcasts create with --topic-id 5. broadcasts send or broadcasts create --send
8. Domain-scoped CI token
resend --json -q api-keys create \
--name "ci-token" \
--permission sending_access \
--domain-id <domain-id>9. Hosted template lifecycle
1. templates create 2. templates publish 3. if the user needs hosted-template sends, check the current local CLI help 4. if direct send-by-template is still missing, fall back deliberately
10. Multi-account operations
Use explicit profiles:
resend --profile staging --json -q doctor
resend --profile production --json -q domains listSending, scheduling, and batch operations
This file covers the transactional sending surface of the Resend CLI.
emails send
Use this for one logical email.
Required flags
--from--to--subject- one of:
--text--html--html-file
High-value optional flags
--cc--bcc--reply-to--scheduled-at--attachment--headers key=value--tags name=value--idempotency-key
Agent recommendations
- Prefer
--html-filefor long content. - Use
--idempotency-keywhenever retries are plausible. - Prefer explicit ISO timestamps for transactional schedules.
- Keep
--fromon an exact verified domain/subdomain.
Sample:
resend --json -q emails send \
--from "Acme <alerts@example.com>" \
--to user@example.com \
--subject "Password reset" \
--html-file ./newsletter.html \
--idempotency-key password-reset-001Scheduling transactional emails
The CLI surface for emails send and emails update documents ISO 8601 style scheduling for transactional email. Use that in agent-generated commands because it is explicit and reproducible.
Useful follow-ups:
resend --json -q emails update <email-id> --scheduled-at 2026-03-15T08:00:00Z
resend --json -q emails cancel <email-id>Scheduling limits
Resend supports scheduling up to 30 days in advance. Keep that underlying product limit in mind when you generate future-dated plans.
emails batch
Use batch sends for up to 100 distinct transactional emails in one request.
File format
emails batch expects a JSON file containing an array of email objects.
Bundled sample:
assets/batch-emails.json
Important batch rules
- hard limit: 100 emails per request
- unsupported per-email fields:
attachmentsscheduled_at- optional idempotency key exists for the whole batch
- validation mode can be
strictorpermissive
Sample:
resend --json -q emails batch --file ./batch-emails.json --idempotency-key shipment-batch-001Chunking rule
If the user needs 250 notifications:
1. split into chunks of 100, 100, and 50 2. send multiple batch requests 3. throttle appropriately
emails list and emails get
Use list for summary and pagination:
resend --json -q emails list --limit 10
resend --json -q emails list --after <cursor> --limit 10Use get for one specific message record:
resend --json -q emails get <email-id>Attachments
Attachments are supported on emails send, not on emails batch.
Before attaching files:
- ensure the file exists
- consider total size after Base64 expansion
- use
emails send, notemails batch
Tags and headers
Good uses:
- correlation IDs
- environment markers
- flow names
- customer or tenant grouping
Keep them short and machine-stable.
Sources and refresh notes
This skill was built from first-party Resend sources and the public Resend CLI source tree.
Primary sources
Official CLI announcement
Used for:
- release date
- install methods
- “built for humans and AI agents”
- 53 commands across 13 resources
- natural-language scheduling examples for broadcasts
- profile switching and CI-oriented JSON output positioning
URL:
- https://resend.com/changelog/cli
CLI repository README
Used for:
- auth priority
- login behaviour
- config paths and permissions
doctor- global flags
- machine-mode contract
- Node 20+ local-dev note
- current release pointer
URL:
- https://github.com/resend/resend-cli
CLI source files
Used for exact command surfaces and agent-relevant implementation details.
Representative files:
src/lib/output.tssrc/lib/client.tssrc/lib/config.tssrc/lib/pagination.tssrc/commands/emails/send.tssrc/commands/emails/batch.tssrc/commands/emails/receiving/*.tssrc/commands/domains/*.tssrc/commands/webhooks/*.tssrc/commands/broadcasts/*.tssrc/commands/templates/*.tssrc/commands/contacts/create.tssrc/commands/topics/create.tssrc/commands/segments/create.tssrc/commands/api-keys/create.ts
Repository root:
- https://github.com/resend/resend-cli
Official product/API docs
Used for underlying Resend platform semantics that still matter when the CLI is the chosen surface:
- webhook verification with Svix and raw body handling
- exact-domain mismatch behaviour
resend.devrestrictions- scheduling limits
- inbound/receiving behaviour
- MCP as a valid fallback surface
Representative URLs:
- https://resend.com/docs/api-reference/introduction
- https://resend.com/docs/webhooks/verify-webhooks-requests
- https://resend.com/docs/knowledge-base/403-error-domain-mismatch
- https://resend.com/docs/dashboard/receiving/introduction
- https://resend.com/docs/mcp-server
- https://resend.com/changelog
Refresh checklist
When updating this skill:
1. check the latest release version 2. re-read the README auth/output sections 3. inspect emails send for hosted-template flags 4. inspect domains update for receiving/sending capability toggles 5. inspect listen commands for output/stream changes 6. re-test the stderr/stdout JSON behaviour 7. update the command catalogue and gap map accordingly
Subprocess contract for AI agents
This file describes how an agent should invoke the official resend CLI so output stays deterministic and machine-friendly.
Default bounded invocation
For most commands, use:
resend --json -q <command> ...Why:
--jsonforces structured output-qsuppresses spinners/status noise and implies JSON mode- global flags go before the command/subcommand
Default environment
Set this unless you have a reason not to:
RESEND_NO_UPDATE_NOTIFIER=1That prevents update notices from appearing and keeps output stable.
Auth for agents
Prefer one of these:
1. RESEND_API_KEY in the environment 2. --profile <name> with a pre-seeded profile
Avoid interactive prompts and avoid raw --api-key unless you intentionally want to override the resolved key for one invocation.
Output parsing order
Use this parser strategy:
1. try full JSON from stdout 2. try NDJSON from stdout 3. try full JSON from stderr 4. try NDJSON from stderr 5. fall back to raw text only if structured parse fails everywhere
Why parse stderr too?
The README documents JSON-to-stdout for machine mode, but the current output.ts implementation uses console.error for JSON error output. Agents should therefore parse both channels.
Streaming commands
Treat these specially:
resend webhooks listenresend emails receiving listen
Guidance:
- do not wait forever unless the user explicitly wants a long-running stream
- if you need only a short observation window, run with a supervisor/timeout
- in JSON/piped mode, expect one JSON object per line
- keep stderr because it carries useful operational status in interactive mode
File inputs
Use files, not shell-escaped blobs, when content is non-trivial.
Prefer:
--html-file ./message.html--file ./batch-emails.json
over very long inline strings.
Secret handling
Good:
- environment variables injected by CI
- stored profiles
- secret managers that export
RESEND_API_KEY
Riskier:
--api-keyinside recorded shell history- logging fully expanded commands with secrets included
Suggested bounded wrapper behaviour
A robust wrapper should:
- prepend
--json -q - set
RESEND_NO_UPDATE_NOTIFIER=1 - capture stdout and stderr
- parse both channels
- preserve the return code
- include the raw text in its structured result for debugging
The bundled scripts/resend_cli.py run command does exactly this.
Templates and current CLI coverage gaps
This file covers the template lifecycle and the most important “do not promise this too early” behaviour for agents.
Template lifecycle
The CLI clearly supports the hosted template workflow:
templates createtemplates gettemplates listtemplates updatetemplates publishtemplates duplicatetemplates delete
Typical sequence:
1. create a draft template 2. update as needed 3. publish it 4. re-publish after later edits
templates create
Useful options include:
--name--htmlor--html-file--subject--text--from--reply-to--alias--var KEY:typeor--var KEY:type:fallback
Template variables use triple-brace syntax:
<h1>Hello {{{NAME}}}</h1>
<p>Total: {{{PRICE}}}</p>templates publish
Publishing promotes the draft to live use. After editing a published template, publish again to make the new draft current.
Important current caveat: direct template sends
The template command group says published templates can be used in emails via template_id.
However, the current emails send command implementation does not expose a direct --template-id / template-variable flag surface.
How to handle this safely
If the user asks to manage hosted templates:
- stay in the CLI
- use the
templates *commands
If the user asks to send using a hosted template:
1. say that the hosted template lifecycle exists in the CLI, 2. note that direct send-by-template is not clearly exposed in the current emails send flags, 3. offer one of these paths:
- render the content to local HTML and use
emails send --html-file - fall back to MCP/API for true hosted-template sending
- inspect local
resend emails send --helpin case the installed CLI has advanced beyond this snapshot
Do not pretend the flag definitely exists unless local help proves it.
Other important gaps and ambiguities
Domain receiving toggle ambiguity
Inbound help text references enabling receiving via domains update, but the current domains update implementation does not expose that option. Plan receiving at domains create time or fall back if needed.
JSON error channel discrepancy
The README promises JSON-to-stdout for machine mode, but the current implementation writes JSON errors to stderr. Wrappers must parse both.
Transactional natural-language scheduling
Broadcast commands explicitly support natural-language scheduling. Transactional emails send / emails update help documents ISO examples. For agents, prefer ISO on transactional sends even if the underlying API is more permissive.
Practical template advice for agents
- Do not promise a pure CLI hosted-template send flow unless local help confirms it.
- Do not mix “hosted template lifecycle” and “raw HTML file send” as if they were the same thing.
- If the user mainly needs hosted-template sends in production automation, MCP/API may currently be
the cleaner path.
Webhooks and local listeners
This file covers both production webhook registration and local development listening.
webhooks create
Use webhooks create to register an HTTPS endpoint and subscribe to events.
Sample:
resend --json -q webhooks create \
--endpoint https://app.example.com/hooks/resend \
--events email.sent email.delivered email.bouncedImportant rules
- endpoint must use HTTPS
--eventsreplaces “which events do I care about?”allis supported as shorthand for all current event types- the returned
signing_secretis shown once — store it immediately
webhooks update
Use this to replace the endpoint URL, replace the full event list, or enable/disable delivery.
Be careful: --events is a replacement, not an additive merge.
Production-safe webhook handling
The CLI registers webhooks, but your application still needs to verify them safely.
Always:
1. read the raw request body 2. verify with the signing secret 3. use svix-id, svix-timestamp, and svix-signature 4. dedupe on svix-id 5. assume at-least-once delivery 6. assume events may arrive out of order
webhooks listen
This is a local development command, not a normal bounded request.
What it does:
1. starts a local HTTP server on --port (default 4318) 2. creates a temporary webhook pointing at your public --url 3. prints incoming events 4. optionally forwards payloads to --forward-to 5. deletes the temporary webhook when you stop the process
Sample:
resend --json webhooks listen \
--url https://example.ngrok-free.app \
--forward-to localhost:3000/webhooks/resendAgent guidance for webhooks listen
- treat it as a stream
- use a timeout or explicit stop condition
- in JSON/piped mode, expect one JSON object per event
- keep stderr because interactive mode prints useful status there
Forwarding behaviour
When --forward-to is used, the CLI forwards the original payload with the Svix headers preserved. That makes it good for local app testing.
Event modelling
Important event families include:
- email events (
email.sent,email.delivered,email.bounced,email.received, etc.) - contact events
- domain events
Remember that event delivery is per recipient: one outbound action can generate multiple event records.