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: true on 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 html body is required, otherwise 422 html_required.
  • If the operator turned the feature off (OPEN_TRACKING_ENABLED=false) the request fails with 422 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 gets tracked: true.
  • The pixel URL base is TRACKING_BASE_URL, falling back to PUBLIC_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.

QueryMeaning
rate_typesbounce, complaint, open (comma separated or repeated). Default: bounce and complaint, plus open when the scope has tracked messages.
start, endRange. start must be within the last 90 days. Default: the last 24 hours.
periodSeconds between points, 60 to 86400. Default 3600.
windowTrailing seconds each point looks back, a whole multiple of period, at most 2592000. Default 86400.
limit, descendingNumber 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.