Email SDK
Reference

CLI

The complete email-sdk command manual — adapters, doctor, send, dry runs, credential flags, and exit codes.

The email-sdk CLI ships inside @opencoredev/email-sdk. Use it to list adapters, check credentials with doctor, validate a message with --dry-run, and run a real smoke send — no app code required.

Run it

One-off, without installing:

bunx --bun --package @opencoredev/email-sdk email-sdk adapters

Or install the package and use the local binary:

npm install @opencoredev/email-sdk
npx email-sdk adapters

Node 20+ or Bun 1.1+. The examples below use npx email-sdk; substitute the bunx form for no-install runs.

Commands

CommandAliasesWhat it does
help--help, -h, no commandPrint usage.
version--version, -vPrint the installed package name and version.
adaptersprovidersList every adapter with its required environment.
doctorCheck that one adapter's credentials are present.
sendValidate and send one message.

Any other command fails with Unknown command "x". and exit code 1.

Flag syntax

  • --flag value and --flag=value are equivalent; a flag without a value is treated as true.
  • --header, --tag, --metadata, and --attachment (alias --attach) are repeatable — pass them once per value.
  • Repeating any other flag keeps the last value.
  • --to, --cc, --bcc, and --reply-to accept comma-separated address lists.

help

npx email-sdk help

Prints usage, all send flags, and the adapter-specific option groups. Running email-sdk with no command does the same.

version

npx email-sdk version
# @opencoredev/email-sdk 0.6.1

npx email-sdk version --json
# { "name": "@opencoredev/email-sdk", "version": "0.6.1" }

Reads the metadata of the package actually installed on your machine — check it before comparing behavior against the docs.

adapters

npx email-sdk adapters

Prints one line per adapter: routing name, required environment variables, and a short note. --json prints the same data as a JSON array of { name, env, note } objects.

AdapterRequired environmentOptional environment
resendRESEND_API_KEY
postmarkPOSTMARK_SERVER_TOKENPOSTMARK_MESSAGE_STREAM
sendgridSENDGRID_API_KEY
cloudflareCLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_IDCLOUDFLARE_BASE_URL
unosendUNOSEND_API_KEYUNOSEND_BASE_URL
iterableITERABLE_API_KEY, ITERABLE_CAMPAIGN_IDITERABLE_BASE_URL, ITERABLE_SEND_AT, ITERABLE_ALLOW_REPEAT_MARKETING_SENDS
sesAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGIONAWS_SESSION_TOKEN, AWS_SES_CONFIGURATION_SET, AWS_SES_BASE_URL
mailgunMAILGUN_API_KEY, MAILGUN_DOMAINMAILGUN_BASE_URL
mailersendMAILERSEND_API_KEY
brevoBREVO_API_KEY
mailchimpMAILCHIMP_API_KEY
sparkpostSPARKPOST_API_KEY
loopsLOOPS_API_KEY, LOOPS_TRANSACTIONAL_ID
sequenzySEQUENZY_API_KEYSEQUENZY_BASE_URL
plunkPLUNK_API_KEY
mailtrapMAILTRAP_API_KEY
scalewaySCALEWAY_SECRET_KEY, SCALEWAY_PROJECT_IDSCALEWAY_REGION
zeptomailZEPTOMAIL_TOKEN
mailpaceMAILPACE_API_KEY
smtpSMTP_HOSTSMTP_PORT (default 587), SMTP_SECURE, SMTP_REQUIRE_TLS, SMTP_ALLOW_INSECURE_AUTH, SMTP_USER, SMTP_PASS

Adapter auto-detection

When doctor or send runs without --adapter, the CLI walks the table above in order and picks the first adapter whose required environment variables are all set. If none qualifies:

Pass --adapter or set the required environment for one adapter.

Pass --adapter explicitly when more than one provider is configured in the same shell.

doctor

RESEND_API_KEY="re_..." npx email-sdk doctor --adapter resend
# resend looks configured.

Checks that each required variable is set — or supplied via its credential flag. On failure it prints the missing names and exits 1:

