Use the CLI

Install the Samva CLI, authenticate, select an execution context, and operate email from your terminal.

The samva CLI operates the email API from a terminal. Use it for direct work, scripts, and agent automation. Run samva <command> --help for command-specific flags and examples.

samva mailboxes reads, sends, and tails mailbox mail, and samva extensions builds and installs mailbox extensions. See Mailbox CLI.

Install and complete commands

npm install -g @samva/cli
samva --help

The package prefers its platform-native executable and falls back to the packaged JavaScript CLI through Bun when npm omits optional dependencies.

The installed command includes shell completion generation:

# Bash
samva --completions bash >> ~/.bashrc

# Zsh
mkdir -p ~/.zsh/completions
samva --completions zsh > ~/.zsh/completions/_samva
# Add ~/.zsh/completions to fpath, then run `autoload -Uz compinit && compinit` in .zshrc.

# Fish
mkdir -p ~/.config/fish/completions
samva --completions fish > ~/.config/fish/completions/samva.fish

# sh emits the Bash-compatible completion script
samva --completions sh

Regenerate the script after upgrading the CLI.

Authenticate and choose a context

Use an API key for non-interactive automation. It is scoped to one organization.

export SAMVA_API_KEY="samva_sk_live_your_api_key"

For interactive OAuth, sign in and choose an organization:

samva login
samva org list
samva org use <slug>

SAMVA_API_KEY takes precedence over stored OAuth credentials. An API key already identifies its organization, so the CLI rejects --org and SAMVA_ORG while that key is set. With OAuth, the organization resolves as --org, then SAMVA_ORG, then the selected profile's organization.

samva org current prints the active organization's slug. Under SAMVA_API_KEY it asks the API which organization the key belongs to, so it works for any key, including one that can only send; --json adds the organization and the key's id and name.

samva login is interactive. In a non-TTY, use SAMVA_API_KEY for automation or explicitly choose the device flow with samva login --no-browser; --no-input, --json, and --jsonl reject login. If a stored OAuth record is malformed, run samva logout and then samva login; API-key calls do not read or decode stored OAuth credentials.

Profiles keep non-secret execution settings. OAuth credentials remain in the OS keyring.

samva profile create acme --org acme
samva profile use acme
samva --profile acme email doctor

The profile resolves as --profile, then SAMVA_PROFILE, then the active profile, then default. The API URL resolves as --api-url, then SAMVA_API_URL, then https://api.samva.dev. Profiles do not select an API; a config whose profile sets target fails to load and names these overrides.

Send, inspect, and wait

Read contacts with a bounded list or a full contact record:

samva contacts list --fields plan,signup_at --json
samva contacts list --search ada --json
samva contacts list --custom-field plan --custom-field-value '"creator"' --json
samva contacts get contact_k9mvt4znw8rh7q2x --json
samva contacts list --fields plan,signup_at --all --jsonl

--fields selects defined custom field keys. --custom-field and --custom-field-value select one exact value; the value must be a JSON scalar, including quotes around strings. See contacts for lookup, creation, and custom-field behavior.

samva email send \
  --to ada@example.com \
  --subject "Welcome to Samva" \
  --text "Welcome"

printf 'Welcome' | samva email send \
  --to ada@example.com \
  --subject "Welcome to Samva" \
  --text -

render-email | samva email send \
  --to ada@example.com \
  --subject "Welcome to Samva" \
  --html -

samva email send \
  --to ada@example.com \
  --template-slug welcome-email \
  --template-data '{"name":"Ada"}' \
  --wait --timeout 2m

samva messages get <message-id>
samva messages events <message-id> --json

Inline email requires --subject and exactly one body source: --html or --text. Pass - as the value to read that body from stdin: --text - reads a plain-text body and --html - reads an HTML body. Only one body may come from stdin, so passing both --text - and --html - is rejected. Template sends use one of --template-id or --template-slug, with optional --template-data, and omit --subject. Add --locale (for example de-DE) and --time-zone (for example Europe/Berlin) to a template send to format its dates and money for the recipient; they are rejected on inline content.

--wait polls for delivery or read status, or a terminal failure. It defaults to two minutes. Use --timeout only with --wait. --wait cannot combine with --dry-run, since a dry run makes no API call and has nothing to wait on. A timeout or terminal delivery failure prints the last message state and exits non-zero.

Choose which publication renders

A template send renders the template's current publication unless you say otherwise. --publication-id pins one exact publication, so nothing you publish afterwards changes what that send renders. --input-contract-id instead follows new publications while the input contract holds: a republish that only rewords field descriptions is picked up; one that changes what input is valid is not, until you name its contract.

