CLI
@letterapp/cli does two jobs:
- Connect your app to Letter with a secure, interactive device login - no API key ever touches your shell or chat history.
- Operate your workspace from the terminal: the same things the dashboard does (projects, contacts, segments, lists, forms, sequences, broadcasts, templates, domains, members) are available as resource commands over the Management API.
npx @letterapp/cli # == letter login
# or install globally:
npm install -g @letterapp/cli
Logging in
letter login
This runs an RFC 8628 device flow: the CLI prints a short code, opens your browser, you confirm the code and pick a project, and the CLI receives two credentials over a secure back channel:
- a project ingestion key (
lt_live_*) written to your project’s env file (LETTER_API_KEY) for the SDK / data-plane, and - a workspace Personal Access Token (
lt_pat_*) stored in~/.letter/credentials.jsonand used by every management command below.
Neither secret is printed to the terminal. The connected project becomes the
default for management commands (override per-command with --project).
Which credential does what
You almost never pick one by hand: letter login mints both and every command
grabs the right one automatically. The two map across surfaces like this:
| What it does | Format | Dashboard | CLI group | Env var |
|---|---|---|---|---|
| Send events from your app (SDK / ingestion API) | lt_live_* | Project tokens | project-tokens | LETTER_API_KEY |
| Run the CLI / Management API | lt_pat_* | API keys | api-keys | LETTER_PAT |
Rule of thumb: a lt_live_* ingestion key belongs in your app’s environment;
a lt_pat_* token is your credential for operating the workspace. The CLI’s
project-tokens and api-keys groups manage each kind (the older keys and
tokens names still work as aliases).
letter auth status shows the current connection; letter auth logout removes
the stored credential.
CI / non-interactive
For pipelines, set the PAT in the environment instead of running the device flow:
export LETTER_PAT=lt_pat_xxx
letter projects list --json
Global flags
--json- emit raw JSON for scripting and agents. Every command supports it; errors come back as{ "error": "..." }with a non-zero exit code.--project <slug>- target a specific project (management commands only). Resolved in order:--projectflag, theLETTER_PROJECTenv var (handy for scripts and CI), then the project connected byletter login.
Resource commands
Each resource is a command group with list, get, create, update,
delete, plus verb subcommands where the API has them. Run
letter <group> --help for the exact flags.
| Group | Commands |
|---|---|
projects | list · get <slug> · create · update <slug> · delete <slug> |
members | list · create (invite) · delete <userId> |
invitations | list · create · delete <id> |
api-keys | list · create (--name) · delete <id> - your personal access tokens (lt_pat_*) |
contacts | list · get <externalId> · suppress <externalId> · resubscribe <externalId> · import |
accounts | list · get <externalId> |
events | list (--name) |
traits | list - distinct contact/account trait keys, types, and a sample value |
suppressions | list · create (--email) · delete <email> |
segments | list · get <id> · create · update <id> · delete <id> · preview |
lists | list · get <id> · create · update <id> · delete <id> · members <id> · add-member <id> --contact · remove-member <id> --contact |
forms | list · get <id> · create · update <id> · delete <id> · submissions <id> · duplicate <id> |
sequences | list · get · create · update · delete · scaffold · draft · validate · publish · preview · test-email · activity · versions · schema |
broadcasts | list · get · create · update · delete · preflight · schedule · cancel · live |
templates | list · get · create · update · delete · default · reset · logo · remove-logo |
domains | list · create (--domain) · verify <id> · delete <id> |
project-tokens | list · create (--name) · delete <id> - project ingestion keys (lt_live_*) |
sender-identity | --from-email --from-name --reply-to-email |
sending-mode | <letter_subdomain|byo_domain> |
me | the token’s user, workspace, and role |
JSON fields and files
Flags that take JSON (--filter, --graph, --trigger, --audience,
--design, --mapping) accept either an inline JSON string or @path to read
from a file:
letter segments create --name "Paid" --filter @filter.json
letter sequences draft seq_123 --graph @graph.json --trigger @trigger.json \
--expected-revision 4
Authoring sequences
A few conveniences make sequences self-serve from the terminal (see the Sequences guide for the JSON shapes):
sequences scaffoldbuilds a complete, valid trigger → wait → email → exit graph in one command.--body-mdconverts Markdown (including links and bold) to the email’s TipTap body,--audience-trait plan=freeis shorthand for a contact-trait filter, and--dry-runprints the graph without saving.--if-not-existsreuses an existing sequence with the same name instead of creating a duplicate.sequences draftauto-fetches the current revision when you omit--expected-revision, so you don’t have to look it up first.sequences validate <id>runs the publish checks against the draft and prints any blocking errors without shipping.sequences preview <id> --node-id n1andtest-emailrender the saved draft node by default - no--configneeded. Pass--config @file.jsonto override.traits listshows every trait key with its inferred type and a sample, so you know what to filter on. Remember filter values compare as strings (plan=free,isPro=false).
# One-shot scaffold of a free-user limit nudge.
letter sequences scaffold \
--name "Limit reached - free users" \
--event modification_limit_reached \
--audience-trait plan=free \
--wait 1h --subject "You hit your limit" \
--body-md "**Upgrade** to keep going - [see plans](https://acme.com/billing)." \
--publish
letter traits list # discover filterable keys
letter sequences validate seq_123 # dry-run the publish checks
letter sequences preview seq_123 --node-id email1
Lists and forms
Lists are static contact groupings with explicit membership. Manage the list and its members from the terminal:
letter lists list # all lists + member counts
letter lists create --name "Beta testers"
letter lists members lst_123 --limit 100
letter lists add-member lst_123 --contact alice@acme.com
letter lists remove-member lst_123 --contact alice@acme.com
Adding a contact fires the same list_entered event the dashboard does, so any
sequence triggered by “entered a list” enrolls the contact.
Forms collect external signups into one or more lists. Edit the field list and delivery config as JSON, then publish by setting the status:
letter forms list
letter forms create --name "Newsletter"
letter forms update frm_123 --fields @fields.json --target-list-ids '["lst_123"]'
letter forms update frm_123 --status published # needs >=1 target list
letter forms submissions frm_123 --status confirmed
Sequence triggers for forms and lists
sequences scaffold can build the new entry triggers directly. Find the id with
letter forms list / letter lists list, then:
# Enroll when a contact submits a specific form.
letter sequences scaffold --name "Welcome" --form frm_123 \
--subject "Welcome aboard" --body-md "Thanks for signing up." --publish
# Enroll when a contact is added to a list.
letter sequences scaffold --name "Onboarding" --list lst_123 \
--wait 1h --subject "Getting started" --body-md "Here's step one."
letter sequences schema prints the canonical form_submitted and
list_entered trigger shapes for use with sequences draft --trigger.
Examples
# Create a project and inspect it
letter projects create --name Acme --timezone Europe/Paris --json
letter projects get acme --json
# Work inside a project (default project comes from `letter login`)
letter contacts list --limit 20
letter contacts get user_alice
letter contacts suppress user_alice
# Segments
letter segments preview --filter @paid.json
letter segments create --name "Paid" --filter @paid.json
# Lists: create one, then manage membership
letter lists create --name "Newsletter"
letter lists add-member lst_123 --contact user_alice # fires `list_entered`
letter lists members lst_123 --limit 100
# Forms: build a draft, point it at a list, publish, read submissions
letter forms create --name "Welcome form"
letter forms update frm_123 --fields @fields.json --target-list-ids '["lst_123"]'
letter forms update frm_123 --double-opt-in true --submit-label "Subscribe"
letter forms update frm_123 --status published # needs >=1 target list
letter forms submissions frm_123 --status confirmed
# Sequences: edit the draft, then publish
letter sequences draft seq_123 --graph @graph.json --trigger @trigger.json \
--expected-revision 7
letter sequences publish seq_123
# ...or scaffold a sequence that enrolls on a form submission / list entry
letter sequences scaffold --name "Welcome" --form frm_123 \
--subject "Welcome aboard" --body-md "Thanks for signing up." --publish
# Broadcasts: preflight, then send now (omit --scheduled-at) or schedule
letter broadcasts preflight bc_42
letter broadcasts schedule bc_42 # send now
letter broadcasts schedule bc_42 --scheduled-at 2026-06-01T09:00:00Z
letter broadcasts live bc_42 --json
# CSV import
letter contacts import --file users.csv --mapping @mapping.json
# Sending setup
letter domains create --domain mail.acme.com
letter domains verify dom_1
letter sender-identity --from-email hi@mail.acme.com --from-name "Acme"
letter sending-mode byo_domain
Roles
Your PAT carries your workspace role. Read and project-resource writes need
member; creating/deleting projects, managing members, invitations, domains,
project tokens, API keys, and sending settings need admin. Calls beyond your
role return a 403 forbidden, surfaced as a clear CLI error.
See the Management API reference for the underlying endpoints and the machine-readable OpenAPI spec.