Documentation menu

Libraries and tools

CLI

The tsmart command covers the whole sending API from a terminal or a CI job: send and schedule email, follow what happened to it, manage domains, API keys, webhooks, the suppression list and the request log, and set up the company's mailboxes, alternate emails and groups. It is one JavaScript file without dependencies and needs Node.js 18 or newer.

Install

Install it globally as the tsmart command:

Shell
npm install -g https://tsmart.ai/cli/tsmart-cli-1.1.0.tgz
tsmart --version

Or download the single file tsmart.mjs and run it with Node.js, without installing anything. Every example on this page then starts with node tsmart.mjs instead of tsmart.

Shell
curl -fsSLO https://tsmart.ai/cli/tsmart.mjs
node tsmart.mjs --version

The SHA-256 checksums of both downloads are published in https://tsmart.ai/cli/SHA256SUMS. The CLI works the same on Linux, macOS and Windows.

Log in

Create an API key in the console under tsmartSend → API keys (see API keys), then save it for your user:

Shell
$ tsmart login
API key (input is hidden):
Logged in with ts_AbCdEfGh… (full access).
Key saved to /home/jane/.config/tsmart/config.json

tsmart login asks for the key without showing it when it runs in a terminal. It also reads the key from standard input, or from --key (which leaves the key in your shell history, so prefer the prompt or a pipe). It checks the key with the API first: a key the API does not accept is not saved. --no-verify saves it without checking, for a computer that cannot reach the API yet.

Shell
tsmart login < key.txt                 # from a file
printf '%s' "$KEY" | tsmart login       # from a variable
tsmart login --key ts_xxxxxxxx_… --no-verify

The key is stored in a JSON file that only your user can read (file mode 0600):

SystemConfig file
Windows%APPDATA%\tsmart\config.json
Linux, macOS $XDG_CONFIG_HOME/tsmart/config.json, or ~/.config/tsmart/config.json when XDG_CONFIG_HOME is not set

Each command uses the first key it finds, in this order:

  1. the --api-key option,
  2. the TSMART_API_KEY environment variable,
  3. the key saved by tsmart login.

tsmart whoami shows which key is in use, where it comes from and what it may do. It checks the key with GET /domains: a full-access key may read it, a sending-access key is refused with restricted_api_key.

Shell
$ tsmart whoami
key:    ts_AbCdEfGh…
source: config file (/home/jane/.config/tsmart/config.json)
access: full access
api:    https://send.tsmart.ai

tsmart logout deletes the saved key. A key in TSMART_API_KEY keeps working after it.

Send an email

CLI
tsmart emails send \
  --from "Acme <hello@acme.com>" \
  --to jane@example.com \
  --subject "Your order has shipped" \
  --html-file shipped.html \
  --text "Your order is on its way." \
  --attach invoice-1042.pdf \
  --tag category=order \
  --idempotency-key order-1042/shipped
Output
Email sent.
id: 4ef9a417-02e9-4d39-ad75-9611e0fcc33c
Name Type Description
--from required address Sender on a verified domain of your company, optionally with a name: "Acme <hello@acme.com>". A sending-access key restricted to a domain can only send from that domain.
--to required address Recipient. Repeat the option or separate addresses with commas. At most 50 recipients in to, cc and bcc together.
--cc address Copy recipient; repeatable or comma-separated.
--bcc address Blind copy recipient; repeatable or comma-separated.
--reply-to address Reply-To address; repeatable or comma-separated.
--subject required text One line, up to 998 characters.
--text text Plain-text body.
--text-file path Plain-text body from a file; "-" reads standard input.
--html html HTML body. Without a text body, a text part is made from the HTML.
--html-file path HTML body from a file; "-" reads standard input. A byte order mark at the start is dropped.
--attach path Attaches a file, named after the last part of the path. Repeatable: at most 20 files and 20 MB in total.
--tag name=value Tag returned with the email and in webhook events. Name and value: ASCII letters, digits, _ and -, up to 256 characters. Repeatable, at most 20.
--header "Name: value" Extra header: X-… headers, List-Unsubscribe, List-Unsubscribe-Post, In-Reply-To or References. Repeatable, at most 30.
--scheduled-at when Send later: ISO 8601 with a time zone (2026-11-02T09:00:00+03:00) or a phrase like "in 10 min", "in 2 hours", "in 1 day". At most 30 days ahead.
--idempotency-key 1–256 chars A retry with the same key and the same email within 24 hours returns the first answer and sends nothing again.