samva email send --to ada@example.com --template-id tmpl_... \
  --publication-id tpub_... --template-data '{"name":"Ada"}'

samva scheduled-messages create --to ada@example.com --template-id tmpl_... \
  --input-contract-id tcon_... --template-data '{"name":"Ada"}' --at 2026-10-01T09:00:00Z

samva campaigns create --name "Launch" --template-id tmpl_... \
  --publication-id tpub_... --include-tags customers

Both flags reference one template's history, so each needs --template-id, and naming both at once is refused. samva templates types writes these references for you.

Configure email and check readiness

samva email domains list
samva email domains add --domain example.com --dry-run
samva email senders list
samva email senders add --email hello@example.com --dry-run
samva email senders set-default <sender-id> --dry-run
samva email domains set-default <domain-id> --dry-run
samva email doctor --json
samva email review status --json
samva email review reapply --json
samva email tracking defaults get
samva email tracking defaults set --opens on --clicks off

samva email senders set-default makes a verified sender the organization default. Sends that omit from use it when the template names none. samva email domains set-default marks a verified domain as the default sending domain. email domains list and email senders list show which one is the default. Both set-default commands take --dry-run to print the change without calling the API.

samva email doctor is read-only. It reports the resolved profile, API URL, auth source, organization access, the send review decision, how many domains are sendable and senders verified, and domain-verification readback. readyToSend is true when a production send would be admitted and at least one domain or sender can carry it. It does not inspect receiving configuration, send email, check DNS itself, or replay fixtures. See Verify your domain for DNS setup.

Each check is passed, failed, or unavailable. A failed check carries the underlying error detail; unavailable, such as no sending domain yet or a review in progress, is informational; a rejected review fails. The command always prints the full report, then exits 1 if any check failed and 0 otherwise. The --json result's data adds an aggregate ok boolean. Human output marks a passed check with ✓, an unavailable one with !, and a failed one with ✗.

Use the receiving resource commands when configuring inbound mail:

# Read each receiving prerequisite in order, with a live MX lookup.
samva email receiving status <domain-id>

# Preview without credentials or an API call.
samva email receiving enable <domain-id> \
  --domain example.com --address replies@example.com --dry-run

# Configure receiving. This ordinary mutation acts immediately and does not use --yes.
samva email receiving enable <domain-id> \
  --domain example.com --address replies@example.com

# Receive every address on the domain. A catch-all needs a destination: a webhook endpoint
# (--endpoint) or an active forwarding rule that covers the domain.
samva email receiving enable <domain-id> \
  --domain example.com --catch-all --endpoint <webhook-endpoint-id>

Schedule email and run campaigns

samva scheduled-messages create --to ada@example.com --template-slug welcome-email \
  --template-data '{"name":"Ada"}' --at 2026-08-01T09:00:00Z \
  --timezone America/New_York --idempotency-key welcome-ada
samva scheduled-messages get <scheduled-message-id>
samva scheduled-messages cancel <scheduled-message-id> --yes
samva scheduled-messages resume <scheduled-message-id> --send-now

samva campaigns create --name "August launch" --template-slug august-launch \
  --include-tags newsletter
samva campaigns runs create <campaign-id> --at 2026-08-01T09:00:00Z \
  --idempotency-key august-launch
samva campaigns runs recipients <campaign-id> <run-id> --status failed

--at is an absolute ISO 8601 instant. --timezone is display and audit metadata for scheduled messages. Campaign definitions freeze after their first run. Use --yes to cancel non-interactively.

For resource-specific behavior, see Schedule an email and Send an email campaign.

Test a webhook endpoint

# Validate and preview without credentials or an API call.
samva webhooks test <webhook-id> --data @payload.json --dry-run

# Queue a signed test delivery to the webhook's endpoint.
samva webhooks test <webhook-id> --data '{"event":"webhook.test"}' --json

# Read what the endpoint answered.
samva webhooks logs <webhook-id>

--data accepts an inline JSON object or @file as the test event's data. Omit it to send the default test event. The command acts by default. Samva queues a signed webhook.test event through the normal delivery and retry lifecycle and answers at once, so the command prints the queued event and delivery ids (eventId and deliveryId with --json) and exits 0 without waiting for your endpoint. Read the endpoint's response with samva webhooks logs <webhook-id>.

Pull and push a brand

A template imports its organization's brand with @import "samva:brand"; (the default brand) or "samva:brand/<slug>" in theme.css. samva brands works with those brands from the project; Brands explains the import and the brand CSS.

samva brands list

# Write .samva/brands/<slug>.json; local previews and checks use it when offline.
samva brands pull
samva brands pull acme

