This article defines every metric, rate, and shared query parameter used by the Reports API. Use it as the glossary for the message activity list, the single-message and compare reports, the message contacts report, and the contact activity report; the account overview shares its error responses and unknown-parameter rule.
Overview
The Reports API returns email activity counts for the messages and contacts in your Envoke account. It is read-only and has five groups of endpoints, all on /v2:
Group | Endpoints | Article |
Message activity |
| |
Message stats |
| |
Message contacts |
| |
Contact activity |
| |
Account overview |
| Account Overview |
Every message report uses the same ten metrics, defined once in this article, so the same message gives the same numbers in the list, in its single report, and in a comparison. Message id values are the same numbers as the id of the Message API, message folders (group_id) are managed with the Group API, and the tags and subscriptions used as filters with the Tag API and Subscription API. Contact id values are the same as the id of the Contact API. The account overview does not use the ten metrics: its contact counts describe the account now, and its twelve-month email block counts records, as the Database overview does, not distinct contacts.
The Reports API replaces the beta GET https://e1.envoke.com/v1/reports/emailActivityMetrics (beta reference). The two do not count the same way: the beta counts opens and bounces as records rather than distinct contacts, its revoked_consent misses one-click unsubscribes, its start_date and end_date filter activity as activity_from and activity_to do here, and it returns every value as a string.
Counts are read from a reporting copy of the database, so activity from the last few moments may not appear yet. There is no CSV format; every response is JSON.
All requests require HTTP Basic Auth using your API ID and API Key.
Fields
The ten metrics, in the order they appear in every response. Each is an integer count of distinct contacts.
Field | Type | Read-only | Required on Create | Description |
| integer | Yes | — | Contacts the message was sent to. |
| integer | Yes | — |
|
| integer | Yes | — | Contacts with any bounce. |
| integer | Yes | — | Contacts with a hard bounce or an invalid address. |
| integer | Yes | — | Contacts with a soft bounce, deferral, rejection, or SMTP error. |
| integer | Yes | — | Contacts who opened the message. |
| integer | Yes | — | Contacts who clicked any tracked link. |
| integer | Yes | — | Contacts who revoked consent or used one-click unsubscribe from the message. |
| integer | Yes | — | Contacts who reported the message as spam. |
| integer | Yes | — | Contacts selected for the message but not sent it. |
The rates, returned only by the single-message and compare reports, are decimals rounded to four places:
Field | Type | Read-only | Required on Create | Description |
| number or null | Yes | — |
|
| number or null | Yes | — |
|
| number or null | Yes | — |
|
| number or null | Yes | — |
|
| number or null | Yes | — |
|
| number or null | Yes | — |
|
Field Details
How the counts work
Envoke records message activity as one record per message, event type, event sub-type, and contact. The sub-type is the detail of the event: the email client for an open, the link for a click, the reason for a bounce. A repeated event adds nothing: a contact who opens a message ten times in the same email client has one open record, and a contact who clicks the same link twice has one click record for that link. No count of repeated events exists, so the Reports API has no "total opens" or "total clicks" figure.
Every metric counts distinct contacts: a contact with at least one matching record counts once, however many sub-types they have. A contact who opened in two email clients counts once in opens; a contact who was deferred and then hard-bounced counts once in bounces.
Each metric is listed below with the event type and sub-type codes it reads, the same codes the Sent message report in Envoke uses. The Sent message report counts some rows differently: its Opens and Bounced rows count one per contact per sub-type, so they can be larger than opens and bounces. Each metric states exactly how it relates to the report row. The JSON paths refer to https://e1.envoke.com/messages/{id}/stats.json, the data behind that report.
Activity window. When activity_from or activity_to is given, every metric counts only the records made inside the window, by the time the activity happened (see Date parameters). A contact sent the message before the window and who opened it inside the window counts in opens but not in sends or delivered. delivered has its own windowed definition, so it is never negative.
Current-state blocks are null in a window. The single-message report's unsubscribe_reasons and polls describe contacts as they are now (the consent a contact holds today, the poll answer a contact holds today), not records made at a moment, so they cannot be limited to a window. When activity_from or activity_to is given, both are null rather than current figures beside windowed ones.
Integers, not strings. Counts and rates are JSON numbers. A rate or other decimal that is a whole value is written without a decimal point (1, 0), so read rates as floating-point numbers. IDs are strings ("2001"), as in the other Envoke APIs.
sends
Type: Integer
Codes: Type
1(Sent), any sub-type. Today every send record has sub-type11(send attempted).Counts: Distinct contacts.
Sent message report: Equal to the Sent row,
.stats[] | select(.type=="Sent") | .data.Example:
412
The number of contacts the message was handed off to for delivery. A contact sent the same automated message more than once counts once, because the send is recorded once per contact. Contacts selected but not sent (no consent, invalid address, unverified) are in skips, not here.
delivered
Type: Integer
Codes: Computed. Lifetime (no activity window):
sends−bounces. With an activity window: contacts with a send record (type1) inside the window and no bounce record (type2) for the message, whenever that bounce was recorded.Counts: Distinct contacts. Never negative.
Sent message report: Lifetime: greater than or equal to the Delivered row,
.stats[] | select(.type=="Delivered") | .data. The report subtracts its Bounced row (one per contact per bounce sub-type), so the two are equal only when no contact bounced in two ways. With an activity window there is no fixed relation: the report subtracts the window's Bounced row from the window's Sent row and can show a negative Delivered.Example:
403
Today a contact counts as delivered when it was sent the message and has no bounce record of any kind, including a temporary deferral that was later delivered. Envoke has begun recording confirmed deliveries (type 7, sub-type 11); once those records cover past messages too, delivered will switch to counting them, with and without a window, and the change will be announced before it happens.
Inside an activity window, delivered counts only contacts the message was sent to inside the window, so it is never larger than the window's sends and never negative. A bounce counts against a contact whenever it arrived: a contact sent the message on the last day of the window whose bounce arrived the next day is not delivered. The window's sends minus its bounces is therefore not this figure: a message whose bounce arrived after a window that excludes its sends has sends 0, bounces 1, and delivered 0, where the Sent message report shows Delivered -1 for the same window.
bounces
Type: Integer
Codes: Type
2, any sub-type:10SMTP error,11soft bounce,12hard bounce,13skipped for fail score (no longer written),14temporarily deferred,15rejected,16invalid email address. A few messages sent before 2015 carry bounce records with a retired sub-type outside this list: they count here, but in neitherhard_bouncesnorsoft_bouncesand in no entry of the single-message report'sbounces.Counts: Distinct contacts.
Sent message report: Less than or equal to the Bounced row,
.stats[] | select(.type=="Bounced") | .data, which counts one per contact per bounce sub-type. Equal when no contact bounced in two ways.Example:
9
Includes temporary deferrals (14), so a contact deferred and then delivered counts as bounced.
hard_bounces
Type: Integer
Codes: Type
2, sub-types12(hard bounce) and16(invalid email address).Counts: Distinct contacts.
Sent message report: Between the larger of, and the sum of, the Hard bounce and Invalid email address rows (
.stats[] | select(.type=="Hard bounce"),select(.type=="Invalid email address")). There is no single report row.Example:
5
soft_bounces
Type: Integer
Codes: Type
2, sub-types10(SMTP error),11(soft bounce),14(temporarily deferred), and15(rejected).Counts: Distinct contacts.
Sent message report: No matching row. The report shows sub-types
11(Soft bounce) and10(Other) as rows;14and15are inside its Bounced row only.Example:
4
A contact can be in both hard_bounces and soft_bounces (for example, deferred and then hard-bounced), so the two can add up to more than bounces. Sub-type 13 is in neither; it has not been written for years and appears only on old messages.
opens
Type: Integer
Codes: Type
3, any sub-type. The sub-type identifies the email client (user agent) the open came from; sub-type1is an open with no user agent and11an open recorded before user agents were tracked.Counts: Distinct contacts.
Sent message report: Less than or equal to the Opens row,
.stats[] | select(.type=="Opens") | .data, which counts one per contact per email client. Equal when no contact opened in two email clients. The report's Opens row can therefore be larger than its Sent row.opensis usually no larger thansends, but it can be: Envoke records opens and clicks for contacts with no send record for the message (see Did not open and did not click), and those contacts count inopensandclicksbut not insends.Example:
188
Opens depend on images being loaded and include opens made automatically by mail privacy and security proxies (Apple Mail Privacy Protection, and the Gmail and Yahoo image proxies); opens counts them like any other open. The single-message report's email_clients block shows how many open records Envoke attributes to Apple Mail Privacy Protection (machine_open_records), counted as records, not contacts. A contact-level machine-opens metric may be added later.
clicks
Type: Integer
Codes: Type
4, any sub-type. The sub-type is the tracked link, including the view-online link, the preferences and unsubscribe links, form links, and poll answers.Counts: Distinct contacts.
Sent message report: Equal to the Clicks row,
.stats[] | select(.type=="Clicks") | .data.Example:
57
Clicks are not limited to contacts who opened: a contact whose email client blocks images can click without an open being recorded, so click_to_open can be above 1 on a small message.
unsubscribes
Type: Integer
Codes: Type
5, sub-types11(revoked consent) and30(one-click unsubscribe).Counts: Distinct contacts.
Sent message report: Between the larger of, and the sum of, the Revoked consent row (
.stats[] | select(.type=="Revoked consent") | .data, the5/11part) and the One-click unsubscribe row (select(.type=="One-click unsubscribe"), the5/30part). A contact in both rows counts once here.Example:
4
Includes contacts who revoked consent on the preferences page reached from the message, through its unsubscribe link, and through an email client's one-click unsubscribe button. A one-click unsubscribe on a message sent with a primary subscription removes the contact from that subscription and records only sub-type 30, not 11; the beta's revoked_consent misses those contacts and unsubscribes does not. Removing a subscription on the preferences page (5/17), spam reports, and consent changes made outside the preferences page (the Envoke app, the Contact API, imports) are not unsubscribes.
spam_complaints
Type: Integer
Codes: Type
5, sub-types12(reported through Envoke's report-abuse link) and13(reported to the recipient's mailbox provider and passed back to Envoke).Counts: Distinct contacts.
Sent message report: Between the larger of, and the sum of, the Envoke spam reported and External spam reported rows.
Example:
1
skips
Type: Integer
Codes: Type
6, any sub-type:11API error,12invalid email address,13no consent to send email,14invalid parameters,15unverified email address.Counts: Distinct contacts.
Sent message report: Less than or equal to the Skipped row,
.stats[] | select(.type=="Skipped") | .data, which counts one per contact per skip reason. The report has no Skipped row for messages sent before 17 June 2014, when skips began to be recorded.Example:
6
Contacts who were in the message's recipient list when it was sent but were not sent it. They are not in sends.
Rates
Rates appear in the rates object of the single-message and compare reports, never in the list. Each is a decimal, normally between 0 and 1 (0.4665 means 46.65%), rounded to four decimal places and returned as a JSON number, so a whole value appears as 1 or 0 without a decimal point, computed from the distinct-contact counts above for the same message and window, so inside an activity window the denominators are the window's sends, opens, and windowed delivered:
Rate | Formula | Denominator |
|
| Contacts sent |
|
| Contacts delivered |
|
| Contacts delivered |
|
| Contacts who opened |
|
| Contacts sent |
|
| Contacts sent |
A rate whose denominator is 0 is null, not 0 (the Sent message report shows 0). Inside an activity window the numerator and the denominator need not be the same contacts: opens, clicks, bounces, and unsubscribes in the window can come from contacts sent the message before it, so open, click, bounce, and unsubscribe can be above 1, and a window that holds none of the message's sends gives null for every rate except click_to_open. A lifetime open or click rate can also be above 1 when opens or clicks were recorded for contacts with no send record (see opens).
Because opens counts distinct contacts and the Sent message report's Opens row counts contacts per email client, the API's open rate is less than or equal to the report's open rate. click_to_open can exceed 1 (see clicks).
Messages
Rows of the message activity list and the header of the single-message report describe the message with these fields:
Field | Type | Description |
| string | Envoke message ID; the Message API |
| string | The message's internal name. |
| string or null | The stored subject line, merge tags unreplaced. |
| string |
|
| string or null | When the message was launched, |
| boolean |
|
sent_time is the value that sent_from and sent_to filter and that the list is sorted by.
The contacts behind each metric of one message are listed by the message contacts report (event=opened for opens, and so on).
Times and time zone
Every time the Reports API returns (sent_time, last_send_time, last_open_time, last_click_time, first_open_time, first_click_time, event_time) is YYYY-MM-DD HH:MM:SS in your account's time zone, with no offset, and every date parameter is read in the same zone. The account time zone is the one set in your Envoke account preferences, for example America/Vancouver; daylight saving time applies. It is the zone the Message API uses for send_time, create_time, and modified_time and the zone the Envoke app shows, so the same moment has the same wall-clock time in all three.
The account overview is the exception: it returns no times, and its intervals use Envoke's server clock, as the Database overview does.
Envoke records the time of the first occurrence of each record: a contact's first open of a message in each email client, their first click on each link. A repeat open or click does not change any time.
Shared Query Parameters
Date parameters
Two pairs of optional parameters, used by different endpoints:
Parameter | Bounds | Used by |
| The message's | Message activity list |
| The time of each activity record: which records are counted. | Every endpoint |
Both pairs are optional and can be combined. With no
sent_*parameter, messages sent at any time are listed; with noactivity_*parameter, every count is lifetime.Each takes
YYYY-MM-DDorYYYY-MM-DD HH:MM:SS(URL-encode the space as%20). A date alone means the whole day: a_fromdate starts at00:00:00and a_todate ends at23:59:59. Both bounds are inclusive.Values are read in your account's time zone (see Times and time zone), the zone the Message API uses.
The year must be from 1970 to 2999. Any other value, including a valid date in another format (
10/24/2026,2026-10-24T09:00:00Z,yesterday), an impossible date (2026-02-30), or a year outside that range (0000-01-01,9999-12-31), returns HTTP 400:Invalid 'sent_from': '10/24/2026' (expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS), with the parameter's own name.A
_fromlater than its_tois not an error; nothing matches.
So sent_from=2026-05-01&sent_to=2026-05-31 lists the messages launched in May with their lifetime counts, and activity_from=2026-05-01&activity_to=2026-05-31 counts only May's activity.
limit and skip
Lists (the message activity list, the message contacts report, and the messages array of the contact report) page with limit and skip:
Parameter | Type | Default | Description |
| integer |
| Maximum number of items. Values above |
| integer |
| Number of items to skip. |
A limit or skip that is not a whole number returns HTTP 400 with Invalid 'limit': 'ten' or Invalid 'skip': 'abc', the same strings as the Message API. There is no page parameter, no fields or result_type, and no sort: the order is fixed, so pages never overlap. The one exception is the message contacts report, which takes fields and then lifts the cap to 10,000 (see fields and the 10,000 limit).
Unknown parameters
Each endpoint accepts only the parameters listed in its article. Any other parameter, including the beta's start_date, end_date, segment, and page, and the CRUD APIs' filter, sort, fields, and result_type, returns HTTP 400 with Parameter 'page' is not valid, naming the parameter. Only the message contacts report accepts fields.
Error Responses
Validation errors return HTTP 400 with the validation envelope. Every problem in the request is listed:
{ "success": false, "result_data": { "errors": [ "Invalid 'sent_from': '10/24/2026' (expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)", "Parameter 'page' is not valid" ] }, "result_text": "Validation failed, see 'result_data' -> 'errors'" }The validation strings shared by every endpoint:
Error | Cause |
| A parameter the endpoint does not accept. |
| A date parameter ( |
|
|
|
|
| A single-value parameter sent as an array ( |
Each endpoint's article lists its own further errors.
Query time limit and database errors. A report whose database query runs longer than 30 seconds is stopped, and the request fails with HTTP 400 and this body, which is also what any database error returns:
{ "success": false, "result_data": [], "result_text": "Invalid request" }Narrow the request and try again: a shorter activity window, fewer messages or a smaller folder in a comparison, or a smaller page of contacts.
Path not found. A message or contact ID in the path that is not a number (/v2/reports/messages/abc) and any path that is not a Reports API endpoint return HTTP 404 with this body:
{ "success": false, "result_data": [], "result_text": "404 Not Found" }The body is JSON but is sent with Content-Type: text/html, as on the beta.
Authentication and limits. An invalid API ID or key returns 401 with the body {"success":false,"result_data":[],"result_text":"Invalid credentials"}, sent with Content-Type: text/html like the 404. A valid key on a closed or inactive account returns 403 with the JSON body {"success":false,"result_data":[],"result_text":"This account has been closed."}, or the same body with This account is not active. Exceeding your account's API limit returns 429 with success: false and a result_text naming the limit. The contact activity endpoints also return 403, with a JSON body, on an account restricted from seeing contact-level email activity; see Contact Activity errors. The message contacts report returns the same 403 for every event; see Message Contacts errors. The account overview is not restricted.
Not found
Not-found behaviour varies by endpoint:
GET
/reports/messages— a filter that matches nothing returns[]with HTTP 200. Amessage_idthat does not exist, belongs to another account, or is deleted is simply absent.GET
/reports/messages/{id}— a message that does not exist, belongs to another account, is deleted, or has not started sending returns[]with HTTP 200.GET
/reports/messages/compare— anymessage_idthat is not a sent or automated message in your account returns HTTP 400 listing each one:No message found for 'message_id': 2099. An unknowngroup_idreturnsNo message group found for 'group_id': 39.GET
/reports/messages/{id}/contacts— a message that does not exist, belongs to another account, is deleted, or has not started sending returns[]with HTTP 200, as does an event no contact has.GET
/reports/contacts/{id}— a contact that does not exist, belongs to another account, or is deleted returns[]with HTTP 200.GET
/reports/contacts— anemailorremote_idthat matches no contact returns[]with HTTP 200.GET
/reports/overview— always an object; atag_idorsubscription_idthat is not a tag or subscription of your account returns HTTP 400 listing each one:No tag found for 'tag_id': 599(see Account Overview errors).
For code examples and interactive testing, visit api.envoke.com.