One of --text, --text-file, --html or --html-file is needed. The command sends the API's JSON body exactly as described in POST /emails; the example above sends:

Request body
{
  "from": "Acme <hello@acme.com>",
  "to": ["jane@example.com"],
  "subject": "Your order has shipped",
  "html": "<contents of shipped.html>",
  "text": "Your order is on its way.",
  "attachments": [{ "filename": "invoice-1042.pdf", "content": "<base64 of the file>" }],
  "tags": [{ "name": "category", "value": "order" }]
}

Schedule an email, move it, and cancel it before it goes out:

CLI
tsmart emails send --from hello@acme.com --to jane@example.com \
  --subject "Your trial ends tomorrow" --text "…" --scheduled-at "in 1 day"

tsmart emails reschedule 4ef9a417-02e9-4d39-ad75-9611e0fcc33c --scheduled-at 2026-11-02T09:00:00+03:00
tsmart emails cancel 4ef9a417-02e9-4d39-ad75-9611e0fcc33c

Only a scheduled email that has not been sent yet can be rescheduled or canceled; otherwise the API answers validation_error. Inline images (attachments with a content id) need the API or an SDK; see Sending email.

Send a batch

tsmart batch send sends up to 100 emails in one request. The file holds a JSON array of email objects in the API's format (the same fields as POST /emails, without attachments); --file - reads the array from standard input.

emails.json
[
  { "from": "Acme <hello@acme.com>", "to": ["jane@example.com"], "subject": "Your receipt", "text": "Thank you, Jane." },
  { "from": "Acme <hello@acme.com>", "to": ["sam@example.com"], "subject": "Your receipt", "text": "Thank you, Sam." }
]
CLI
tsmart batch send --file emails.json --idempotency-key receipts/2026-10-12
tsmart batch send --file emails.json --validation permissive

With --validation strict (the default) one invalid email refuses the whole batch. With permissive the valid emails are sent and the others are listed with their index and the reason:

Output
1 of 2 emails accepted.
INDEX  ID
0      7c1e9a20-4b3d-4f6e-8a2c-1d9b0e3f5a7c

1 refused:
INDEX  ERROR
1      Recipients must be valid email addresses (at most 50)

Read emails and their events

CLI
tsmart emails list --limit 2
tsmart emails get 4ef9a417-02e9-4d39-ad75-9611e0fcc33c
tsmart emails events 4ef9a417-02e9-4d39-ad75-9611e0fcc33c
Output of emails list
ID                                    TO                SUBJECT                 LAST EVENT  CREATED (UTC)
4ef9a417-02e9-4d39-ad75-9611e0fcc33c  jane@example.com  Your order has shipped  delivered   2026-10-09 08:15:02
9b2d7c41-5e8a-4f0b-8c3d-2a1e6f9b0c74  sam@example.com   Reset your password     opened      2026-10-09 08:11:47
More results: tsmart emails list --after 9b2d7c41-5e8a-4f0b-8c3d-2a1e6f9b0c74

Every list command takes the same paging options. When more results exist, the command for the next page is printed on standard error.

Name Type Description
--limit 1–100 Items per page (default 20).
--after id The next page: items after this id. Lists run newest first, so these are older items.
--before id The previous page: items before this id (newer items).

tsmart emails events shows the history of one email oldest first (scheduled, sent, delivered, delivery_delayed, bounced, opened, clicked, …), so there --after means later events. tsmart emails get shows the email with its tags; the HTML and text bodies are kept for 3 days and shown in full with --json.

Metrics

tsmart metrics counts what happened to the emails sent in a time range (default: the last 7 days), in total and optionally per period and per sending domain. Rates are shown as percentages; --json returns them as numbers between 0 and 1.

CLI
tsmart metrics
tsmart metrics --start-date 2026-10-01 --end-date 2026-10-07 --timezone Europe/Istanbul \
  --dimensions period --metrics sent,delivered,bounced,delivery_rate
Name Type Description
--start-date date ISO 8601 date or date-time. Default: 7 days before the end.
--end-date date ISO 8601 date or date-time; a date alone means the whole day. Default: now.
--timezone IANA zone Zone of the dates and periods, e.g. Europe/Istanbul. Default UTC.
--granularity unit hourly, daily (default), weekly or monthly.
--metrics list sent, delivered, delivery_delayed, bounced, complained, failed, opened, clicked, suppressed, canceled, scheduled, delivery_rate, bounce_rate, open_rate, click_rate. Default: all.
--dimensions list period and/or domain.
--domain-id list Only these sending domains (up to 100 ids).

