Reports API - Message Stats
Use the message stats reports to retrieve the full report for one message (totals, rates, bounce and preference breakdowns, links, email clients, unsubscribe reasons, polls, and A/B variants), or to compare several messages or a whole folder side by side with a combined total.
Endpoints
Section titled “Endpoints”| Method | URL | Description |
|---|---|---|
| GET | https://e1.envoke.com/v2/reports/messages/{ID} |
Return the full report for one message. |
| GET | https://e1.envoke.com/v2/reports/messages/compare |
Return totals and rates for several messages, or for every message in a folder, plus a combined block. |
Authentication is via HTTP Basic Auth using your API ID and API Key.
Get a Single Message Report
Section titled “Get a Single Message Report”GET https://e1.envoke.com/v2/reports/messages/{ID}Returns one JSON object: the message, its ten metrics in totals, its six rates in rates, and the breakdowns below. Replace {ID} with the Envoke message ID. The totals are the same numbers the message activity list returns for the message with the same activity window. To list the contacts behind a total, a bounce reason, or a link, use the message contacts report.
The report is available for sent campaigns, automated messages, and campaigns that have started sending. A message that does not exist, belongs to another account, is deleted, or has not started sending (a draft or a scheduled message) returns [] with HTTP 200.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
activity_from |
string | — | Count only activity on or after this date or time. See Date parameters. |
activity_to |
string | — | Count only activity on or before this date or time. |
Every count in the report, including the breakdowns, links, and email_clients, uses the window. unsubscribe_reasons and polls describe contacts as they are now and cannot be limited to a window, so when activity_from or activity_to is given both are null (see below). Any other parameter, including sent_from and sent_to, returns HTTP 400 (Parameter 'sent_from' is not valid).
Response fields
Section titled “Response fields”| Field | Type | Description |
|---|---|---|
id, name, subject, type, sent_time, has_variants |
As in the message activity list. | |
totals |
object | The ten metrics. |
rates |
object | The six rates. |
bounces |
array | Contacts per bounce reason. |
preference_changes |
array | Contacts per preference change. |
links |
array | Contacts per tracked link. |
email_clients |
object or null | Open records (not contacts) per email client. null for messages sent before June 2022. |
unsubscribe_reasons |
array or null | Contacts per reason given when unsubscribing. null when an activity window is given. |
polls |
array or null | Answers to each poll in the message; [] when the message has none. null when an activity window is given. |
variants |
array | One object per A/B version, always present when has_variants is true; [] when the message is not an A/B test, and when its A/B block no longer exists. |
bounces
Section titled “bounces”One entry per bounce reason, always all six, in this order. contacts is the number of distinct contacts with that reason:
type |
label |
Codes |
|---|---|---|
hard_bounce |
Hard bounce |
2/12 |
soft_bounce |
Soft bounce |
2/11 |
deferred |
Temporarily deferred |
2/14 |
rejected |
Rejected |
2/15 |
invalid_address |
Invalid email address |
2/16 |
other |
Other |
2/10 (SMTP error) and the retired 2/13 |
A contact with two bounce reasons is in two entries, so the entries can add up to more than totals.bounces. On a few messages sent before 2015, bounce records with a retired sub-type count in totals.bounces but appear in no entry, and no bounce_type of the message contacts report selects them. hard_bounces covers hard_bounce and invalid_address; soft_bounces covers soft_bounce, deferred, rejected, and the SMTP-error part of other. The type values are stable keys; label is English display text and may change.
The contacts behind an entry: message contacts with event=bounced&bounce_type={type}.
preference_changes
Section titled “preference_changes”One entry per kind of preference change made from the message, always all six, in this order:
type |
label |
Codes |
|---|---|---|
revoked_consent |
Revoked consent |
5/11 |
one_click_unsubscribe |
One-click unsubscribe |
5/30 |
spam_reported |
Spam reported |
5/12 and 5/13 |
subscription_changed |
Subscription changed |
5/17 |
language_changed |
Language changed |
5/16 |
express_consent_granted |
Express consent granted |
5/21 |
totals.unsubscribes is the distinct contacts across revoked_consent and one_click_unsubscribe; totals.spam_complaints equals spam_reported. Each entry equals the Sent message report row with the same label, except spam_reported, which combines two report rows. A one-click unsubscribe on a message sent with a primary subscription removes the contact from that subscription but records only one_click_unsubscribe, not subscription_changed. Nurture campaign changes (5/18) are not reported.
The contacts behind revoked_consent and one_click_unsubscribe together: message contacts with event=unsubscribed; behind spam_reported: event=spam_reported. The other three entries have no contact list.
One entry per tracked link in the message with at least one click (inside the window, when one is given), most clicked first; a link nobody clicked has no entry:
| Field | Type | Description |
|---|---|---|
id |
string | The tracked link’s ID. |
url |
string | The link as written in the message, with Envoke’s tracking parameters (utm_*, eid) removed; "" for the view-online and preferences links. Merge tags are not replaced, so url can be or contain a merge tag ({@customfield-url_test}) rather than a valid URL. |
title |
string | The link’s anchor text, or a shortened URL when it has none, cut at 60 bytes (fewer characters when the text has accented or other non-ASCII characters); the view-online and preferences links have a fixed title. |
type |
string | link for a link in the content, including links to pages with Envoke forms; view for the view-online link; pref for the preferences and unsubscribe links. |
clicks |
integer | Distinct contacts who clicked this link. |
A contact who clicked two links is in both entries, so the entries can add up to more than totals.clicks. Poll answers are tracked as clicks and counted in totals.clicks, but are reported under polls, not here. When the same URL appears twice in a message, each copy has its own entry; the Sent message report merges them into one row whose count is the sum of the entries.
The contacts behind an entry: message contacts with event=clicked&link_id={id}.
email_clients
Section titled “email_clients”The email client panel of the Sent message report, from the open records’ user agents. Every figure in this block counts open records, one per contact per email client, not distinct contacts, so known_opens can be larger than totals.opens. It is the only part of the report that counts records rather than contacts.
| Field | Type | Description |
|---|---|---|
known_opens |
integer | Open records whose email client was recorded. |
machine_open_records |
integer | Open records made by Apple Mail Privacy Protection (user agent Mozilla/5.0), which Envoke classifies as machine opens. A contact has at most one such record per message, so this also equals the number of contacts with at least one machine open, including contacts who also opened in another email client. There is no machine-opens figure in totals. |
by_app |
array | { "name", "opens" } per email client, always five entries: Apple Mail (including the machine-open records), Gmail, Outlook, and Yahoo, most opens first (ties in that order), then Other for every client Envoke does not recognise, always last and 0 when every client was recognised. |
email_clients is null for a message sent before 1 June 2022, when Envoke began recording user agents. Opens recorded with no user agent are in neither known_opens nor by_app, so known_opens can be smaller than the report’s Opens row.
unsubscribe_reasons
Section titled “unsubscribe_reasons”One entry per reason, in the order the preferences page lists them, for reasons at least one contact chose:
| Field | Type | Description |
|---|---|---|
reason |
string | no_longer_interested, never_subscribed, too_many_emails, irrelevant, or other. |
contacts |
integer | Contacts whose current consent was revoked from this message with this reason. |
This counts contacts as they are now: a contact who unsubscribed from this message and later gave consent again is no longer counted. Because it describes the present rather than activity at a time, unsubscribe_reasons is null when activity_from or activity_to is given. Giving a reason is optional, so the entries usually add up to less than totals.unsubscribes.
One entry per poll in the message; [] when it has none, and null when an activity window is given.
| Field | Type | Description |
|---|---|---|
id |
string | The poll’s ID. |
name |
string | The poll question. |
anonymous |
boolean | true for an anonymous poll. The counts are still given; the contacts are not identifiable in Envoke. |
answers |
array | { "id", "text", "contacts" } per answer, in the order the Sent message report lists them: by the answer’s sort position, then alphabetically by text. Envoke stores poll answers without a sort position, so this is normally alphabetical, not the order of the answers in the message. contacts is the number of contacts who currently hold that answer. |
Poll answers are stored on the contact, so the counts describe contacts as they are now, not activity: every contact in the account who holds the answer today is counted, however they came to hold it. For that reason polls is null when activity_from or activity_to is given.
A/B tests
Section titled “A/B tests”For an A/B test, has_variants is true, the message’s subject is null for a subject-line test, totals and rates cover every version together (including the contacts sent the winning version after the test), and variants has one object per version, each with its own subject or sender. variants is always included when has_variants is true; there is no parameter to request it or leave it out. It is [] when the message’s A/B block no longer exists, although has_variants is still true. For a test that does not vary the subject (preheader, sender, or content), subject is the stored subject, which can be the literal placeholder {@ab_test} (see Messages).
| Field | Type | Description |
|---|---|---|
id |
string | The version’s ID. |
name |
string | A, B, C, … in the order of the versions, as in the Sent message report’s tabs. |
test_type |
string | What the test varies: subject, preheader, sender, or content. |
subject |
string or null | This version’s subject line, for a subject test; otherwise null. |
sender |
string or null | This version’s sender as Name <email>, for a sender test; otherwise null. |
winner |
boolean | true for the version Envoke chose as the winner; false for every version while the test is running, and when the test ended without a winner being recorded. |
totals |
object | The ten metrics for the contacts who were sent this version during the test. |
rates |
object | The six rates for those contacts. |
Each version counts the contacts sent that version, as the Sent message report’s version tabs do: the contacts assigned to it during the test phase and, for the winning version, the contacts sent the winner after the test. So the winner’s sends is usually the larger figure, and the versions’ sends usually add up to totals.sends. They can add up to less, because contacts sent the message outside the versions’ assignment (overrides and resends, for example) are in totals but in no version, and to more on old tests where a contact was recorded under two versions. A version’s metrics relate to the Version A or Version B tab of the Sent message report (.variant_stats[n].stats in stats.json) exactly as the message’s totals relate to the Combined tab: sends and clicks are equal, opens and bounces are less than or equal, and so on per the glossary.
Response
Section titled “Response”{ "id": "2004", "name": "Pirates Opening Night", "subject": null, "type": "campaign", "sent_time": "2026-05-14 09:30:00", "has_variants": true, "totals": { "sends": 386, "delivered": 379, "bounces": 7, "hard_bounces": 4, "soft_bounces": 3, "opens": 201, "clicks": 64, "unsubscribes": 2, "spam_complaints": 0, "skips": 3 }, "rates": { "delivered": 0.9819, "open": 0.5303, "click": 0.1689, "click_to_open": 0.3184, "bounce": 0.0181, "unsubscribe": 0.0052 }, "bounces": [ { "type": "hard_bounce", "label": "Hard bounce", "contacts": 3 }, { "type": "soft_bounce", "label": "Soft bounce", "contacts": 1 }, { "type": "deferred", "label": "Temporarily deferred", "contacts": 1 }, { "type": "rejected", "label": "Rejected", "contacts": 0 }, { "type": "invalid_address", "label": "Invalid email address", "contacts": 1 }, { "type": "other", "label": "Other", "contacts": 1 } ], "preference_changes": [ { "type": "revoked_consent", "label": "Revoked consent", "contacts": 1 }, { "type": "one_click_unsubscribe", "label": "One-click unsubscribe", "contacts": 1 }, { "type": "spam_reported", "label": "Spam reported", "contacts": 0 }, { "type": "subscription_changed", "label": "Subscription changed", "contacts": 1 }, { "type": "language_changed", "label": "Language changed", "contacts": 0 }, { "type": "express_consent_granted", "label": "Express consent granted", "contacts": 3 } ], "links": [ { "id": "88125", "url": "https://example.org/tickets/pirates", "title": "Get your tickets", "type": "link", "clicks": 39 }, { "id": "88126", "url": "https://example.org/shows/pirates-of-penzance", "title": "About the show", "type": "link", "clicks": 18 }, { "id": "88129", "url": "", "title": "Envoke Preferences / Unsubscribe", "type": "pref", "clicks": 4 }, { "id": "88128", "url": "", "title": "Envoke View Message in Browser", "type": "view", "clicks": 3 } ], "email_clients": { "known_opens": 214, "machine_open_records": 47, "by_app": [ { "name": "Apple Mail", "opens": 88 }, { "name": "Gmail", "opens": 64 }, { "name": "Outlook", "opens": 41 }, { "name": "Yahoo", "opens": 7 }, { "name": "Other", "opens": 14 } ] }, "unsubscribe_reasons": [ { "reason": "too_many_emails", "contacts": 1 } ], "polls": [ { "id": "61", "name": "Which performance will you attend?", "anonymous": false, "answers": [ { "id": "7301", "text": "Friday 7:30 p.m.", "contacts": 48 }, { "id": "7302", "text": "Saturday matinee", "contacts": 31 }, { "id": "7303", "text": "Saturday 7:30 p.m.", "contacts": 22 } ] } ], "variants": [ { "id": "5121", "name": "A", "test_type": "subject", "subject": "Opening night: The Pirates of Penzance", "sender": null, "winner": false, "totals": { "sends": 96, "delivered": 94, "bounces": 2, "hard_bounces": 1, "soft_bounces": 1, "opens": 49, "clicks": 14, "unsubscribes": 1, "spam_complaints": 0, "skips": 0 }, "rates": { "delivered": 0.9792, "open": 0.5213, "click": 0.1489, "click_to_open": 0.2857, "bounce": 0.0208, "unsubscribe": 0.0104 } }, { "id": "5122", "name": "B", "test_type": "subject", "subject": "Curtain up on Pirates, {fname}!", "sender": null, "winner": true, "totals": { "sends": 97, "delivered": 95, "bounces": 2, "hard_bounces": 1, "soft_bounces": 1, "opens": 58, "clicks": 19, "unsubscribes": 0, "spam_complaints": 0, "skips": 0 }, "rates": { "delivered": 0.9794, "open": 0.6105, "click": 0.2, "click_to_open": 0.3276, "bounce": 0.0206, "unsubscribe": 0 } } ]}Errors
Section titled “Errors”| Response | Cause |
|---|---|
[] with HTTP 200 |
The message does not exist, belongs to another account, is deleted, or has not started sending. |
HTTP 400, Parameter 'sent_from' is not valid |
A parameter other than activity_from and activity_to. |
HTTP 400, Invalid 'activity_from': '...' (expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS) |
A bad date (see Date parameters). |
HTTP 404, 404 Not Found |
{ID} is not a number. |
Compare Messages
Section titled “Compare Messages”GET https://e1.envoke.com/v2/reports/messages/compareReturns the totals and rates of several messages side by side, and a combined block for the set. Give the set either as message IDs or as a folder:
message_id[]=2001&message_id[]=2004— the listed messages. Each must be a sent campaign or an automated message in your account. A repeated ID counts once: the message appears once andmessage_countcounts it once.group_id=31— every sent campaign and automated message filed in the folder or in any of its subfolders, at any depth, as in the folder report in Envoke.
The per-message totals and rates are the same numbers the single report and the message activity list return for each message with the same activity window.
With an activity window, messages launched after activity_to are left out of the set, exactly as the list leaves them out: a folder’s messages launched later are in neither messages nor combined, and a message_id launched later is dropped without an error.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
message_id |
integer or array | — | One or more message IDs, at most 100. Not with group_id. |
group_id |
integer | — | One message folder ID; its subfolders are included. Not with message_id. |
activity_from |
string | — | Count only activity on or after this date or time. |
activity_to |
string | — | Count only activity on or before this date or time. |
Exactly one of message_id and group_id is required. Any other parameter returns HTTP 400.
The combined block
Section titled “The combined block”combined.totals counts distinct contacts across the whole set: a contact sent two of the messages counts once in sends, a contact who opened both counts once in opens. So combined.totals.sends is the number of different people the set reached, and it is less than or equal to the sum of the messages’ sends. The same holds for every metric:
| Metric | Combined value |
|---|---|
sends, bounces, hard_bounces, soft_bounces, opens, clicks, unsubscribes, spam_complaints, skips |
Contacts with a matching record for any message in the set. |
delivered |
Lifetime: combined sends − combined bounces. With an activity window: contacts who, for at least one message in the set, were sent it inside the window and have no bounce for it (see delivered); never negative. |
combined.rates are computed from combined.totals with the formulas in Rates, so they are rates of the set’s reach, not averages of the messages’ rates. combined.message_count is the number of messages in the set, after the launch cut-off when an activity window is given.
A/B tests count as one message each; their versions are not shown.
Response
Section titled “Response”messages is in the same order as the message activity list: newest sent_time first, then highest id.
{ "messages": [ { "id": "2004", "name": "Pirates Opening Night", "sent_time": "2026-05-14 09:30:00", "totals": { "sends": 386, "delivered": 379, "bounces": 7, "hard_bounces": 4, "soft_bounces": 3, "opens": 201, "clicks": 64, "unsubscribes": 2, "spam_complaints": 0, "skips": 3 }, "rates": { "delivered": 0.9819, "open": 0.5303, "click": 0.1689, "click_to_open": 0.3184, "bounce": 0.0181, "unsubscribe": 0.0052 } }, { "id": "2001", "name": "Spring Season Announcement", "sent_time": "2026-04-15 10:00:00", "totals": { "sends": 412, "delivered": 403, "bounces": 9, "hard_bounces": 5, "soft_bounces": 4, "opens": 188, "clicks": 57, "unsubscribes": 4, "spam_complaints": 1, "skips": 6 }, "rates": { "delivered": 0.9782, "open": 0.4665, "click": 0.1414, "click_to_open": 0.3032, "bounce": 0.0218, "unsubscribe": 0.0097 } } ], "combined": { "message_count": 2, "totals": { "sends": 702, "delivered": 686, "bounces": 16, "hard_bounces": 9, "soft_bounces": 7, "opens": 331, "clicks": 108, "unsubscribes": 6, "spam_complaints": 1, "skips": 9 }, "rates": { "delivered": 0.9772, "open": 0.4825, "click": 0.1574, "click_to_open": 0.3263, "bounce": 0.0228, "unsubscribe": 0.0085 } }}Here 96 contacts were sent both messages, so combined sends is 702, not 798, and the combined open rate (0.4825) is not the average of the two messages’ rates.
A folder that exists but holds no sent or automated messages returns messages [] and a combined block with message_count 0, every total 0, and every rate null.
Errors
Section titled “Errors”Invalid requests return HTTP 400 with the validation envelope. Unknown message IDs are all listed:
{ "success": false, "result_data": { "errors": [ "No message found for 'message_id': 2098", "No message found for 'message_id': 2099" ] }, "result_text": "Validation failed, see 'result_data' -> 'errors'"}| Error | Cause |
|---|---|
One of the parameters 'message_id' or 'group_id' must be provided |
Neither was given. |
Only one of the parameters 'message_id' or 'group_id' may be provided |
Both were given. |
No message found for 'message_id': 2099 |
The message does not exist, belongs to another account, is deleted, or is not a sent campaign or automated message. One error per ID. |
No message group found for 'group_id': 39 |
The folder does not exist or belongs to another account. |
Too many messages to compare: 140 (maximum 100) |
More than 100 message IDs, or a folder holding, with its subfolders, more than 100 sent and automated messages (counted before the activity window’s launch cut-off). |
Invalid 'message_id': 'abc' |
A message_id or group_id value that is not a whole number. |
Parameter 'group_id' accepts one value |
group_id sent as an array. |
Parameter 'sent_from' is not valid |
Any parameter other than those above. |
The shared date errors also apply.
Examples
Section titled “Examples”Full report for one message
Section titled “Full report for one message”curl -X GET -gs -u "$API_ID:$API_KEY" \ "https://e1.envoke.com/v2/reports/messages/2004"One message’s activity in its first week
Section titled “One message’s activity in its first week”curl -X GET -gs -u "$API_ID:$API_KEY" \ "https://e1.envoke.com/v2/reports/messages/2004?activity_from=2026-05-14&activity_to=2026-05-20"With a window, unsubscribe_reasons and polls are null; every other part of the report counts only that week’s activity.
Compare two messages
Section titled “Compare two messages”curl -X GET -gs -u "$API_ID:$API_KEY" \ "https://e1.envoke.com/v2/reports/messages/compare?message_id%5B%5D=2001&message_id%5B%5D=2004"Every message in a folder, May activity only
Section titled “Every message in a folder, May activity only”curl -X GET -gs -u "$API_ID:$API_KEY" \ "https://e1.envoke.com/v2/reports/messages/compare?group_id=31&activity_from=2026-05-01&activity_to=2026-05-31"A message that is not found
Section titled “A message that is not found”curl -X GET -gs -u "$API_ID:$API_KEY" \ "https://e1.envoke.com/v2/reports/messages/2099"Response:
[]Note: The curl examples above are formatted for readability. All requests must use properly encoded URLs.
Full reference, OpenAPI description, and Postman collection: developers.envoke.com.

