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 --helpThe 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 shRegenerate 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 doctorThe 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> --jsonInline 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 customersBoth 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 offsamva 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 acmepull 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.tscheck 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-2add-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> --yescreate 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 -iPrefer 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.
| Flag | Effect |
|---|---|
-X, --method | Choose the HTTP method. The default is GET, or POST once a field or --input is given. |
-f, --raw-field key=value | Add a string field. |
-F, --field key=value | Add a typed field: true, false, null, and integers become JSON values. @path reads the value from a file and @- from stdin. |
--input file | Send a file, or - for stdin, as the request body. Fields then go to the query string. |
-H, --header key:value | Add a request header. |
--paginate, --slurp | Follow nextCursor and print each page on its own line, or all pages as one JSON array. |
-i, --include | Print the status line and response headers before the body. |
--silent, --verbose | Suppress 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.
| Code | Meaning |
|---|---|
0 | Success |
1 | API, operational, or terminal delivery failure |
2 | Command syntax, validation, configuration, or confirmation failure |
4 | Authentication or organization-context failure, including an expired session |
124 | Wait or credential timeout |
130 | Interrupted with SIGINT |
143 | Terminated 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
Authentication
Choose a credential for interactive work or automation.
Send an email
Use the same send model from the SDK or REST API.
Connect an agent over MCP
Let an AI agent operate Samva through tools.
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
TypeScript SDK
Reference for the samva TypeScript SDK, covering client setup, the email send facade, every service namespace, errors, and the native Effect client.
Connect an agent over MCP
Point an authenticated AI agent at Samva's hosted Model Context Protocol server for organization-scoped email tools.