CLI
The Reply command line: sign in once, then every v3 API request runs as you, from your terminal or your scripts. Install with npm i -g reply-cli. It handles authentication, profiles and teams, runs sequence, contact, contact-list, inbox, account, schedule and direct-send commands directly, reaches the whole v3 API through reply api, installs the Reply agent skills into your AI assistants, and keeps itself up to date.
Status: Available (v0.17.2)
reply ships five things today:
- Authentication and identity – OAuth or API-key login, multi-account profiles, and team selection, so one machine can act as several users across several workspaces.
-
Dedicated work commands –
reply sequence,reply contact,reply contact-list,reply inbox,reply account,reply scheduleandreply direct-sendfind and run sequences, manage contacts, send email and LinkedIn touches outside a sequence, triage the inbox, list connected accounts, list sending schedules, and read or cancel a scheduled send, each with JSON output and its own error codes. See Work commands. -
reply api– an authenticated passthrough that reaches every documented endpoint of the Reply.io v3 API, for anything the dedicated commands don't cover yet. -
reply skills– installs the Reply agent skills into every AI assistant on the machine, dependencies resolved. -
reply install– updates the CLI itself, so nobody has to remember an npm command.
Account-wide reporting beyond reply sequence stats has no dedicated command yet. See Where the command surface is going for why that is a deliberate wait rather than a backlog. reply api /v3/... reaches it today. The CLI is pre-1.0. See Versioning & stability.
Source: github.com/reply-team/reply-cli · Package: npmjs.com/package/reply-cli · Docs: docs.reply.io/cli/overview.
Install
npm install -g reply-cli # installs the `reply` command (Node 20+)
reply --version
Staying current
One command, and you never type an npm invocation again:
reply install # update to the newest release, or say you are already on it
reply install --dry-run # check without changing anything, usable as a CI gate
It looks up the newest release and updates the copy it can safely update. Where the copy is not the CLI’s to change (installed inside a project, run through npx, built from a checkout) it prints the command that fits your setup and touches nothing. reply --version mentions a newer release when one exists; that check reads the public GitHub releases, is cached for a day, times out after a second and a half, and stays silent when it fails. It never runs with --json, when output is piped, in CI, or with REPLY_NO_UPDATE_CHECK=1 set.
Authentication
Sign in once; every request then runs as you.
reply auth login # browser OAuth
reply auth login --with-token # store an API key, read from stdin (never hits shell history)
reply auth status # who you're signed in as, and how – no secrets shown
reply auth whoami # verify the stored credential against the API
reply auth logout # remove the stored credential
Create an API key in Reply → Settings → API Keys. For a single command, pass --api-key or set REPLY_API_KEY; both take precedence over a stored login and are never written to disk. Resolution order: --api-key → REPLY_API_KEY → stored login.
Command map
Run reply <command> --help for flags and examples on any command: the installed help always matches your version; add --json for machine-readable output. The canonical command reference, including every global flag, is docs.reply.io/cli/overview.
| Command | What it does |
|---|---|
reply sequence list|create|get|contacts|stats|add-contact|remove-contact|pause|start | Find sequences by name, create a draft, read one, page its contacts, pull its stats, add or remove a contact, or pause/start it |
reply contact find|get|update|delete|opt-out|send-email|linkedin-connect|linkedin-message|linkedin-inmail|linkedin-voice | Look a contact up by email or LinkedIn URL, read or edit it, delete or opt it out, or reach it with an email or LinkedIn touch outside any sequence |
reply contact-list add-contact <list-id> | Put a contact on a contact list; a contact given by email is imported, so it needs a first name |
reply inbox list|changes|get|reply|categories|categorize|mark-read|mark-unread | Page inbox threads, pull the change feed by cursor, read one, reply on its own channel, and file or mark threads |
reply account list | The email and LinkedIn accounts a send can go from, and each one's state |
reply schedule list | Every sending schedule the account can use, with its time zone and its sending windows by weekday |
reply direct-send get|cancel <reference> | Read or cancel a send scheduled with --send-after, before it goes out |
reply api <path> | Authenticated passthrough to any v3 endpoint: the agent/CI escape hatch for everything not wrapped above |
reply auth login|status|whoami|logout | Sign in (OAuth or --with-token), inspect, verify, or clear the credential |
reply profile add|use|list|current|show|set|rename|unset|delete | Keep multiple accounts signed in; each profile stores its own credential |
reply team list|current|use|clear | See the teams you can act in and pin one (sent as X-TEAM-ID) |
reply skills install|list|update|remove | Put the Reply agent skills into the AI assistants on this machine, or take them out |
reply install | Update the CLI itself (reply update is the same command) |
Raw API access
reply api is a raw, authenticated passthrough to any v3 endpoint. Use the path exactly as it appears in the API reference (starting with /v3); the query string goes in the path. A --body switches the method to POST (it also accepts @file or - for stdin).
reply api /v3/whoami # your identity + team
reply api /v3/sequences # list sequences
reply api /v3/contacts --pretty # list contacts, indented
reply api /v3/sequences/12345 # one sequence by id
reply api /v3/contacts --body @contact.json # create a contact (POST; body schema per the docs)
echo '<json>' | reply api /v3/contacts --body - # body from stdin
reply api /v3/whoami --verbose # full request/response on stderr
It prints { "code": <status>, "data": <body> } and exits non-zero on HTTP >= 400. --verbose adds a full request/response trace on stderr with credentials redacted; stdout stays plain JSON, so pipes keep working.
Work commands
Seven commands reach Reply's core objects directly instead of through reply api. Each prints JSON and shares one error/exit-code contract: on error (with --json), one line on stderr, {"error":{"code":…,"ids":…}}; exit 0 done, 1 failed with nothing (or only the partial effects in ids) applied, 2 usage error (nothing sent), 3 outcome unknown (a write may have gone through – the command's own --help says whether re-running it is safe). None of them take --dry-run or prompt for confirmation; a write runs as issued. Full flags: reply <command> <subcommand> --help.
A command that names a contact by flag takes it one way: --contact-id <id> (a contact Reply already holds), --email <address> (with --first-name, --last-name, --company, --title), or --contact <json>|@file|-. contact find reads its contact from stdin only, and the LinkedIn sends need the contact's LinkedIn URL: an email alone is refused. A LinkedIn URL is matched by its lowercased slug.
What happens to a person Reply does not hold yet depends on the command. sequence add-contact and contact-list add-contact import a contact given by email every time, so they need a first name even for one Reply already holds; contact update and contact opt-out import one only when Reply holds nobody under it; contact find and sequence remove-contact never create one. A send given a person's details reaches only a contact it creates: when Reply already holds that person, nothing is sent and the answer is contact.already_held with the contact's id, so read it and send with --contact-id.
Any send (contact send-email or a contact linkedin-* command) accepts --send-after <moment> to hold it for later instead of sending at once, --schedule-id <id> to time it by a saved schedule (see Schedules), and --idempotency-key <key> to make retrying it safe. A held send answers with a reference (email-<n> or linkedin-<n>); read or stop it with reply direct-send (see Direct sends).
Sequences
reply sequence finds sequences by name, reads one, pages its contacts, pulls its stats, moves contacts in or out, and pauses or starts it.
echo '{"name":"Q4 outreach"}' | reply sequence list --name - # every sequence of this name, archived included
echo '{"name":"Q4 outreach"}' | reply sequence create --name - # a draft holding nobody, with one task step to replace
reply sequence get 123 # settings, steps, state
reply sequence contacts 123 --limit 50 # one page of its contacts
reply sequence stats 123 --preset last-month # email + LinkedIn counts over a window
reply sequence add-contact 123 --email ann@example.com --first-name Ann
reply sequence remove-contact 123 --contact-id 456 # history stays; never creates a contact
reply sequence start 123 --expect-contacts 40 # or resume; refuses if the count doesn't match
reply sequence pause 123
Contacts
reply contact looks a person up, reads or edits them, and can reach them directly outside a sequence.
echo '{"email":"ann@example.com"}' | reply contact find --contact - # id only; never creates a contact
reply contact get 456
echo '{"title":"CTO"}' | reply contact update 456 --fields -
reply contact delete 456
reply contact opt-out --contact-id 456
echo '{"subject":"Quick question","body":"<p>Hi Ann</p>"}' | reply contact send-email --contact-id 456 --email-account-id 12 --message -
reply contact linkedin-connect --contact-id 456 --linkedin-account-id 34
echo '{"body":"Thanks for connecting, Ann."}' | reply contact linkedin-message --contact-id 456 --linkedin-account-id 34 --message -
echo '{"subject":"Quick question","body":"Hi Ann"}' | reply contact linkedin-inmail --contact-id 456 --linkedin-account-id 34 --message -
echo '{"script":"Hi Ann, a quick hello about Tuesday."}' | reply contact linkedin-voice --contact-id 456 --linkedin-account-id 34 --message -
linkedin-inmail needs Sales Navigator and InMail credits; linkedin-message and linkedin-voice need an existing connection; linkedin-connect does not. Pick the sending account with reply account list.
Contact lists
reply contact-list add-contact <list-id> puts a contact on a list. A contact given by email is imported every time, so it needs a first name; one given only by a LinkedIn URL is looked up first. --refuse-opted-out declines one Reply holds as opted out, which a list may otherwise hold.
Inbox
reply inbox pages threads, reads one, replies on its own channel, and files or marks threads.
reply inbox list --sequence-id 123 --source unread
reply inbox changes --since 2026-11-01T00:00:00Z # change feed, read forward by cursor
reply inbox get 9876
echo '{"body":"Thanks, Tuesday works."}' | reply inbox reply 9876 --message -
reply inbox categories # the categories a thread can be filed under
reply inbox categorize 9876 --category-id 7
reply inbox mark-read 9876
reply inbox mark-unread 9876
A reply goes to the thread's own contact on the thread's own channel; no other recipient can be named.
Accounts
reply account list [--channel email|linkedin] lists the email and LinkedIn accounts a send can go from, and each one's state; it is where a --email-account-id or --linkedin-account-id above comes from.
Schedules
reply schedule list lists every sending schedule the account can use, with its time zone and its sending windows by weekday.
reply schedule list
Direct sends
reply direct-send get|cancel <reference> reads or stops a send held for later with --send-after on contact send-email or a contact linkedin-* command, before it goes out.
reply direct-send get email-1001 # whether it has gone out, when, and why if it failed
reply direct-send cancel email-1001 # stop it; already cancelled or already sent is safe to repeat
Installing the agent skills
The Reply agent skills are the reasoning layer; they teach an agent how outbound actually works and how to compose the API calls this CLI executes. The CLI installs them: it detects the AI assistants on the machine and puts the packs into each one, resolving dependencies so reply-adapter pulls ai-sdr-core with it.
reply skills install # every pack, every detected assistant
reply skills install core # aliases: core · adapter · runtime
reply skills install adapter runtime
reply skills install --agent codex # only this assistant
reply skills install --project # into this repository, not your home directory
✓ detected Claude Code, Codex
✓ Claude Code · ai-sdr-core, reply-adapter, agentic-runtime installed
✓ Codex · ai-sdr-core, reply-adapter, agentic-runtime installed
Start a new session in each assistant so the skills load.
Then manage them:
reply skills list # what is installed where, and what has an update available
reply skills update # bring installed packs to the latest version
reply skills remove runtime # remove one pack; `reply skills remove` removes all of them
--json gives a machine-readable report and --dry-run shows the plan without changing anything. On Claude Code and Codex the packs go in through the assistant’s own plugin mechanism, so they keep updating through it; other SKILL.md hosts receive the skills as files.
Claude Code and Codex are the two hosts this is verified against. For Cursor, Windsurf, Gemini CLI and GitHub Copilot the installer writes to each host’s documented skills directory, but that those hosts read from it has not been confirmed; the command labels them (paths not yet verified) in its output and marks them "verified": false in --json.
Installing skills is not the same as connecting Reply: the reply-adapter pack needs a login (reply auth login) before it can do anything.
Profiles & teams
A profile is a signed-in account; a team is the workspace a request acts in. Most people never touch either; the built-in default profile is used until you create another. They matter when an agent has to act as more than one Reply.io user, or across more than one workspace, without re-authenticating between calls.
Profiles
Each profile stores its own credential. Name them however you like; account emails work well:
reply profile add alice@reply.io # --team-id optionally pins a team at creation
reply profile use alice@reply.io # make it the active profile
reply auth login # signs in alice@reply.io
reply profile list # '*' marks the active profile
reply profile current # just the active profile name
reply --profile bob@reply.io auth whoami # override for a single command
The active profile resolves as --profile → REPLY_PROFILE → the profile set with profile use → the built-in default.
Manage them after creation:
reply profile show # active profile: team + which authorization would be used
reply profile show alice@reply.io # a specific one
reply profile set alice@reply.io --team-id 1045 # pin a team on a profile
reply profile unset alice@reply.io team-id # clear a field (authority|api_base|team-id)
reply profile rename alice@reply.io ally # also moves the stored credential
reply profile delete ally # removes it and its stored credential (-y skips the prompt)
profile show prints the backend URLs, the pinned team, and which authorization would be used; in priority order --api-key → REPLY_API_KEY → stored credential. It never prints tokens or keys, and neither does any other command, --verbose included.
Where credentials live. JSON in the config directory (~/.config/reply, or %APPDATA%\reply on Windows), written 0600 inside a 0700 directory: the same plaintext-file model as gh, aws, and az. Records are keyed by profile name, so two accounts on the same backend never collide. An OAuth record refreshes its access token automatically; if the refresh fails the record is cleared and you’re asked to log in again.
Teams
A profile can pin a team (workspace). The pin is sent as the X-TEAM-ID header on every request, with precedence --team-id → REPLY_TEAM_ID → the profile’s team. The team command reads and writes the active profile’s team:
reply team list # teams you can act in ('*' marks the profile's team)
reply team current # the pinned team, plus the effective team from whoami
reply team use 1045 # verify 1045 is one of your teams, then pin it
reply team clear # remove the pin
reply --team-id 1045 api /v3/sequences # or scope a single call, no pin
If a call needs a team and your credential spans several, the API answers TEAM_REQUIRED listing your teams, and the CLI adds a one-line fix-it hint on stderr. Pin one with reply team use <id>, or pass --team-id for that command only.
With an organization API key you can also name the acting user per call: --user-id <id> or --user-email <email> (the email form requires a team id). Both are ignored by ordinary personal keys.
Built for agents
- Sign in once, run as you – OAuth or API key; subsequent calls carry your identity with no per-request auth code.
- JSON everywhere – add
--jsonfor machine-readable output;reply apiemits{ "code", "data" }, safe to pipe intojqor agent logic. - Dedicated commands for the core objects –
reply sequence,reply contact,reply contact-list,reply inbox,reply account,reply scheduleandreply direct-sendcover sequencing, contact management, list membership, inbox triage, connected accounts, sending schedules and scheduled-send tracking directly;reply api /v3/...reaches everything else, and the skills tell the agent which call to compose for what isn't wrapped yet. - Multi-account – profiles and team pinning let one agent act across workspaces without re-authing.
- It installs its own reasoning layer –
reply skills installputs the outbound expertise into the assistant, so procedure and execution arrive together rather than being wired up by hand. - A stable machine contract – data on stdout, status and errors on stderr; exit
0success,1failure,2usage error, and3on the work commands when a write's outcome is unknown (each command's own--helpsays whether re-running it is safe);429/5xxretried internally (honoringRetry-After), so don’t wrap your own retry loop around writes. Full contract: docs.reply.io/cli/agents.
# reach the whole v3 API through the passthrough available today
reply api /v3/sequences | jq '.data'
Environment variables
| Variable | Description |
|---|---|
REPLY_API_KEY | API key used as the credential for the current invocation |
REPLY_PROFILE | Profile to use (same as --profile) |
REPLY_TEAM_ID | Team/workspace id sent as X-TEAM-ID (same as --team-id) |
REPLY_CONFIG_DIR | Config directory (default ~/.config/reply; %APPDATA%\reply on Windows) |
REPLY_NO_UPDATE_CHECK | Set to 1 to never check whether a newer release exists |
Where the command surface is going
The first wave of dedicated commands has landed: reply sequence, reply contact, reply contact-list, reply inbox, reply account, reply schedule and reply direct-send cover sequencing, contact management, list membership, inbox triage, connected-account and sending-schedule lookups, and scheduled-send tracking. Account-wide reporting beyond reply sequence stats still has no dedicated command, and the gap is deliberate rather than a backlog nobody got to.
A command surface is a promise: once a command exists, its name, flags and output shape are a contract, and a wrong shape is expensive to unpick after people script against it. So rather than translating the endpoint list into commands one for one, the commands follow the SDR operation contract: the vendor-neutral set of business operations already written down in sdr-operations, where each operation carries what it means, whether it is reversible, and whether it needs a human’s approval before it runs. Commands that mirror that contract stay stable as the API grows. Commands that mirror today’s endpoint list do not. No date is published for the rest, and this page does not imply one.
Nothing is blocked in the meantime. reply api /v3/... reaches every documented endpoint, including reporting, and the agent skills exist precisely so an agent does not have to guess which call to make: they carry the operation-to-endpoint mapping, the order to call things in, the scopes each one needs, and where to stop for human approval. An agent with the skills installed composes correct calls today, through the dedicated commands where one exists and reply api otherwise.
Where a shell is the wrong fit, use the surface that is: REST for your own integration, MCP for a tool surface inside an LLM client.
Versioning & stability
reply-cli follows semantic versioning, with the version derived automatically from Conventional Commit messages (fix: → patch, feat: → minor). It is pre-1.0: while the version is 0.x, a minor release (0.x.0) may contain breaking changes, per semver’s own 0.x rule; pin an exact version if you need stability before 1.0.0. 1.0.0 is cut once the command surface and the --json output shape are declared stable.
From 1.0.0 on, a major bump is required to remove or rename a command, flag, or argument; change the meaning of an exit code; change the --json / --pretty output shape; change the config format or a REPLY_* environment variable; or raise the minimum Node.js version. Anything on that list is deprecated first; it keeps working and warns on stderr (never stdout, so --json stays clean) for at least one minor release before it is removed.
Contributing
The CLI is developed in the open, and anyone can clone it, build it, and open a pull request; contributions and bug reports are welcome. Merging is restricted: only Reply employees approve and merge, so open a PR from a branch or a fork and a maintainer reviews it.
git clone https://github.com/reply-team/reply-cli && cd reply-cli
npm install
npm run build # compile TypeScript to dist/
npm test # vitest; the suite is fully offline, no test touches Reply.io
npm link # put your build's `reply` binary on your PATH
The house rules are the same contract your scripts depend on, so they’re worth knowing either way:
- stdout is data, stderr is everything else – status and error messages never pollute a pipe.
--jsonis compact,--prettyis indented; on either, an error is one machine-readable line:{"error":{"status":…,"code":…,"title":…,"detail":…,"hint":…}}.- Exit codes –
0success,1API or runtime failure,2usage error; the sequence, contact, contact-list, inbox and direct-send commands add3for a write whose outcome Reply did not confirm. - Secrets are never printed – token and key fields are redacted in every code path.
- Conventional Commits are enforced by commitlint (a local
commit-msghook and a PR check), because the release version is derived from them. While the CLI is on0.x, don’t use!orBREAKING CHANGE:: a breaking change rides in an ordinaryfeat:.
Build layout, the test approach, credential-store internals, and the release process are documented in CONTRIBUTING.md.
Source and issues: github.com/reply-team/reply-cli · Package: npmjs.com/package/reply-cli · Docs: docs.reply.io/cli/overview
Frequently asked questions
Does the CLI exist today?
Yes. Install it with `npm i -g reply-cli` (Node 20+). The current version covers authentication (OAuth and API key), multi-account profiles, team selection, dedicated commands for sequences, contacts, contact lists, the inbox, connected accounts, sending schedules and scheduled sends, `reply api`; a raw authenticated passthrough to the full v3 API, plus `reply skills` to install the agent skills into your AI assistants and `reply install` to keep the CLI itself current. Account-wide reporting beyond `reply sequence stats` has no dedicated command yet; use `reply api /v3/...` for that today. The canonical API reference is at docs.reply.io.
Why a CLI when there's already REST and MCP?
Coding agents are exceptionally good at running shell commands. The CLI signs in once and carries your identity into every call, so an agent reaches the whole v3 API through `reply api` without writing HTTP client or auth code, and humans get a scriptable surface for one-off operations.
How does it relate to the agent skills?
The skills (see /skills) are the reasoning layer; they teach an agent what an outbound operation is, which one this situation calls for, and where it must stop and ask a human. The CLI is the execution layer that carries it out. The CLI also installs them: `reply skills install` puts the packs into every AI assistant it finds. A skill composes `reply sequence`/`reply contact`/`reply contact-list`/`reply inbox`/`reply account` calls where a dedicated command exists, and `reply api /v3/...` otherwise; as more commands land, it will shell out to those instead, and the procedure it follows will not change.
Can I contribute to the CLI?
Yes. It’s developed in the open. Clone the repo, run `npm install && npm run build && npm test` (the test suite is fully offline), and open a pull request from a branch or a fork. Only Reply employees can approve and merge, so a maintainer reviews every PR. Commit messages follow Conventional Commits because the release version is derived from them. Full guide: CONTRIBUTING.md in github.com/reply-team/reply-cli.
How stable is it?
Pre-1.0. While `reply-cli` is on 0.x, a minor release may contain breaking changes; pin an exact version if you need stability. From 1.0.0 on, anything in the compatibility surface (commands, flags, exit codes, `--json` shape, config format, `REPLY_*` variables, minimum Node version) needs a major bump, and is deprecated with a stderr warning for at least one minor release first.