# Validate brand CSS without credentials, then replace a brand's CSS with it.
samva brands push brand.css --brand acme --dry-run
samva brands push brand.css --brand acme

pull never changes theme.css. samva templates dev, check, build, render and export resolve the live brand with your samva login session or SAMVA_API_KEY, fall back to the pulled snapshot with a warning, and warn when the snapshot is older than the live brand. Keep .samva in .gitignore.

push checks the file against the brand CSS grammar first and exits 1 with every finding at file:line:column, uploading nothing. Without --brand it updates the default brand. Published templates keep the brand they were built against until you republish them; see Update a brand.

Author templates locally

A template is a TSX file in the static profile: the CLI reads and renders it without running it, so a local result matches what a publication sends. Run these from the template project's directory; they look in templates/ and emails/.

# Create a project on Samva and clone the canonical starter.
samva templates init --name "Order shipped"

# Serve the visual editor with every fixture rendered. No vite.config.ts needed.
samva templates dev --open

# Compile every template and render every fixture. Exits 1 on any error.
samva templates check
samva templates check --json
samva templates check --watch

# Print one fixture's subject, preheader, and plain text, or its HTML.
samva templates render order-shipped --fixture no-tracking
samva templates render order-shipped --fixture default --format html --out order.html
samva templates render order-shipped --fixture default --locale de-DE --time-zone Europe/Berlin

# Screenshot one fixture at desktop and mobile widths, light and dark.
samva templates snapshot order-shipped --fixture default --out shots

# Publish the current pushed commit, then generate typed inputs for your application.
samva templates publish --commit HEAD
samva templates types --out src/samva-templates.ts

check reports each finding as file:line:column with a diagnostic code and a fix; --json adds ranges and machine-readable edits. render takes a template id or the path of its .tsx file, and its --locale and --time-zone flags preview the formatting a send with the same options produces. snapshot writes four PNGs by default; narrow them with --viewport and --scheme. types --check reports drift without writing, for CI.

Author a template without a clone

samva templates workspace edits a template's hosted project workspace, for a caller with no Git clone such as a CI job or an agent in a sandbox. Every subcommand prints its full result as JSON, and every write is fenced to the revision workspace open returned.

samva templates workspace open tmpl_abc123

# Add an image; it lands at assets/logo.png unless --path names another project path.
samva templates workspace add-asset tmpl_abc123 ./logo.png \
  --revision 4 --idempotency-key logo-1

# Change a file that already exists.
samva templates workspace add-asset tmpl_abc123 ./logo@2x.png --path assets/logo.png \
  --replace --revision 5 --idempotency-key logo-2

add-asset sends the file's bytes as base64 in the JSON request to Put a template project asset, which lists the image and font formats it accepts and its size limit. The bytes must be the format the file extension names. Without --replace, a path that already holds a file is refused; with it, a path that holds nothing is refused. The asset stays unsaved until samva templates workspace save commits it.

Manage API keys

# Preview a restricted key without credentials or an API call.
samva api-keys create --name ci --grant email=write --grant contacts=read --dry-run

# Create it and write the secret straight into an env file.
echo "SAMVA_API_KEY=$(samva api-keys create --name ci --grant email=write --json | jq -r .data.key)" >> .env

# Read, rename, rotate, and revoke.
samva api-keys list --status active
samva api-keys get <key-id>
samva api-keys update <key-id> --name ci-prod
samva api-keys rotate <key-id> --yes --json | jq -r .data.key
samva api-keys revoke <key-id> --yes

create takes --name, an optional --description and --expires-at (an ISO 8601 instant), and exactly one way to set access: --full-access, or one or more --grant <resource>=read|write for a restricted key. Omitting both or passing both exits 2. A resource that has no write action, such as analytics, takes read only. Grants and expiry are fixed at creation; create a new key to change them. Mailbox and namespace scope are not flags: set them with samva api. The API refuses a key that would hold more access than the key or person creating it, so a restricted key cannot mint a full-access one. See Authentication.

The secret appears once, in create and rotate. With --json it is data.key, next to the key's apiKey metadata, displayOnce: true, and a warning; in human output the secret is a field on stdout and the warning goes to stderr. rotate replaces the key and stops the old one at once, so a CLI that authenticates with it needs SAMVA_API_KEY set to the new secret. rotate and revoke ask for confirmation in a terminal and need --yes otherwise, and every mutation offers --dry-run. Revocation is final, and the key stays in list as revoked. list pages with --limit and --cursor, filters with --status and --search, and streams every page with --all --jsonl.

Call any endpoint

Every operation in the API reference is reachable from the CLI. Commands cover the common workflows. For an operation that has no command, samva api sends the request with the same credentials as every other command and prints the response.

