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:
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.
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:
$ 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.
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):
| System | Config 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:
- the
--api-keyoption, - the
TSMART_API_KEYenvironment variable, - 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.
$ 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
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 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:
{
"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:
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.
[
{ "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." }
] 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:
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
tsmart emails list --limit 2
tsmart emails get 4ef9a417-02e9-4d39-ad75-9611e0fcc33c
tsmart emails events 4ef9a417-02e9-4d39-ad75-9611e0fcc33c 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.
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
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 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
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
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:
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
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.
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 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.
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:
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
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:
$ 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):
$ 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 code | Meaning |
|---|---|
0 | Success. |
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.
# 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:
# $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:
| Command | Endpoint |
|---|---|
emails send | POST /emails |
emails get <id> | GET /emails/{id} |
emails list | GET /emails |
emails reschedule <id> | PATCH /emails/{id} |
emails cancel <id> | POST /emails/{id}/cancel |
emails events <id> | GET /emails/{id}/events |
batch send | POST /emails/batch |
metrics | GET /emails/metrics |
domains list | get | create | verify | tracking | delete | GET /domains, GET /domains/{id}, POST /domains, POST /domains/{id}/verify, PATCH /domains/{id}, DELETE /domains/{id} |
api-keys list | create | rename | delete | GET /api-keys, POST /api-keys, PATCH /api-keys/{id}, DELETE /api-keys/{id} |
webhooks list | get | create | update | delete | GET /webhooks, GET, POST, PATCH and DELETE /webhooks/{id} |
webhooks rotate-secret | test | POST /webhooks/{id}/signing-secret/rotate, POST /webhooks/{id}/test |
webhooks events | event | attempts | replay | GET /webhooks/{id}/events, GET …/events/{event_id}, GET …/attempts, POST …/replay |
suppressions list | get | add | remove | GET /suppressions, GET /suppressions/{id or email}, POST /suppressions, DELETE /suppressions/{id or email} |
logs list | get | GET /logs, GET /logs/{id} |
mailboxes list | get | create | update | delete | GET /mailboxes, GET /mailboxes/{email}, POST /mailboxes, PATCH /mailboxes/{email}, DELETE /mailboxes/{email} |
mailboxes reset-password | POST /mailboxes/{email}/password |
mailboxes aliases | alias-add | alias-remove | GET /mailboxes/{email}/aliases, POST …/aliases, DELETE …/aliases/{address} |
groups list | get | create | update | delete | GET /groups, GET /groups/{address}, POST /groups, PATCH /groups/{address} (--add-member and --remove-member read the group first), DELETE /groups/{address} |
whoami | GET /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).