The official command line interface for Reloop — open-source email infrastructure you can self-host or use as a hosted service.
Official npm package: reloop-cli. Binary: reloop.
reloop login
reloop contacts create --email luna@example.com --first-name Luna --last-name Gurnee
reloop contacts list --json | jq -r '.contacts[].email'These are two different products and they do not overlap.
reloop-cli (this repo) |
reloop-tools |
|
|---|---|---|
| Binary | reloop |
reloop-tools |
| Talks to | Your Reloop account — Cloud or self-hosted | Public Reloop Tools endpoints |
| Needs an API key | Yes | No |
| Purpose | Operate the platform: contacts, and in time every resource the dashboard exposes | Standalone developer utilities, e.g. disposable-address checks |
| Example | reloop contacts list |
reloop-tools check user@example.com |
Install both if you want both. They never conflict — different package names, different binaries, different jobs.
npm install -g reloop-cli
reloop --helpOr run it without installing:
npx reloop-cli contacts listRequires Node.js 20 or newer.
Create an API key in your Reloop dashboard under API keys, then:
reloop loginThe key is verified against the endpoint before anything is written to disk, and it is stored with 0600 permissions. Nothing ever prints your key back to you — output is masked to a short prefix.
Every command resolves its credential in this order, and stops at the first hit:
--api-key <key>RELOOP_API_KEY- the selected profile in the config file
The endpoint is resolved the same way:
--endpoint <url>RELOOP_ENDPOINT- the selected profile's endpoint
https://reloop.sh
reloop auth status shows exactly what a command would use, and where each value came from:
$ reloop auth status
Profile production
Endpoint https://mail.example.com (profile)
API key rl_prod_abcd… (profile)
Key name deploy-bot
Created by luna@example.comSelf-hosting is first class. Nothing in the CLI is hardwired to reloop.sh — the hosted URL is only the default when no profile or flag says otherwise.
Point a profile at your own install once:
reloop login --profile production --endpoint https://mail.example.com
reloop login --profile local --endpoint https://local.reloop.shThen pick a target per command, or set a default:
reloop contacts list --profile local
reloop auth use production
reloop auth listA profile holds a name, an endpoint, and a credential. The organization and team context come from the API key itself, so there is nothing else to configure.
For a one-off call against a host you do not want to store:
RELOOP_ENDPOINT=https://mail.example.com RELOOP_API_KEY=rl_prod_... reloop contacts listIn a terminal, commands print a readable table or detail view on stdout, with progress and hints on stderr.
$ reloop contacts list
EMAIL NAME STATUS CREATED
luna@example.com Luna Gurnee subscribed Sep 9, 2026
alex@example.com Alex Example subscribed Sep 8, 2026Everywhere else — piped, redirected, in CI — commands emit JSON automatically. You do not need --json for scripts to work, though passing it is always safe.
$ reloop contacts list --limit 1 --json
{
"object": "contact",
"contacts": [ { "object": "contact", "id": "con_...", "email": "luna@example.com", ... } ],
"total": 2,
"page": 1,
"limit": 1,
"totalContacts": 2,
"subscribedContacts": 2,
"unsubscribedContacts": 0,
"event": "contact.list"
}The JSON is the Reloop API response, unwrapped and undecorated, so field names stay stable and match the platform docs and SDKs.
Rules the CLI holds to:
- stdout carries only the requested data
- diagnostics, hints, spinners, and prompts go to stderr
- prompts never appear when stdin or stdout is not a terminal
--quietimplies--jsonand silences stderr hintsNO_COLORandTERM=dumbdisable colour
Failures print a machine-readable envelope on stderr:
$ reloop contacts get nobody@example.com --json
{
"error": {
"code": "contact_not_found",
"message": "No contact exists with email nobody@example.com.",
"exitCode": 4,
"fix": "Run `reloop contacts list --search <term>` to find the contact."
}
}In a terminal the same failure reads:
Error: No contact exists with email nobody@example.com.
Run `reloop contacts list --search <term>` to find the contact.
| Exit code | Meaning |
|---|---|
0 |
Success |
1 |
Unexpected failure, or a 5xx from the API |
2 |
Usage error — unknown flag, missing argument, conflicting options, confirmation required |
3 |
Authentication — no credential, or the API rejected it |
4 |
Not found |
5 |
Conflict, e.g. the contact already exists |
6 |
The API rejected the request body |
7 |
Rate limited (retry-after is surfaced in error.details) |
8 |
Network failure or timeout |
130 |
Cancelled |
Every command documents the error codes it can produce under Error codes in its --help.
--debug prints one line per request to stderr — method, URL, status, duration, and rate-limit headers. It never prints your API key.
A contact can be addressed by its ID or by its email address. Anything containing @ is treated as an email and resolved with an exact, case-insensitive match; anything else is used as an ID directly.
reloop contacts list
reloop contacts list --limit 50 --status subscribed
reloop contacts list --search luna --json
reloop contacts list --channel chn_123
reloop contacts list --all --json > contacts.json--all walks every page and is the supported way to export. Without it, the CLI fetches one page — it never downloads the whole database to render ten rows. --all cannot be combined with --page or --limit.
reloop contacts create --email luna@example.com --first-name Luna --last-name Gurnee
reloop contacts create \
--email luna@example.com \
--property company=LunarLabs \
--property role=developer
reloop contacts create --email luna@example.com --properties '{"seats":5}' --group-id grp_123Run reloop contacts create with no --email in a terminal and it walks you through the fields:
Create contact
? Email: luna@example.com
? First name (optional): Luna
? Last name (optional): Gurnee
? Add properties? (y/N): y
? Property: company
? Value: LunarLabs
? Add another property? (y/N): n
✓ Contact created: luna@example.com
Pass --email and the command runs exactly what you asked for with no prompts, so a terminal and a CI job behave identically.
--property key=value always sends a string. Use --properties with a JSON object when a property must be a number.
reloop contacts get luna@example.com
reloop contacts get con_123456789 --jsonreloop contacts update luna@example.com --status unsubscribed
reloop contacts update luna@example.com --property plan=pro
reloop contacts update luna@example.com --unset-property plan
reloop contacts update con_123 --properties '{"plan":"pro"}'Only the fields you pass are changed.
Property semantics matter here, because the Reloop API replaces the whole property set on every update:
--propertyand--unset-propertymerge — the CLI reads the contact's current properties, applies your change, and sends the full set back. Everything you did not mention survives.--propertiesreplaces — this is the raw API behaviour. Any property missing from the JSON object is removed.
The two forms cannot be combined.
Property names must be lowercase letters, numbers, and underscores — the same rule the Reloop server enforces when it stores them.
reloop contacts delete luna@example.com
reloop contacts delete con_123456789 --yes --jsonIn a terminal you get a confirmation prompt. In a script --yes is required — without it the command exits 2 with confirmation_required rather than hanging or deleting silently. Declining the prompt exits 130 and sends no request.
reloop commandsprints the entire command tree as JSON — every subcommand, alias, argument, flag, default, and error code. It always emits JSON, in a terminal or not.- Argument parsing is deterministic: unknown flags fail with exit
2and a suggestion rather than being ignored. - No command ever prompts when stdin or stdout is not a terminal.
- Field names in JSON output come straight from the Reloop API, so they match the platform docs and the official SDKs.
reloop commands | jq -r '.commands[] | .path'
reloop commands | jq '.commands[] | select(.path=="reloop contacts") | .commands[].flags'| Variable | Effect |
|---|---|
RELOOP_API_KEY |
API key, used when --api-key is absent |
RELOOP_ENDPOINT |
Base URL, used when --endpoint is absent |
RELOOP_PROFILE |
Profile to use when --profile is absent |
RELOOP_CONFIG_DIR |
Override the config directory (useful in CI and tests) |
RELOOP_NO_PROMPT |
Never prompt, even on a terminal |
NO_COLOR |
Disable colour |
CI, GITHUB_ACTIONS, TERM=dumb |
Treated as non-interactive |
| Platform | Path |
|---|---|
| macOS / Linux | $XDG_CONFIG_HOME/reloop/config.json, else ~/.config/reloop/config.json |
| Windows | %APPDATA%\reloop\config.json |
Written atomically with 0600 permissions inside a 0700 directory.
{
"version": 1,
"activeProfile": "production",
"profiles": {
"production": { "endpoint": "https://mail.example.com", "apiKey": "rl_prod_..." },
"local": { "endpoint": "https://local.reloop.sh", "apiKey": "rl_prod_..." }
}
}reloop logout removes the stored credential and keeps the endpoint, so the target stays configured.
# every subscribed address, one per line
reloop contacts list --all --status subscribed --json | jq -r '.contacts[].email'
# create a contact and capture its ID
id=$(reloop contacts create --email luna@example.com --json | jq -r '.id')
# unsubscribe everyone in a CSV of addresses
while read -r email; do
reloop contacts update "$email" --status unsubscribed --json > /dev/null
done < emails.txt
# branch on the exit code
if reloop contacts get luna@example.com --json > /dev/null 2>&1; then
echo "exists"
else
case $? in
4) echo "not found" ;;
3) echo "check your credentials" ;;
*) echo "something else went wrong" ;;
esac
fiIn CI, set RELOOP_API_KEY (and RELOOP_ENDPOINT if you self-host) as secrets and skip reloop login entirely.
Requires Bun and Node.js 20+.
bun install
bun test
bun run typecheck
bun run lint
bun run build
bun run dev contacts --helpbun run format applies Biome's fixes. The project has zero runtime dependencies — everything the CLI needs at runtime is in src/.
See ARCHITECTURE.md for the module layout and how to add a new resource, and CONTRIBUTING.md for the contribution workflow.
- Reloop — the platform
- Documentation
- Reloop Tools CLI
- Discord
Apache-2.0