Docs Go to app →

CLI

@letterapp/cli does two jobs:

  1. Connect your app to Letter with a secure, interactive device login - no API key ever touches your shell or chat history.
  2. 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.json and 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 doesFormatDashboardCLI groupEnv var
Send events from your app (SDK / ingestion API)lt_live_*Project tokensproject-tokensLETTER_API_KEY
Run the CLI / Management APIlt_pat_*API keysapi-keysLETTER_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: --project flag, the LETTER_PROJECT env var (handy for scripts and CI), then the project connected by letter 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.

GroupCommands
projectslist · get <slug> · create · update <slug> · delete <slug>
memberslist · create (invite) · delete <userId>
invitationslist · create · delete <id>
api-keyslist · create (--name) · delete <id> - your personal access tokens (lt_pat_*)
contactslist · get <externalId> · suppress <externalId> · resubscribe <externalId> · import
accountslist · get <externalId>
eventslist (--name)
traitslist - distinct contact/account trait keys, types, and a sample value
suppressionslist · create (--email) · delete <email>
segmentslist · get <id> · create · update <id> · delete <id> · preview
listslist · get <id> · create · update <id> · delete <id> · members <id> · add-member <id> --contact · remove-member <id> --contact
formslist · get <id> · create · update <id> · delete <id> · submissions <id> · duplicate <id>
sequenceslist · get · create · update · delete · scaffold · draft · validate · publish · preview · test-email · activity · versions · schema
broadcastslist · get · create · update · delete · preflight · schedule · cancel · live
templateslist · get · create · update · delete · default · reset · logo · remove-logo
domainslist · create (--domain) · verify <id> · delete <id>
project-tokenslist · create (--name) · delete <id> - project ingestion keys (lt_live_*)
sender-identity--from-email --from-name --reply-to-email
sending-mode<letter_subdomain|byo_domain>
methe 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 scaffold builds a complete, valid trigger → wait → email → exit graph in one command. --body-md converts Markdown (including links and bold) to the email’s TipTap body, --audience-trait plan=free is shorthand for a contact-trait filter, and --dry-run prints the graph without saving. --if-not-exists reuses an existing sequence with the same name instead of creating a duplicate.
  • sequences draft auto-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 n1 and test-email render the saved draft node by default - no --config needed. Pass --config @file.json to override.
  • traits list shows 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.