Skip to content

Reports API - Fields Reference

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 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 GET https://e1.envoke.com/v2/reports/messages Message Activity
Message stats GET https://e1.envoke.com/v2/reports/messages/{id} and GET https://e1.envoke.com/v2/reports/messages/compare Message Stats
Message contacts GET https://e1.envoke.com/v2/reports/messages/{id}/contacts Message Contacts
Contact activity GET https://e1.envoke.com/v2/reports/contacts/{id} and GET https://e1.envoke.com/v2/reports/contacts Contact Activity

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 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.


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
sends integer Yes — Contacts the message was sent to.
delivered integer Yes — sends minus bounces; inside an activity window, contacts sent the message in the window who have no bounce for it. Never negative.
bounces integer Yes — Contacts with any bounce.
hard_bounces integer Yes — Contacts with a hard bounce or an invalid address.
soft_bounces integer Yes — Contacts with a soft bounce, deferral, rejection, or SMTP error.
opens integer Yes — Contacts who opened the message.
clicks integer Yes — Contacts who clicked any tracked link.
unsubscribes integer Yes — Contacts who revoked consent or used one-click unsubscribe from the message.
spam_complaints integer Yes — Contacts who reported the message as spam.
skips 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
delivered number or null Yes — delivered ÷ sends.
open number or null Yes — opens ÷ delivered.
click number or null Yes — clicks ÷ delivered.
click_to_open number or null Yes — clicks ÷ opens.
bounce number or null Yes — bounces ÷ sends.
unsubscribe number or null Yes — unsubscribes ÷ sends.

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.


  • Type: Integer
  • Codes: Type 1 (Sent), any sub-type. Today every send record has sub-type 11 (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.


  • Type: Integer
  • Codes: Computed. Lifetime (no activity window): sends − bounces. With an activity window: contacts with a send record (type 1) inside the window and no bounce record (type 2) 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.


  • Type: Integer
  • Codes: Type 2, any sub-type: 10 SMTP error, 11 soft bounce, 12 hard bounce, 13 skipped for fail score (no longer written), 14 temporarily deferred, 15 rejected, 16 invalid email address. A few messages sent before 2015 carry bounce records with a retired sub-type outside this list: they count here, but in neither hard_bounces nor soft_bounces and in no entry of the single-message report’s bounces.
  • 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.


  • Type: Integer
  • Codes: Type 2, sub-types 12 (hard bounce) and 16 (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

  • Type: Integer
  • Codes: Type 2, sub-types 10 (SMTP error), 11 (soft bounce), 14 (temporarily deferred), and 15 (rejected).
  • Counts: Distinct contacts.
  • Sent message report: No matching row. The report shows sub-types 11 (Soft bounce) and 10 (Other) as rows; 14 and 15 are 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.


  • Type: Integer
  • Codes: Type 3, any sub-type. The sub-type identifies the email client (user agent) the open came from; sub-type 1 is an open with no user agent and 11 an 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. opens is usually no larger than sends, 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 in opens and clicks but not in sends.
  • 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.


  • 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.


  • Type: Integer
  • Codes: Type 5, sub-types 11 (revoked consent) and 30 (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, the 5/11 part) and the One-click unsubscribe row (select(.type=="One-click unsubscribe"), the 5/30 part). 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.


  • Type: Integer
  • Codes: Type 5, sub-types 12 (reported through Envoke’s report-abuse link) and 13 (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

  • Type: Integer
  • Codes: Type 6, any sub-type: 11 API error, 12 invalid email address, 13 no consent to send email, 14 invalid parameters, 15 unverified 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 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
delivered delivered ÷ sends Contacts sent
open opens ÷ delivered Contacts delivered
click clicks ÷ delivered Contacts delivered
click_to_open clicks ÷ opens Contacts who opened
bounce bounces ÷ sends Contacts sent
unsubscribe unsubscribes ÷ sends 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).


Rows of the message activity list and the header of the single-message report describe the message with these fields:

Field Type Description
id string Envoke message ID; the Message API id.
name string The message’s internal name.
subject string or null The stored subject line, merge tags unreplaced. null for a subject-line A/B test, whose versions have different subjects: has_variants is true and each version’s subject is in the variants of the single-message report. (The Message API returns the stored placeholder {@ab_test} for the same message.) An A/B test of the preheader, sender, or content has one subject and returns it. The stored subject is returned as is, so the Reports API also returns the literal placeholder {@ab_test} when that is the stored subject and the message’s A/B block does not vary the subject (a preheader, sender, or content test), or no A/B block remains. Also null, with has_variants false, when the message has no stored subject (old messages without a content row), as the Message API returns it.
type string campaign for an email campaign, automated for an automated (nurture) message.
sent_time string or null When the message was launched, YYYY-MM-DD HH:MM:SS in your account’s time zone. For an automated message, the time it was set up or last activated, not the time of any one send. null when the message has no launch time.
has_variants boolean true when the message is an A/B test. It stays true when the message’s A/B block no longer exists; the single-message report’s variants is then [].

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).


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.

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.


Two pairs of optional parameters, used by different endpoints:

Parameter Bounds Used by
sent_from, sent_to The message’s sent_time: which messages are listed. Message activity list
activity_from, activity_to 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 no activity_* parameter, every count is lifetime.
  • Each takes YYYY-MM-DD or YYYY-MM-DD HH:MM:SS (URL-encode the space as %20). A date alone means the whole day: a _from date starts at 00:00:00 and a _to date ends at 23: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 _from later than its _to is 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.

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
limit integer 10 Maximum number of items. Values above 100 return 100. limit=0, a limit of zeros such as 00, or an empty limit gives the default. limit=1 returns an array of one item, not a bare object.
skip integer 0 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).

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.


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
Parameter 'page' is not valid A parameter the endpoint does not accept.
Invalid 'sent_from': '10/24/2026' (expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS) A date parameter (sent_from, sent_to, activity_from, activity_to) in another format, not a real date, or with a year outside 1970 to 2999.
Invalid 'limit': 'ten' limit is not a whole number.
Invalid 'skip': 'abc' skip is not a whole number.
Parameter 'limit' accepts one value A single-value parameter sent as an array (limit[]=5, type[]=campaign, sent_from[]=2026-01-01): limit, skip, type, include_in_progress, the four dates, tag_id, segment_id, subscription_id, group_id on compare, and event, link_id, fields, email and remote_id. Only message_id (on the list and compare), group_id (on the list only) and bounce_type (on the message contacts report) accept arrays.

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.

Not-found behaviour varies by endpoint:

  • GET /reports/messages — a filter that matches nothing returns [] with HTTP 200. A message_id that 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 — any message_id that is not a sent or automated message in your account returns HTTP 400 listing each one: No message found for 'message_id': 2099. An unknown group_id returns No 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 — an email or remote_id that matches no contact returns [] with HTTP 200.

Full reference, OpenAPI description, and Postman collection: developers.envoke.com.