The range may span at most 366 days. Every field of the answer is described in GET /emails/metrics.

Domains

CLI
tsmart domains create acme.com
tsmart domains verify 6a0f3c1e-2b4d-4e8f-9a1b-3c5d7e9f1a2b
tsmart domains get 6a0f3c1e-2b4d-4e8f-9a1b-3c5d7e9f1a2b
tsmart domains list
tsmart domains tracking 6a0f3c1e-2b4d-4e8f-9a1b-3c5d7e9f1a2b --open on --click off
tsmart domains delete 6a0f3c1e-2b4d-4e8f-9a1b-3c5d7e9f1a2b
Output of domains create
Domain added. Publish the DNS records below, then run: tsmart domains verify 6a0f3c1e-2b4d-4e8f-9a1b-3c5d7e9f1a2b

id:             6a0f3c1e-2b4d-4e8f-9a1b-3c5d7e9f1a2b
name:           acme.com
status:         not_started
region:         eu-central
created_at:     2026-10-12 09:00:00.100000+00
sending:        disabled
receiving:      disabled
open_tracking:  false
click_tracking: false

DNS records

  Ownership (pending)
    type:     TXT
    name:     _tsmart-verify.acme.com
    value:    tsmart-verify=Q2x5c3VjY2Vzcw
    ttl:      Auto

The first verify proves ownership through the TXT record; after that, get lists the SPF, DKIM, DMARC and MX records with their status, and each later verify checks them again. The domain can send when its status is verified. Deleting a domain needs a key created by a company owner and is refused while the domain still has mailboxes. The full walkthrough is in Domains and DNS.

API keys

CLI
tsmart api-keys create --name "Web shop" --permission sending_access --domain-id 6a0f3c1e-2b4d-4e8f-9a1b-3c5d7e9f1a2b
tsmart api-keys list
tsmart api-keys rename 2b7e4c90-1f3a-4d5b-9c8e-7a6f5e4d3c2b --name "Web shop (production)"
tsmart api-keys delete 2b7e4c90-1f3a-4d5b-9c8e-7a6f5e4d3c2b

--permission is full_access (the default) or sending_access; --domain-id restricts a sending-access key to one verified domain. The new token is printed once. Deleting a key revokes it at once and cancels its scheduled emails. More in API keys.

Webhooks

CLI
tsmart webhooks create --endpoint https://example.com/hooks/tsmart \
  --events email.delivered,email.bounced
tsmart webhooks list
tsmart webhooks get 0b6a5f1e-7c2d-4e3f-8a9b-1c2d3e4f5a6b
tsmart webhooks update 0b6a5f1e-7c2d-4e3f-8a9b-1c2d3e4f5a6b --status disabled
tsmart webhooks test 0b6a5f1e-7c2d-4e3f-8a9b-1c2d3e4f5a6b
tsmart webhooks rotate-secret 0b6a5f1e-7c2d-4e3f-8a9b-1c2d3e4f5a6b
tsmart webhooks delete 0b6a5f1e-7c2d-4e3f-8a9b-1c2d3e4f5a6b

create prints the signing secret (whsec_…) that your endpoint uses to verify each request; get shows it again and rotate-secret replaces it (the old one stops working at once). The endpoint must be a public https URL; a company can have at most 10 webhooks. Event types: email.sent, email.delivered, email.delivery_delayed, email.bounced, email.failed, email.opened, email.clicked, email.scheduled, email.suppressed, domain.created, domain.updated, domain.deleted. email.complained arrives when a recipient marks an email as spam and their mailbox provider reports it (see Spam complaints). update takes --endpoint, --events (replaces the list) and --status enabled|disabled.

Follow the deliveries of a webhook and send an event again:

CLI
tsmart webhooks events 0b6a5f1e-7c2d-4e3f-8a9b-1c2d3e4f5a6b
tsmart webhooks event 0b6a5f1e-7c2d-4e3f-8a9b-1c2d3e4f5a6b msg_2abcDEF345ghiJKL678mnoPQR9
tsmart webhooks attempts 0b6a5f1e-7c2d-4e3f-8a9b-1c2d3e4f5a6b msg_2abcDEF345ghiJKL678mnoPQR9
tsmart webhooks replay 0b6a5f1e-7c2d-4e3f-8a9b-1c2d3e4f5a6b msg_2abcDEF345ghiJKL678mnoPQR9

