The analytics endpoints turn your marketing sends and their provider events into three read-only views: an account-wide overview, a per-campaign broadcast report, and a per-contact engagement score. They aggregate the same messages and events you already see under events and broadcasts — nothing new is tracked, it is read back as rates and trends. The endpoints live under /v1/analytics.
message_type: marketing. One-off transactional sends via POST /v1/send are not counted in the overview. Everything here is email; there is no other send channel.How rates are computed
Every endpoint returns a totals object of distinct-message counts and a rates object of fractions in 0..1 (so 0.3547 means 35.47%, not 35). Rates are rounded to four decimal places and clamped to [0, 1] — open, click, and delivery events are independent tallies that can briefly race, so a numerator never renders above its denominator.
The denominator is the count of messages that actually reached the provider. A message counts as sent once its status is sent, delivered, bounced, or complained — the provider accepted it. Messages that were never handed off (queued, scheduled, failed, cancelled) and sandbox test_mode traffic are excluded from the account-wide population, so they never inflate or deflate a rate.
| Rate | Numerator ÷ denominator |
|---|---|
| delivery_rate | delivered ÷ sent |
| open_rate | opened ÷ delivered (falls back to sent when no deliveries are recorded) |
| click_rate | clicked ÷ delivered (same sent fallback) |
| click_to_open_rate | clicked ÷ opened |
| bounce_rate | bounced ÷ sent |
| complaint_rate | complained ÷ sent |
| unsubscribe_rate | unsubscribed ÷ delivered (same sent fallback) |
totals are distinct messages, not raw events — a recipient who opens an email five times counts once toward opened. unsubscribed is the number of email addresses that moved to an unsubscribed state in the window, so it is tracked separately from the opens-and-clicks tallies.Account overview
GET /v1/analytics/overview aggregates all marketing traffic for your account over the last days and returns the headline numbers: totals, derived rates, a daily timeseries, a deliverability health summary, and your top_campaigns.
Query parameters
The response:
{
"days": 30,
"totals": {
"sent": 18420,
"delivered": 18100,
"opened": 6420,
"clicked": 1840,
"bounced": 220,
"complained": 8,
"unsubscribed": 42
},
"rates": {
"delivery_rate": 0.9826,
"open_rate": 0.3547,
"click_rate": 0.1017,
"click_to_open_rate": 0.2866,
"bounce_rate": 0.0119,
"complaint_rate": 0.0004,
"unsubscribe_rate": 0.0023
},
"timeseries": [
{ "date": "2026-06-14", "sent": 640, "opened": 228, "clicked": 65 }
],
"deliverability": {
"bounce_rate": 0.0119,
"complaint_rate": 0.0004,
"health": "good",
"notes": []
},
"top_campaigns": [
{
"id": "bc_a1b2c3",
"name": "October newsletter",
"sent_at": "2026-06-14T10:00:00Z",
"sent": 1840,
"delivered": 1798,
"opened": 642,
"clicked": 190,
"open_rate": 0.3571,
"click_rate": 0.1057
}
]
}Daily timeseries
timeseries is a dense, zero-filled array of one point per day, oldest to newest, over the exact same window as the totals. Each point has a UTC date (YYYY-MM-DD) and the sent, opened, and clicked counts for that day. Days with no activity are still present with zeros, so you can plot the curve without gaps.
Deliverability health
deliverability echoes bounce_rate and complaint_rate and rolls them into a single health status with human-readable notes. The thresholds mirror common ESP guidance — keep complaints under 0.1% and bounces under a few percent to protect your sender reputation.
| Health | When it fires |
|---|---|
| good | Complaints at or below 0.1% and bounces at or below 2%. No notes. |
| warning | Bounce rate is elevated — above 2%, or above 5%. A note explains which. |
| critical | Spam complaint rate is above 0.1% — the note recommends pausing and cleaning your list. |
critical complaint rate is the one to act on immediately. Mailbox providers throttle or block senders whose complaint rate stays above 0.1%, so pause the send, prune unengaged addresses, and review what triggered the complaints before you continue.Top campaigns
top_campaigns lists your most recent sent broadcasts in the window (up to 8), newest first, each with its own sent, delivered, opened, clicked counts and an open_rate / click_rate. sent_atis the broadcast's completion time (RFC 3339, UTC) and may be omitted for a campaign that has no recorded completion. Use the id to pull the full per-campaign report below.
Broadcast report
GET /v1/analytics/broadcasts/{id} drills into a single campaign. It returns the same shape of totals and rates as the overview, plus a per-campaign timeseries and a top_links click map. Because the report is scoped by broadcast_id, it includes that broadcast's own data even for a sandbox test broadcast.
Pass ?days=N to size the daily timeseries window; the headline totals and ratescover the broadcast's full lifetime regardless of the window.
{
"broadcast_id": "bc_a1b2c3",
"totals": { "sent": 1840, "delivered": 1798, "opened": 642, "clicked": 190 },
"rates": {
"delivery_rate": 0.9772,
"open_rate": 0.3571,
"click_rate": 0.1057,
"click_to_open_rate": 0.2960,
"bounce_rate": 0.0168,
"complaint_rate": 0.0011,
"unsubscribe_rate": 0
},
"timeseries": [
{ "date": "2026-06-14", "sent": 1840, "opened": 642, "clicked": 190 }
],
"top_links": [
{ "url": "https://acme.com/launch", "clicks": 120, "unique_clicks": 98 }
]
}The click map
top_links ranks the URLs clicked in this campaign (up to 20), most clicks first. Each entry carries the url, total clicks, and unique_clicks (distinct messages that clicked it). It is read from the link recorded on each click event.
top_links comes back empty and clicked stays at 0 even though the campaign delivered. See events for how open and click events are produced.Contact engagement
GET /v1/analytics/contacts/{id}returns one contact's lifetime marketing engagement: how many messages they were sent, how many they opened and clicked, their open_rate / click_rate, when they last_engaged_at, and a computed score (0..100) with a statusbucket. The contact must belong to your account; matching is by the contact's email against your marketing sends.
{
"contact_id": "ct_a1b2c3",
"sent": 24,
"opened": 18,
"clicked": 6,
"open_rate": 0.75,
"click_rate": 0.25,
"last_engaged_at": "2026-06-14T10:04:30Z",
"score": 82,
"status": "engaged"
}The engagement score
scoreblends the contact's open and click rates — weighting clicks more heavily — into a 0..100 number, then decays it by how long since they last engaged. A click-heavy contact who engaged recently scores high; a once-active contact who has gone quiet decays toward zero. last_engaged_at is the most recent open or click (RFC 3339, UTC), or absent if they have never engaged.
| Status | Meaning |
|---|---|
| engaged | Opened or clicked within the last 30 days. Full score, no decay. |
| cooling | Last engaged 30–90 days ago. Score is decayed (×0.7). |
| dormant | Last engaged more than 90 days ago. Score is decayed more heavily (×0.4). |
| never_engaged | Received marketing email but never opened or clicked. score is 0. |
| no_sends | No marketing messages have been sent to this contact yet. score is 0. |
health at good.Errors
Analytics endpoints use the standard error envelope.
| Code | Status | When it fires |
|---|---|---|
| invalid_request | 400 | A required path id (broadcast or contact) is missing or empty. |
| unauthorized | 401 | Missing or invalid API key. |
| forbidden | 403 | Key lacks the read scope these endpoints require. |
no_sends status for a contact), because there is no matching marketing traffic to aggregate.Scopes
All three analytics endpoints are read-only and require a key with the read scope. There is nothing to write here — analytics only read back the messages and events your marketing sends already produced. See Authentication for how scopes work.