# Read a resource. The path is relative to the API, and the /v1 prefix is optional.
samva api email/domains

# Create a resource. Fields form the JSON body, and the method becomes POST.
samva api webhooks -f name=orders -f url=https://example.com/hook -f 'eventTypes[]=message.delivered'

# Read every page of a list and filter it with jq.
samva api email/domains --paginate --slurp | jq -r '.[].items[].domain'

# Delete a resource and print the status line and headers.
samva api webhooks/wep_abc123 -X DELETE -i

Prefer a command when one exists. It validates its input, previews with --dry-run, confirms destructive work, and prints the CLI result envelope. samva api passes the request through and prints the API's own response, so its output matches the API reference and --json and --jsonl are refused. Pipe the output to jq to filter it.

FlagEffect
-X, --methodChoose the HTTP method. The default is GET, or POST once a field or --input is given.
-f, --raw-field key=valueAdd a string field.
-F, --field key=valueAdd a typed field: true, false, null, and integers become JSON values. @path reads the value from a file and @- from stdin.
--input fileSend a file, or - for stdin, as the request body. Fields then go to the query string.
-H, --header key:valueAdd a request header.
--paginate, --slurpFollow nextCursor and print each page on its own line, or all pages as one JSON array.
-i, --includePrint the status line and response headers before the body.
--silent, --verboseSuppress the body, or write the request and response headers to stderr with credentials redacted.

Fields nest with a[b]=value and build arrays with a[]=value; a bare a[] is an empty array. On a GET, or with --input, fields go to the query string and arrays repeat the key. The response body is written to stdout exactly as the API returned it; an interactive terminal gets indented JSON. A response other than 2xx writes its body to stderr and exits 4 for 401 and 403, otherwise 1. A malformed flag or field exits 2. The path must be relative: samva api sends your credentials only to the API URL of the selected profile and does not follow redirects.

Automate safely

# Validate and print a request without an API call.
samva email send --to ada@example.com --subject "Hello" --text "Hi" --dry-run --json

# Stream every page as JSON Lines.
samva messages list --all --jsonl

# Suppress successful human output.
samva --quiet profile use acme

--dry-run validates supported mutations and previews their intended request. email send --dry-run does not use credentials or call the API. Another preview can read state when needed to show the resulting change. Destructive operations prompt in a TTY. In automation, use --yes to confirm or --no-input to disallow prompts and browser launches. Without confirmation, the command fails without a state change.

--json writes one versioned result envelope to stdout. --jsonl writes one versioned result envelope per line and is required for --all pagination. --quiet suppresses successful output. The three modes are mutually exclusive. Machine output keeps stdout to result data only; warnings and failures are written to stderr. Assignment spellings such as --json=true are usage errors.

{
  "schemaVersion": 1,
  "type": "result",
  "command": "email.send",
  "data": { "id": "msg_abc123", "status": "sent" }
}

For resource lists, --jsonl preserves that outer envelope on every line and places the resource itself in data. --all changes traversal only, not the record schema. Empty lists emit no lines; otherwise parse one line at a time and do not expect an array.

Use --limit for a bounded page and --cursor with the Next cursor a page prints to read the next one; lists that filter by time take --after and --before. Lists are paginated by default.

CodeMeaning
0Success
1API, operational, or terminal delivery failure
2Command syntax, validation, configuration, or confirmation failure
4Authentication or organization-context failure, including an expired session
124Wait or credential timeout
130Interrupted with SIGINT
143Terminated with SIGTERM

In machine modes, a command failure emits a JSON diagnostic on stderr and does not mix it with stdout data:

{
  "schemaVersion": 1,
  "type": "error",
  "command": "messages.wait",
  "reason": "message_wait_timeout",
  "message": "Timed out waiting for message msg_abc123; it remains sent.",
  "next": "samva messages wait msg_abc123"
}

Lifecycle timeout, terminal delivery failure, and a later observation failure first preserve the last authoritative Message in a result envelope with partial: true on stdout, then emit the error envelope on stderr. Save data.id and resume with samva messages wait <message-id>. email doctor is a report rather than an execution failure: it writes its full result envelope with data.ok: false and exits 1 when a check fails.

A command that needs a subcommand, such as a bare samva or a group like samva email, prints its help to stdout and exits 2, so a script never mistakes a missing subcommand for success.

Next steps

API errors on stderr carry the API tag as reason, HTTP status, public fields in details, and recovery guidance in next. Fix details.fields before retrying. API 401 and 403 exit with code 4; human mode prints the API message and a Try: line.

Related documentation

On this page