MepMail Docs

CLI

@millionsend/cli — move an email account to MepMail from your terminal: plan, apply, status, rollback.

@millionsend/cli moves an email account to MepMail — Cloud or your own instance. It reads the source provider, diffs it against the target, applies the difference and writes a report. Only Resend is a source today.

Install

Node 18 or newer, no dependencies. Run it without installing:

npx @millionsend/cli migrate --from resend

Or install it once:

npm install -g @millionsend/cli
millionsend --version

Commands

millionsend migrate --from resend                          # connect, choose resources, plan, confirm, apply, summary
millionsend migrate plan --from resend [--out plan.json]   # read-only; exit 0 nothing to do, 2 changes, 1 error
millionsend migrate apply [plan.json] [--yes]              # apply a saved plan, or plan and apply in one go
millionsend migrate status                                 # what the last run created and what is left
millionsend migrate rollback [--yes]                       # delete only what this tool created
millionsend --help | --version
  • migrate is the interactive path: it asks for what it is missing (keys, target URL), lets you pick resources with a checkbox list (all checked by default except sent broadcasts), shows the plan, asks for confirmation, applies it and prints the summary.
  • migrate plan reads both sides and prints what would change without writing anything to the target. --out plan.json saves it. Before any write, the plan checks the target's GET /usage — plan, limits, cloud flag — and says precisely what does not fit ("7 domains to create; the Free plan allows 3"), plus an estimate: "~2,140 requests · about 4 min at 8 req/s".
  • migrate apply applies a saved plan, or plans and applies in one go. Conflicts are resolved the same way on every run: contacts are upserted by email; topics, segments, properties, webhooks, templates and domains are matched by name, key, endpoint or alias and updated when their fields differ, left unchanged when they match.
  • migrate status prints what the last run created and what is left on the checklist. Needs no credentials.
  • migrate rollback deletes only the ids the tool created — never rows it merely updated — in reverse dependency order, after printing the list and asking for confirmation (--yes skips it). Deleting contacts is one request per contact; the prompt shows the time estimate.

Flags

