# CLI (rly)

`rly` gives you (or your agent) direct access to email from the terminal: send,
receive, search, reply, and manage mailboxes without opening a browser. It ships
as a self-contained native binary — no Node toolchain required once installed —
and talks only to the ReplyLayer API over HTTPS.

This page covers installation, authentication, a tour of the everyday commands,
configuration, and how to verify a signed release. The **machine contract** —
exit codes, `--strict` semantics, and the `--json` output shapes an agent branches
on — lives on [the agent CLI reference](/agents/cli). For the exhaustive
command-and-flag reference, see the CLI guide in the public
[`rly` repository](https://github.com/replylayer/rly).

## Install

### npm (recommended)

```bash
npm i -g rly
```

Requires Node.js 22 or newer to install. The package carries a prebuilt native
binary, so `rly` itself runs without a Node runtime afterward.

### PyPI (pipx)

```bash
pipx install rly
rly --help
```

The `rly` PyPI package installs a bundled native launcher — no Node toolchain
needed. Installing it exposes **both** the `rly` and `replylayer` commands (same
launcher, two names); examples here use the short form. On Debian/Ubuntu systems
that enforce PEP 668, `pip install rly` may fail with
`externally-managed-environment` — use `pipx install rly`, or run
`pip install rly` inside a virtual environment.

### pnpm

```bash
pnpm add -g rly
```

### Supported platforms

The prebuilt native binary targets:

| OS | Architectures | Minimum |
|---|---|---|
| Linux | x86_64, aarch64 | glibc 2.28+ |
| macOS | arm64, x86_64 | macOS 14.0+ |
| Windows | x64 | — |

The bundled binary is **glibc-linked**, so Alpine / musl base images are not
supported. On a musl host, install via npm on host Node 22+ instead.

### From source

The CLI source is public at <https://github.com/replylayer/rly>. Building from
source needs Node.js 22+ on your `PATH`:

```bash
git clone https://github.com/replylayer/rly.git
cd rly
npm install
npm run build
npm link
```

If you can't run a recent Node toolchain, prefer the bundled binary
(`npm i -g rly` or `pipx install rly`) — it carries its own runtime.

## Authenticate

`rly` resolves an API key from three sources, highest precedence first:

| Source | How | Notes |
|---|---|---|
| `--api-key <key>` flag | Highest | **Avoid in shared shells / CI** — the key lands in process arguments and shell history. |
| `REPLYLAYER_API_KEY` env var | Middle | Recommended for CI/automation. Never persisted to disk. |
| `~/.replylayer/credentials` file | Lowest | Written by `rly auth login`; mode `0600`, plaintext. |

```bash
# Interactive — prompts for the key and stores it (mode 0600)
rly auth login

# Environment variable — nothing is written to disk
export REPLYLAYER_API_KEY=rly_live_...

# Confirm which source is in effect (the key value is never printed)
rly auth status
rly config show
```

Keys are formatted `rly_live_<public-id>.<secret>` (older `rl_live_` keys still
work). Admin keys have full account access; agent keys are scoped to specific
mailboxes. For the full key model — admin vs. agent scoping, session-vs-Bearer,
and the create/verify/revoke rotation pattern — see
[Authentication](/docs/authentication).

A brand-new account starts with `rly signup --phone +<country-code>...` and two
one-time checks: email and SMS. Complete them with `rly auth verify --code ...`
and `rly auth verify-phone --code ...`. If the SMS does not arrive, use
`rly auth resend-phone`; before verification, `--phone` on that command corrects
a typo and sends a new challenge. The [Quickstart](/docs/quickstart) walks the
full first-send path.

## Command tour

Ten commands cover most day-to-day use. Each accepts `--json` for machine output,
and `-h` / `--help` prints the flags for any command.

```bash
# 1. Create a mailbox (your sending/receiving identity)
rly mailbox create support-bot

# 2. Mint a scoped agent key bound to that mailbox
rly api-key create --role agent --label support-bot-key --mailbox support-bot

# 3. Add a recipient (sandbox accounts can only send to confirmed recipients —
#    this validates the address, then emails a confirmation link)
rly recipients add customer@example.com

# 4. Send an email (synchronous — the response carries the scan verdict)
rly send --from support-bot --to customer@example.com \
  --subject "Hello" --body "Your order has shipped."

# 5. List messages in a mailbox
rly inbox list --mailbox support-bot

# 6. Read a message (non-mutating — does NOT mark it read)
rly inbox read <message-id>

# 7. Reply to a message
rly reply <message-id> --body "We're looking into this."

# 8. Wait for the next arrival (long-poll; pass --since to skip the backlog)
rly inbox wait --mailbox support-bot --timeout 60

# 9. Draft, review, then send (the scanner runs at create and again at send)
rly draft create --mailbox support-bot --to customer@example.com \
  --subject "Update" --body "Your ticket is resolved."
rly draft send <draft-id>

# 10. Preflight your send budget (works with an agent-scoped key)
rly account quota
```

Notes on the tour:

- **`inbox read` never advances read state** — an agent inspecting a message
  won't silently mark it read. Advance read state explicitly with
  `rly inbox mark-read <message-id>`.
- **`send` and `reply` exit `0` even when the scanner blocks or holds a message**
  — read the JSON `status` for the outcome, or pass `--strict` to map a
  non-delivered outcome to a distinct non-zero exit. The message lifecycle and
  verdict vocabulary live at [Messages](/agents/messages); the exit-code and
  `--strict` contract lives at [the agent CLI reference](/agents/cli).
- **`account quota`** reports your effective daily cap, sends remaining, and reset
  time; the tier and sandbox limits behind those numbers are documented under
  [Limits](/docs/limits).

Beyond the tour, the CLI also manages the recipient allowlist and inbound firewall
([recipient allowlist](/docs/guides/recipient-allowlist),
[inbound firewall](/docs/guides/inbound-firewall)), the do-not-contact list
([suppressions](/docs/guides/suppressions)), scheduled sends
([scheduled send](/docs/guides/scheduled-send)), attachment previews
([attachments](/agents/attachments)), custom domains, webhooks, and legal holds.
The [`rly` repository guide](https://github.com/replylayer/rly) is the exhaustive
reference.

## Simulator

Outbound simulation uses the normal `send` command, so it works in every CLI version.
The dedicated inbound command requires an installed build that includes simulator
client support; the [email simulator guide](/docs/guides/simulator) carries the
current availability note and the complete outcome contract.

One Sandbox account can run the four exact outbound scenarios in the same 24-hour
period. They bypass only destination-concentration controls and still consume normal
daily/cumulative usage allowance.

```bash
rly send --from <mailbox> --to delivered+ci-run-42@simulator.replylayer.net \
  --subject "simulator check" --body "exercise delivery" --json

rly simulate inbound --mailbox <mailbox> --scenario clean --label ci-run-42 --json
rly simulate inbound --mailbox <mailbox> --scenario prompt_injection_quarantined --json

rly webhook test <webhook-id> --event message.delivered
```

The inbound command prints `available`, `quarantined`, or `pending`. Branch on that
returned status. A `pending` response means processing continues after the command
returns; use the label to identify the eventual message in `rly inbox list`.

## Global options

| Flag | Description |
|---|---|
| `-V, --version` | Print the version number. |
| `--api-url <url>` | API base URL. Defaults to `https://api.replylayer.ai` (env: `REPLYLAYER_API_URL`). |
| `--api-key <key>` | API key, overriding the stored credential (env: `REPLYLAYER_API_KEY`). |
| `--json` | Emit JSON instead of formatted tables. |
| `-h, --help` | Show help for any command. |

`--json` makes every command machine-readable — output goes to stdout, errors are
a single JSON object on stderr with a stable `code`:

```bash
rly --json inbox list --mailbox support-bot | jq '.messages[0].subject'
```

The full `--json` output shapes, the stable error `code` field, and the
exit-code semantics are the [agent CLI contract](/agents/cli); the error codes
themselves are catalogued at [Errors](/agents/errors).

## Configuration & environment

| Variable | Description |
|---|---|
| `REPLYLAYER_API_KEY` | API key (alternative to `rly auth login`). |
| `REPLYLAYER_API_URL` | API base URL. **Override-only** — defaults to production, so customers on production set nothing. |
| `REPLYLAYER_MAILBOX` | Default mailbox for a fresh `send --from` / `inbox list` / `inbox wait` / `draft create`. An explicit flag always wins. |

`REPLYLAYER_API_URL` (or `--api-url`) is only needed to target a **non-production**
API — a staging account, a self-hosted deployment, or a local server. Point it at
the host your operator gave you, and prefer a stable hostname. A `401` on the
first authenticated call after switching keys usually means the URL is still
pointed at the wrong environment.

Two commands inspect the effective setup with **no auth and no network call**:

```bash
rly config show   # resolved API URL, credential source, proxy env, config dir
rly doctor        # runs environment + connectivity checks (rly doctor --offline skips network)
```

`config show` reports the credential *source* only — never the key value. `doctor`
surfaces conditions worth knowing about, such as a loosened-permission credential
file or a custom CA in your trust store (below).

## Verify a signed release

Every stable release publishes a GPG-signed checksum manifest
(`SHA256SUMS` + `SHA256SUMS.sig`), per-platform SPDX SBOMs, and SLSA build
provenance on the public [`rly` repository](https://github.com/replylayer/rly)
Releases page. macOS binaries are additionally Apple-notarized. The bundled native
binaries are delivered inside the npm and PyPI packages; the published manifest
lists each one by SHA-256 so you can verify what you installed.

The public signing key and the step-by-step "Verifying a release" walkthrough live
in that repo — the key is distributed via its `KEYS.txt` file, **not** public
keyservers (so `gpg --recv-keys` won't find it):

```bash
# Import the signing key from the rly repo (NOT a keyserver)
curl -fsSL https://raw.githubusercontent.com/replylayer/rly/main/KEYS.txt | gpg --import

# Verify the manifest signature
gpg --verify SHA256SUMS.sig SHA256SUMS

# Check your platform's asset against the manifest (Linux; use `shasum -a 256 -c` on macOS)
sha256sum -c <(grep linux-x64 SHA256SUMS)
```

A `Good signature` line followed by a "not certified with a trusted signature"
warning is **expected and benign** — the signature verified; you simply haven't
marked the key as locally trusted.

The per-platform SBOM is an SPDX-2.3 document describing the final binary by its
SHA-256, listing the bundled runtime and dependencies. It is produced from the
actual build artifact, not a lockfile scan, and folded into the signed manifest.

## Security & network behavior

- **The stored credential is a plaintext file** (`~/.replylayer/credentials`,
  owner-only `0600`). Treat it as a secret on disk; `rly doctor` warns when the
  file exists or its permissions have loosened.
- **The key is never printed.** `config show`, `doctor`, and `auth status` report
  only the resolution *source*. Proxy URLs that embed credentials are redacted in
  all diagnostic output.
- **No telemetry, no silent auto-update.** The CLI makes no outbound connections
  other than HTTPS to the API base — no analytics, no update checks, no
  third-party calls. Installed binaries never update themselves; pull a new
  release explicitly.
- **Custom CA / TLS interception.** The CLI honors `NODE_EXTRA_CA_CERTS`. Adding a
  CA lets whoever holds the matching key terminate and inspect TLS to the API —
  legitimate behind a corporate proxy, but a deliberate man-in-the-middle. Point
  it only at a CA bundle you trust; `rly doctor` reports when it is set.
- **HTTP(S) proxies.** `rly doctor` detects and reports `HTTPS_PROXY` /
  `HTTP_PROXY` / `NO_PROXY` (credentials redacted), but the CLI does not currently
  route requests through them — behind a mandatory proxy, requests go direct and
  `doctor` flags this so the cause is visible.

## Where to go next

- [Quickstart](/docs/quickstart) — first account to first send.
- [Authentication](/docs/authentication) — key model, scoping, rotation.
- [Agent CLI reference](/agents/cli) — exit codes, `--strict`, `--json` shapes.
- [SDKs](/docs/sdks) and [MCP](/docs/mcp) — the other client surfaces.