event shows the payload; attempts lists the delivery attempts newest first with the HTTP status, error, duration and the start of the endpoint's answer. test queues an example email.sent event (at most 5 waiting test events per webhook). Test and replay are refused while the webhook is disabled. Verifying the signature is described in Webhooks.

Suppressions and logs

CLI
tsmart suppressions add jane@example.com
tsmart suppressions get jane@example.com
tsmart suppressions list
tsmart suppressions remove jane@example.com

tsmart logs list --limit 50
tsmart logs get 1d2c3b4a-5f6e-4d7c-8b9a-0f1e2d3c4b5a

Suppressions are found and removed by id or by address. Emails to a suppressed address are not sent; addresses that bounce permanently are added automatically (see Bounces and suppressions). The log lists the API requests of the last 30 days with method, endpoint and status; request bodies are not kept.

Mailboxes and alternate emails

Mailboxes can be created in the company's verified domains. A full-access key acts with the console privileges of the person who created it (see Mailboxes); deleting needs a key created by a company owner.

CLI
tsmart mailboxes create jane@acme.com --first-name Jane --last-name Doe --quota-mb 2048
tsmart mailboxes list --domain acme.com
tsmart mailboxes get jane@acme.com
tsmart mailboxes update jane@acme.com --name "Jane Smith" --quota-mb 4096
tsmart mailboxes update jane@acme.com --active off
tsmart mailboxes reset-password jane@acme.com
tsmart mailboxes delete jane@acme.com
Output of mailboxes create
Mailbox created.
email:              jane@acme.com
name:               Jane Doe
quota_mb:           2048
org_unit:           Acme (1)
temporary_password: Xy7kQ2mN9pR4sT6vW8zA

