Open tracking and rates
Open tracking is optional and off unless you ask for it per message. Read the trade-offs before you use it.
Trade-offs#
- Deliverability. Tracking pixels are a signal spam filters look at, and mailbox providers and deliverability guides advise against them for mail that must reach the inbox. Leave tracking off for transactional and cold mail unless you need it.
- Apple Mail Privacy Protection and some webmail image proxies download images ahead of the reader. Those opens are inflated or fake. Known proxies are flagged with
via_proxy: trueon a best-effort basis (it is a heuristic, not a guarantee). - An open is not proof of delivery or reading. Clients that block images never fire it, and one pixel is injected per message, not per recipient, so you cannot tell which recipient opened it.
Sending with tracking#
Add track_opens: true to send, reply, reply-all, forward, draft create/update. Drafts and scheduled sends keep the flag.
curl -X POST "$BASE_URL/v0/inboxes/$INBOX/messages/send" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"to":"a@example.com","subject":"Hi","html":"<p>Hello</p>","track_opens":true}'
- An
htmlbody is required, otherwise422 html_required. - If the operator turned the feature off (
OPEN_TRACKING_ENABLED=false) the request fails with422 tracking_disabled. - The pixel
<img src=".../t/o/<token>.gif" width="1" height="1" alt="" style="display:none">is added to the outgoing HTML just before</body>, or at the end. The stored message keeps your original HTML and getstracked: true. - The pixel URL base is
TRACKING_BASE_URL, falling back toPUBLIC_URL.
What happens on an open#
GET /t/o/<token>.gif is public and always answers 200 with a 43-byte transparent GIF and Cache-Control: no-store, even for invalid tokens. The token is the message id plus an HMAC, so it cannot be guessed or forged. Nothing is written for invalid tokens.
For a valid token the server stores a counter (capped at 1000), the first and last open time and whether the request looked like a proxy. It never stores the IP address or the user agent. Opens made with a dashboard session of the sending organization are not counted.
On the first open the message and its thread get the opened label and a message.opened webhook / WebSocket event is emitted once.
Rates#
GET /v0/metrics/rates, /v0/pods/:pod_id/metrics/rates and /v0/inboxes/:inbox_id/metrics/rates need the metrics_read permission.
| Query | Meaning |
|---|---|
rate_types | bounce, complaint, open (comma separated or repeated). Default: bounce and complaint, plus open when the scope has tracked messages. |
start, end | Range. start must be within the last 90 days. Default: the last 24 hours. |
period | Seconds between points, 60 to 86400. Default 3600. |
window | Trailing seconds each point looks back, a whole multiple of period, at most 2592000. Default 86400. |
limit, descending | Number of points; reverse order. |
The range plus the window divided by period must not exceed 1000 buckets, otherwise 400.
The response maps each type to points { timestamp, sent, rate }. For a point at time t the window is (t - window, t]. sent counts messages per recipient; rate is bounced (or complained) recipients divided by sent, as a fraction (0.05 is 5%), and null when sent is 0. For open, sent is the number of tracked messages sent in the window and rate is the share of them that have been opened.