FlagMeaning
--from <provider>Source provider. Only resend exists.
--from-key-stdinRead the source API key from stdin (first line).
--from-key <key>Source API key as an argument. Visible in process lists; the tool warns. Prefer the env var.
--to-url <url>API URL of a self-hosted MepMail instance. Unset, the target is MepMail Cloud (https://api-mepmail.je4ndev.com), like the SDKs.
--to-key-stdinRead the MepMail API key from stdin (second line when both stdin flags are set).
--to-key <key>MepMail API key as an argument. Same caveat.
--rps <n>Requests per second against the source; default 8. Resend's team limit is 10, shared with your production sending; the CLI prints the limit it detects on connect, paces under it, and warns when the rate is above it. Values above 10 (up to 100) are for a limit Resend raised on request.
--only <a,b>Migrate only these resources.
--skip <a,b>Skip these resources. enrichment is the per-contact pass that runs last: topic subscriptions, then properties, each resumable.
--on-conflict <mode>Contacts that already exist on the target: upsert (default), skip, error.
--include-sentImport sent broadcasts as drafts. Skipped by default.
--fresh-webhook-secretsMint new webhook signing secrets instead of copying them. Shown once, in the report.
--freshForget the resume progress in .millionsend/migrate-state.json and read everything again. The ids earlier runs created are kept, so rollback still works.
--out <file>migrate plan: write the plan as JSON.
--report <file>Also write the Markdown report to this path.
-y, --yesSkip confirmations.
--non-interactiveNever prompt; a missing input is exit 1. Automatic when stdin is not a terminal, and with --json.
--jsonJSON on stdout, progress on stderr.
-v, --verboseLog every request: GET /contacts?limit=100 → 200 (143 ms).
--color <mode>auto (default: colors on a terminal, none when piped or NO_COLOR is set), always, never.
--no-colorSame as --color never.
-h, --help / -V, --versionHelp text / version.

Resource names for --only and --skip, in apply order: domains, properties, topics, segments, contacts, broadcasts, templates, webhooks, suppressions, enrichment, api-keys.

Environment

VariableMeaning
RESEND_API_KEYSource API key, full access. The tool only ever reads from Resend.
MILLIONSEND_API_KEYMepMail API key, full access.
MILLIONSEND_BASE_URLAPI URL of a self-hosted instance, same as --to-url. Unset means MepMail Cloud.
NO_COLORDisable colors.
FORCE_COLORColors even when piped, same as --color always.
DO_NOT_TRACKHonored, as a no-op: the tool sends no telemetry, never phones home and never checks for updates.

Each key is resolved in this order: environment variable, then the -stdin flag, then the argument flag, then — in a terminal — a masked prompt. The target URL comes from MILLIONSEND_BASE_URL or --to-url; with neither set, a terminal offers a choice between MepMail Cloud and a self-hosted URL, and a non-interactive run targets MepMail Cloud, like the SDKs.

Files

Written next to where you run the tool, mode 0600, never containing a key:

FileContents
.millionsend/migrate-state.jsonEvery id the tool created per resource, resume cursors, the plan hash. Written after every batch, so an interrupted run resumes where it stopped.
.millionsend/migrate-report.jsonThe last run's report as data.
.millionsend/migrate-report.mdThe same report as Markdown: counts, the checklist, the DNS records per domain, the id map (source topic and segment ids → MepMail ids, for code that references them), the manual items.

.millionsend/ is appended to .gitignore when one exists in the current directory; the tool says so once.

Security model

  • Read-only against the source. Every request to Resend is a GET to a documented endpoint, sent with the User-Agent millionsend-cli/<version>. Writes go to your MepMail API only.
  • Keys stay in memory. They are never written to any file and are redacted from every log line (re_…, ms_…, whsec_… and Authorization headers).
  • Two hosts, no third party. The tool contacts api.resend.com and the MepMail API URL you named. No telemetry, no update check.
  • 401 or 403 from either side stops the run. No retry, no workaround.
  • Rate limits are respected. 429s wait for retry-after; 5xx and network errors back off exponentially, 5 attempts. Every retry is logged.
  • Opt-outs are preserved. unsubscribed and topic opt-outs are carried over as they are; the tool never re-subscribes anyone. Suppressions keep their origin (bounce, complaint, manual).

Exit codes

CodeMeaning
0Success — or, for migrate plan, nothing to do.
1Error: bad arguments, missing input, rejected key, unrecoverable failure.
2migrate plan only: the plan has changes.
3Partial: some items failed. Details in the state file and the report.

Non-interactive and CI

When stdin is not a terminal — or with --non-interactive or --json — the tool never prompts: a missing input exits 1 with the env var or flag to set. Pass keys through the environment or stdin, never as arguments:

export RESEND_API_KEY=re_...
export MILLIONSEND_API_KEY=ms_...
export MILLIONSEND_BASE_URL=https://api-mepmail.je4ndev.com   # or your instance's URL

millionsend migrate plan --from resend --out plan.json
# exit 2 when there is something to apply
millionsend migrate apply plan.json --yes

Or on stdin, first line source, second line target:

printf '%s\n%s\n' "$RESEND_KEY" "$MS_KEY" | millionsend migrate plan --from resend --from-key-stdin --to-key-stdin --to-url https://api.your-instance

Progress is printed one line per step (✓, ✗, ⟳ with n/N counters), appended when piped, rewritten in place in a terminal.

--json

With --json, stdout carries only JSON — the plan for migrate plan, the report for migrate apply — and progress goes to stderr, so the output can be piped into jq or saved as an artifact. --json implies --non-interactive.

millionsend migrate plan --from resend --json | jq '.counts'

Resend is a registered trademark of Plus Five Five, Inc. MepMail is not affiliated with or endorsed by Resend.

On this page