Sending and tracking email
Send, batch, translate, schedule and cancel mail with the `emails` commands, then follow its delivery, opens and clicks with `tracking`.
Overview
The emails namespace is the send API as commands, one for each method of openemail.emails in the SDK. Each calls one endpoint and prints what it returns. The tracking namespace reads the opens and clicks on the mail you sent. openemail email works in place of openemail emails.
Every command here needs a sign-in, from the browser or an API key, and one of two scopes: emails:send to send, translate, cancel and reschedule, and emails:read for everything that only reads.
Which send to use
openemail send is the hand-written command on the Mail page, and it sends through emails send. It is made for a person at a terminal: it picks the sending address when you leave out --from, reads the body from a file, stdin or your editor, attaches files by path, and shows a summary to confirm before anything goes. openemail emails send takes the request body as flags, one for each field, and asks nothing, which suits a script that knows exactly what it sends.
| send | emails send |
|---|---|
| --from <address> | Required, like --to, unless --data holds it. send can leave it out and pick an address for you |
| -f, --body-file <path> | No file flag for the body. Pass --html "$(cat body.html)", or the whole request in --data @email.json |
| -a, --attach <path> | --attachments, a JSON array of files, each with a filename and base64 content, or with the fileId of a file already in Files |
| --at <when> | --scheduled-at <when>, an ISO 8601 instant or a duration such as PT1H or P2D. send also takes short delays such as 10m, 2h and 1d |
| --undo <seconds> | --cancellable-for-seconds <n>, from 0 to 900 |
| --translate <language> | --translate '{"to":"de"}', which also takes from, includeOriginal and subject |
| --template <id> --props <json> | --template '{"id":"welcome","props":{"name":"Ada"}}', which can also pin a version |
| --draft <id> | --draft-id <id> |
| --thread <id> | --thread-id <id> |
| --tag <key=value> | --tags <key=value>, repeated, or a JSON object |
Only emails send has --tracking to turn opens or clicks off for one send, --signature, --headers for custom headers, --attachment-delivery to choose between attaching files and linking to them, and --data for the whole body as JSON, inline, from a file with @path or from stdin with -.
The two end differently. send exits with code 1 when the email comes back failed. emails send exits with code 0 whenever the API answered, so check status in what it prints.
Every emails command
send, send-batch, translate, cancel and reschedule need emails:send. list, get, list-events and get-tracking need emails:read. An email id is msg_ followed by 24 hex characters, as a send returns it.
| Command | What it does |
|---|---|
| openemail emails send --from <value> --to <a,b> | Send one email now, hold it for an undo window with --cancellable-for-seconds, or schedule it with --scheduled-at. The body is --html, --text or both, a stored --template, or a saved --draft-id |
| openemail emails send-batch <emails> | Send up to 100 independent emails in one request, from a JSON array in a file, inline, or on stdin with -. Each item is shaped like the body of emails send and succeeds or fails on its own |
| openemail emails translate --to <value> | Preview what a translated send would deliver, for --subject, --html or --text. Nothing is stored or sent, and it spends one AI action |
| openemail emails list | One page of sent emails, newest first, narrowed by --status, --from or --broadcast-id |
| openemail emails get <id> | One sent email with each recipient’s own status, error and delivery time, and the full tracking report when it was tracked |
| openemail emails list-events <id> | The event trail of one send, oldest first: accepted, scheduled, sent, delivered, bounced, complained, opened, clicked and the rest |
| openemail emails get-tracking <id> | The engagement report of one send: its totals, one entry per tracked copy, and every rewritten link with its clicks |
| openemail emails cancel <id> | Stop a queued or scheduled email before it goes. It asks you to confirm |
| openemail emails reschedule <id> <scheduled-at> | Move a queued or scheduled email to an ISO 8601 instant, or a duration such as PT30M, from one second to 365 days out |
Every tracking command
All five need emails:read. tracking get, list-opens and list-clicks take either id a message has: the msg_ id its send returned, or the tmsg_ tracking id that tracking list and webhook payloads carry.
| Command | What it does |
|---|---|
| openemail tracking list | One page of tracked messages sent in a window, newest first, each with its full report. --opened and --clicked narrow it, and --no-opened keeps the ones nobody opened. The window is 30 days unless --days or --minutes says otherwise |
| openemail tracking get-stats | The numbers behind an engagement panel: messages tracked, opened and clicked, open and click rates, a time series in --grain buckets, and the top links, mail clients and countries |
| openemail tracking get <id> | One message’s engagement report, the same document emails get-tracking returns |
| openemail tracking list-opens <id> | The individual opens behind a message’s open count, newest first, each marked human, proxy or machine. --include-machine adds the hits that were not counted |
| openemail tracking list-clicks <id> | The individual clicks on a message’s links, newest first, with the original url of each. --include-machine adds link scanners and collapsed repeats |
tracking list and get-stats cover every tracked message the mailbox sent, including mail written in the web app and mail sent by the MCP tools or the assistant, while emails list holds the send records the API made. A report with no send record has sendId set to null.
Examples
Send from a script with an idempotency key of your own. Running it again with the same --idempotency-key prints the first email with replayed: true instead of sending a second one.
openemail emails send \ --from 'Acme Billing <[email protected]>' \ --to [email protected] \ --subject 'Your September invoice' \ --html '<p>The invoice is attached. Tell me if anything on it looks wrong.</p>' \ --attachments '[{"fileId":"file_6bb640f5b99e47deb758f1f5"}]' \ --tracking '{"opens":false}' \ --idempotency-key invoice:inv_2026_09_4192 \ --json | jq -r '.id + " " + .status'Have a person read a translation before it goes. Send the approved wording as a plain --subject and --html, without --translate, or it is translated a second time. The translated html already holds your original below it, unless you pass --no-include-original.
openemail emails translate --to de \ --subject 'Your September invoice' \ --html "$(cat invoice.html)" \ --json > preview.jsonjq -r .html preview.jsonopenemail emails send --from [email protected] --to [email protected] \ --subject "$(jq -r .subject preview.json)" \ --html "$(jq -r .html preview.json)"Send a batch from a file. The command exits with code 0 whenever the batch was processed, even when some items failed, so read failed and each item’s status. Running it again with the same key replays the items that went and sends only the rest, as long as the array keeps its order.
[ { "from": "[email protected]", "to": "[email protected]", "subject": "Receipt 4192", "text": "Thanks for your order." }, { "from": "[email protected]", "to": "[email protected]", "subject": "Receipt 4193", "text": "Thanks for your order." }]openemail emails send-batch receipts.json --idempotency-key receipts:2026-09-27 --json > result.jsonjq '{ sent, failed }' result.jsonjq -r '.items[] | select(.status == "error") | "\(.index) \(.error.code)"' result.jsonSchedule an email, move it, and cancel it. --yes answers the confirmation cancel asks for, which a script cannot.
ID=$(openemail send --from [email protected] --to [email protected] --subject "Standup notes" \ --body-file notes.md --at 2026-10-01T09:00:00Z --json | jq -r .id)openemail emails reschedule "$ID" 2026-10-01T13:00:00Zopenemail emails get "$ID" --json | jq -r '.status + " " + .scheduledAt'openemail emails cancel "$ID" --yesFind the sends that failed and read what happened to one. Piped without --json, --all prints one JSON object per line.
openemail emails list --status failed,partial --from [email protected] --all | jq -r .idopenemail emails get msg_3f9a1c07d2b84e6a9c5b1f20openemail emails list-events msg_3f9a1c07d2b84e6a9c5b1f20 --all --json | jq -r '.items[] | .createdAt + " " + .type'Read a week of engagement in days that break at midnight UTC+2, list what nobody opened, and count the clicks on each link of one message.
openemail tracking get-stats --days 7 --offset-minutes 120 --json | jq '{ tracked, openRate, clickRate }'openemail tracking list --no-opened --days 7 --all | jq -r .subjectopenemail tracking list-clicks msg_3f9a1c07d2b84e6a9c5b1f20 --all | jq -r .url | sort | uniq -cScopes, codes and confirmations
- A browser sign-in asks for scopes on the approval page, and
openemail login --scopes emails:send,emails:readpreselects both. A command whose scope is missing stops with exit code4andinsufficient_scope, and names the scope. send --attachwith more than 5 MB of files uploads them to Files first, which also needsfiles:write.- None of these commands asks for a verification code, so a browser sign-in runs them as an API key does.
emails cancelasks before it cancels, and--yesanswers for you. Unattended without--yes, it stops withRefusing to run unattended. Pass --yes to confirm.and exit code2.emails send,send-batchandreschedulenever ask.sendshows a summary and asks only in a terminal, and--yesskips that too.--dry-runprints the request a command would send, sends nothing and exits with code0. Onemails translatethat spends no AI action, and onemails cancelit asks nothing.
Pages of results
emails list, emails list-events, tracking list, list-opens and list-clicks read one page. --limit sets its size, from 1 to 100 with 25 by default for the two emails lists, and from 1 to 200 with 50 by default for the three tracking lists. --cursor carries on from the cursor a page printed.
--allreads every page and streams the items: a table on a terminal, and one JSON object per line when piped or with--ndjson.--max <n>stops after that many items, and implies--all.--jsonprints one{ items, hasMore, nextCursor }document,--allincluded.- Paging is by cursor, not offset, so mail sent while you page never shifts or repeats a row.
Worth knowing
- Each run makes its own idempotency key, which covers the retries inside that run. Running a send twice sends twice, unless both runs pass the same
--idempotency-key. The same key with a different body is refused withidempotency_key_reuseand exit code7. - Only
queuedandscheduledmail can be cancelled or moved. An immediate send with no undo window goes out inside the request, so by the time you hold its id it is usually too late, and the call ends withemail_not_cancellableand exit code6. - A cancelled email stays cancelled. Rescheduling changes only the time, counted from when the server receives the request for a duration, so to change the text, cancel and send again.
- A translation that cannot be produced refuses the whole send, and nothing goes out untranslated. A translated batch holds at most 10 messages that carry
translate. - A spent send allowance stops a send with
send_quota_exceededuntil the first of the month, and a spent AI allowance stops a translation withai_quota_exceededuntil midnight UTC, both with exit code8. - Mail sent with an
oe_test_key is never delivered. It readssent, withtransportset totest, and is never tracked. emails get-trackingandtracking getanswer 404, exit code5, for a message that carried no pixel and no rewritten link, because not tracked is not the same as not opened. Tracking follows the setting the message was sent with, so turning it on later does not reach earlier mail.- Every count is a floor. A reader whose mail client blocks images never counts as an open, and a click is stronger evidence of reading than an open.
list-opensandlist-clicksanswer 404 for amsg_id with nothing tracked, but take atmsg_id as given, so an unknown one comes back as an empty list.- A key limited to some addresses sees only the mail sent from those addresses, and one that holds a whole domain covers every address on it.
Every flag
This page names the flags that matter most. openemail <command> --help lists every argument and flag a command takes, with its type, the scope it needs, its method and path, what it returns and the notes from the API reference. Add --json for the same help as one JSON document.
openemail emails --helpopenemail emails send --helpopenemail tracking list-opens --help --json