create and reset-password print a 20-character temporary password once, on standard output; the user must choose a new password at the first sign-in. The CLI does not keep it anywhere, so pass it on over a safe channel. create takes --name, or --first-name and --last-name, and optionally --quota-mb (256 to 51200, lowered to the plan's mailbox size) and --org-unit-id. update takes --name, --quota-mb, --active on|off (off suspends the mailbox: mail is still received, the user cannot sign in) and --force-password-change on|off.

Output of mailboxes list
EMAIL          NAME      STATUS     QUOTA (MB)  USED     ORG UNIT  LAST SIGN-IN (UTC)
jane@acme.com  Jane Doe  active     2048        12.4 MB  Sales     2026-10-12 08:15:02
sam@acme.com   Sam Lee   suspended  2048        0 bytes  Acme      -
More results: tsmart mailboxes list --domain acme.com --after sam@acme.com

Mailboxes are listed by address, so --after and --before take an address; --domain narrows the list to one domain. Alternate emails deliver to a mailbox, which may also send as them:

CLI
tsmart mailboxes alias-add jane@acme.com j.doe@acme.com
tsmart mailboxes aliases jane@acme.com
tsmart mailboxes alias-remove jane@acme.com j.doe@acme.com

Groups

CLI
tsmart groups create team@acme.com --name Team --member jane@acme.com,sam@acme.com
tsmart groups create all@acme.com --name "All staff" --members-file staff.txt
tsmart groups list --domain acme.com
tsmart groups get team@acme.com
tsmart groups update team@acme.com --add-member lee@acme.com --remove-member sam@acme.com
tsmart groups update team@acme.com --member jane@acme.com --member lee@acme.com
tsmart groups delete team@acme.com

Members are mailboxes or outside addresses, up to 200. --member is repeatable and takes comma-separated addresses; --members-file reads addresses separated by line breaks, commas or spaces (- reads standard input). In update, --member and --members-file replace the whole list, --add-member and --remove-member read the current list and send it back changed, and --clear-members empties it. A group without members discards its mail. Setting up many mailboxes and groups at once is described in Managing mailboxes, aliases and groups.

Output, errors and exit codes

Output is meant for people: aligned columns for lists, key: value lines for single objects, times in UTC. Add --json to any command to print the raw API response instead, for scripts:

Shell
$ tsmart emails send --from hello@acme.com --to jane@example.com --subject Hi --text Hello --json
{
  "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
}

Errors go to standard error, also with --json. An error of the API is printed as error: <name>: <message> with the name and message of the API's error object (the names are listed in Limits and errors):

Shell
$ tsmart emails send --from hello@acme.org --to jane@example.com --subject Hi --text Hello
error: invalid_from_address: The acme.org domain is not registered in this company
$ echo $?
1
Exit codeMeaning
0Success.
1 The API answered with an error (error: <name>: <message>), the network failed (error: network_error: …), or a confirmation was declined.
2 Usage error: unknown command or option, a missing argument or required option, a file that cannot be read, or no API key. Nothing is sent. The message ends with the help command to run.

A request answered with 429 is retried up to twice, after the number of seconds in its retry-after header (or ratelimit-reset, or 1 second; never more than 60), except monthly_quota_exceeded and validation_error, which do not clear within seconds. A network failure is retried only for reading commands (GET requests); a send is never repeated on its own, so use --idempotency-key when your script retries. Each request may take 30 seconds; --timeout changes that.

domains delete, api-keys delete, webhooks delete, webhooks rotate-secret, mailboxes delete, mailboxes reset-password and groups delete ask for confirmation in a terminal. Without a terminal (a script or a CI job) they need --yes.

Use in CI

In a CI job, keep the key in the CI system's secret store and expose it as the TSMART_API_KEY environment variable; no login and no config file are needed. A sending-access key restricted to one domain is the safest choice for a job that only sends.

Shell
# TSMART_API_KEY is set from the CI secret store
npm install -g https://tsmart.ai/cli/tsmart-cli-1.1.0.tgz
tsmart emails send \
  --from "Acme CI <ci@acme.com>" \
  --to team@acme.com \
  --subject "Release 4.2.0 deployed" \
  --text-file release-notes.txt \
  --idempotency-key "release-4.2.0"

The same on a Windows runner:

Windows
# $env:TSMART_API_KEY is set from the CI secret store
npm install -g https://tsmart.ai/cli/tsmart-cli-1.1.0.tgz
tsmart emails send --from ci@acme.com --to team@acme.com --subject "Nightly build passed" --text "All tests passed."
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

The exit code tells the job whether the email was accepted. The same environment variable is read by the Node.js and Python SDKs.

Options of every command

Name Type Description
--api-key key API key for this command. Default: TSMART_API_KEY, then the saved key.
--base-url url API address. Default: TSMART_BASE_URL, then https://send.tsmart.ai. Must use https (http only for localhost).
--timeout seconds Time limit of each request. Default 30.
--json flag Print the raw API response.
-h, --help flag Help for the command, with every option and examples. tsmart help <command> shows the same.

tsmart --version (or -v), given without a command, prints the version: tsmart-cli 1.1.0.

Every command of the CLI, with the API endpoint it calls:

CommandEndpoint
emails sendPOST /emails
emails get <id>GET /emails/{id}
emails listGET /emails
emails reschedule <id>PATCH /emails/{id}
emails cancel <id>POST /emails/{id}/cancel
emails events <id>GET /emails/{id}/events
batch sendPOST /emails/batch
metricsGET /emails/metrics
domains list | get | create | verify | tracking | deleteGET /domains, GET /domains/{id}, POST /domains, POST /domains/{id}/verify, PATCH /domains/{id}, DELETE /domains/{id}
api-keys list | create | rename | deleteGET /api-keys, POST /api-keys, PATCH /api-keys/{id}, DELETE /api-keys/{id}
webhooks list | get | create | update | deleteGET /webhooks, GET, POST, PATCH and DELETE /webhooks/{id}
webhooks rotate-secret | testPOST /webhooks/{id}/signing-secret/rotate, POST /webhooks/{id}/test
webhooks events | event | attempts | replayGET /webhooks/{id}/events, GET …/events/{event_id}, GET …/attempts, POST …/replay
suppressions list | get | add | removeGET /suppressions, GET /suppressions/{id or email}, POST /suppressions, DELETE /suppressions/{id or email}
logs list | getGET /logs, GET /logs/{id}
mailboxes list | get | create | update | deleteGET /mailboxes, GET /mailboxes/{email}, POST /mailboxes, PATCH /mailboxes/{email}, DELETE /mailboxes/{email}
mailboxes reset-passwordPOST /mailboxes/{email}/password
mailboxes aliases | alias-add | alias-removeGET /mailboxes/{email}/aliases, POST …/aliases, DELETE …/aliases/{address}
groups list | get | create | update | deleteGET /groups, GET /groups/{address}, POST /groups, PATCH /groups/{address} (--add-member and --remove-member read the group first), DELETE /groups/{address}
whoamiGET /domains (to tell full from sending access)

Every API command except emails send and batch send needs a full-access key; with a sending-access key the API answers restricted_api_key. login, logout, whoami and help work with any key. A full-access key acts with the console privileges of the person who created it (invalid_permission when that person may not do something).