Command line

mailctl is the whole API from a shell: send a test in one line, watch what happened to it, and script the rest with --json.

npm i -g ./esendblue-cli.tgz

Node 18 or newer. The binary is mailctl.

Not on the public npm registry yet. Until it is, run npm pack in packages/cli and install the tarball it writes. Everything below is the same once it is on your PATH.

Log in

echo "$ESENDBLUE_KEY" | mailctl login --key - --url https://api.esendblue.com

Pipe the key in; do not type it as an argument. --key ep_… still works, but it stays in your shell history and ps shows it to every other account on the machine while the command runs. --key - reads it from standard input, so it is never an argument at all.

The profile is written to ~/.config/mailctl/config.json, readable only by you. Check what is active with:

mailctl whoami
mailctl health

The profile, and overriding it

Environment beats the saved file, and a flag beats both — so CI never has to write anything to disk.

NameWhat it does
MAILCTL_API_KEYThe key. Set this in CI instead of running login.
MAILCTL_API_URLThe API origin.
MAILCTL_ORG_IDOnly with a dashboard session token.
--key, --url, --orgOverride the profile for one command.
--jsonRaw JSON instead of a table, for piping into jq.

Send an email

mailctl emails send \
  --from "Your Company <receipts@yourdomain.com>" \
  --to customer@example.com \
  --subject "Your receipt" \
  --html "<p>Thanks for your order.</p>"

--to takes a comma-separated list. The other flags:

NameWhat it does
--textA plain-text part alongside the HTML. Worth sending.
--templateA published template id instead of a body.
--send-atAn ISO timestamp. The message is scheduled, and can be cancelled until it goes.
--attachA path. Read from disk and attached under its own filename.
--tagk=v, comma-separated for several. Your own labels, for filtering later.

This is the fastest way to prove a new domain works before writing any code.

Find one, and see what happened

mailctl emails list --status bounced --limit 50
mailctl emails list --q customer@example.com
mailctl emails get <id>
mailctl emails cancel <id>

get shows the message and every event it collected — queued, sent, delivered, opened, clicked, bounced. That timeline is the answer to “what happened to this one email”.

Inbound

mailctl inbound list

Mail that arrived at a domain with receiving turned on. Nothing appears here until the MX record is published.

Contacts

mailctl audiences list
mailctl audiences create "Donors"
mailctl contacts list --audience <id> --q mumbai
mailctl segments list
mailctl topics list

Import and export

mailctl contacts import <audience-id> --file contacts.csv
mailctl contacts export --audience <id> --out contacts.csv

The CSV needs a column called email; every other column becomes a property on the contact, which you can then segment on and use as a {{variable}}.

Broadcasts

mailctl broadcasts list
mailctl broadcasts send <id> --at 2026-09-10T09:00:00Z
mailctl broadcasts cancel <id>
mailctl broadcasts metrics <id>

metrics gives recipients, delivered, bounced, failed, opened and clicked — and clicks per link, which is the number that says whether the email did its job.

Automations

mailctl automations list
mailctl automations activate <id>
mailctl automations pause <id>
mailctl automations runs <id>
mailctl events send cart.abandoned --email donor@example.com

events send is how your own systems drive an event-triggered automation, and how a run parked on wait_for_event is let go.

Domains and keys

mailctl domains add mail.yourcompany.com
mailctl domains list
mailctl domains verify <id>

mailctl keys list
mailctl keys create "checkout service" --scopes emails:send
mailctl keys revoke <id>

A created key is printed once. It goes to standard output, so it lands in your scrollback — and in the log, if you run this in CI. Pipe it straight where it belongs rather than reading it off the screen.

Suppressions

mailctl suppressions list
mailctl suppressions add bad@example.com
mailctl suppressions remove good@example.com

Hard bounces and complaints arrive here on their own and are honoured on every send. Remove one only when you are sure it was a mistake.

Webhooks

mailctl webhooks list
mailctl webhooks add https://yourapp.com/hooks/mail
mailctl webhooks delete <id>

Metrics and logs

mailctl usage --from 2026-09-01 --to 2026-09-30
mailctl metrics --from 2026-09-01 --to 2026-09-30
mailctl logs --limit 50

metrics reports delivery and engagement, with a breakdown by receiving provider, the top bounce reasons, and the most clicked links. logs lists the API requests your integration actually made and what we answered — usually the fastest way to settle whether a problem is yours or ours.

Scripting

--json turns any command's output into the raw API response, so the CLI composes with everything else.

# every domain that is not verified yet
mailctl domains list --json | jq -r '.domains[] | select(.status != "verified") | .domain'

# suppress everything that bounced this week
mailctl emails list --status bounced --limit 200 --json \
  | jq -r '.emails[].to[]' | sort -u \
  | while read -r addr; do mailctl suppressions add "$addr"; done

Values printed as a table have their control characters removed first: a contact name comes from whoever filled in the form that created it, and a terminal does what escape sequences tell it. --json gives you the untouched response.

Exit codes

NameWhat it does
0The command did what it said.
1Anything else — a rejected request, a missing flag, an unreachable API. The reason is on stderr, with the API's own error code.

So set -e and && behave the way a script expects.

Every command

NameWhat it does
login, whoami, healthSave a profile, show the active one, check the API is reachable.
emailssend, list, get, cancel
inboundlist
domainslist, add, verify, delete
keyslist, create, revoke
audienceslist, create
contactslist, import, export
segments, topicslist
broadcastslist, send, cancel, metrics
automationslist, activate, pause, runs
eventssend
suppressionslist, add, remove
webhookslist, add, delete
usage, metrics, logsVolume, delivery and engagement, and the raw API request log.

mailctl --help prints the same list. Building this into an application instead? The Node SDK covers the same surface.