Missing environment for resend: RESEND_API_KEY

doctor never calls the provider; it proves your environment is shaped right, not that the key works. Follow up with a dry run and one real send.

send

RESEND_API_KEY="re_..." npx email-sdk send \
  --adapter resend \
  --from "Acme <hello@acme.com>" \
  --to "user@example.com" \
  --subject "Hello" \
  --text "It works"

On success the CLI prints the normalized response as JSON:

{
  "provider": "resend",
  "id": "0f8d2a3e-...",
  "messageId": "0f8d2a3e-...",
  "raw": { "id": "0f8d2a3e-..." }
}

Message flags

FlagValueNotes
--adapter <name>routing name--provider is an alias. Omit to auto-detect.
--from <address>addressSender.
--to <addresses>comma-separatedRecipients.
--subject <text>stringSubject line.
--text <body>stringPlain-text body.
--html <body>stringHTML body.
--cc <addresses>comma-separatedCC recipients.
--bcc <addresses>comma-separatedBCC recipients.
--reply-to <addresses>comma-separatedReply-to addresses.
--header "Name: value"repeatableCustom header.
--tag "name=value"repeatableProvider tag.
--metadata "key=value"repeatableProvider metadata.
--attachment <path[:mime]>repeatableAttach a local file; optional content type after :. --attach is an alias.
--message <path>file pathRead an EmailMessage JSON file; flags override it.
--dry-runbooleanValidate and print the message without sending.

Dry run

npx email-sdk send \
  --adapter resend \
  --from "Acme <hello@acme.com>" \
  --to "user@example.com" \
  --subject "Hello" \
  --text "It works" \
  --dry-run

--dry-run needs no credentials when --adapter is set (without it, auto-detection still reads the environment). It runs message validation, the adapter's field support checks, and adapter-specific limits (single recipient for Loops and Iterable, Cloudflare and Unosend address rules), then prints the send plan:

{
  "ok": true,
  "adapter": "resend",
  "message": {
    "from": "Acme <hello@acme.com>",
    "to": ["user@example.com"],
    "subject": "Hello",
    "text": "It works"
  }
}

Attachment content is redacted in dry-run output — only filename, path, contentType, contentId, and disposition are echoed.

Send from a JSON file

--message reads a complete EmailMessage as JSON; any message flag you also pass overrides that field from the file:

message.json
{
  "from": "Acme <hello@acme.com>",
  "to": ["user@example.com"],
  "subject": "Welcome",
  "html": "<p>Your account is ready.</p>",
  "tags": [{ "name": "type", "value": "welcome" }]
}
npx email-sdk send --adapter resend --message ./message.json --to "qa@example.com" --dry-run

Here --to replaces the file's recipients; everything else comes from the file. Repeatable flags (--header, --tag, --metadata, --attachment) replace the file's whole list when passed at least once.

Credential flags

Every credential environment variable has a flag override; flags win over the environment. Useful for one-off sends without exporting secrets into the shell.

AdapterFlags
resend, sendgrid, unosend, mailersend, brevo, mailchimp, sparkpost, sequenzy, plunk, mailtrap, mailpace, loops--api-key, plus --transactional-id (Loops) and --base-url (Unosend, Sequenzy)
postmark--server-token, --message-stream
cloudflare--api-token, --account-id, --base-url
iterable--api-key, --campaign-id, --base-url, --send-at, --allow-repeat-marketing-sends
ses--access-key-id, --secret-access-key, --region, --session-token, --configuration-set, --base-url
mailgun--api-key, --domain, --base-url
scaleway--secret-key, --project-id, --region
zeptomail--token
smtp--host, --port, --secure, --require-tls, --allow-insecure-auth, --user, --pass

Exit codes

CodeWhen
0Command succeeded — including a passing doctor and a valid --dry-run.
1Anything else: unknown command, missing flag or environment, validation failure, missing doctor credentials, provider error.

Errors print their message to stderr; success output is JSON (or plain text for help, doctor, version, and adapters without --json) on stdout, so the CLI pipes cleanly into jq and scripts.

On this page