Skip to main content

Reports API - Message Contacts

Use the message contacts report API to list the contacts behind a number in a message's report

Written by Marcus Warren

Use the message contacts report to list the contacts behind a number in a message's report: who was sent the message, who opened it, who clicked a particular link, who bounced, who did not open it. It is the API form of clicking a count in the Sent message report in Envoke.


Endpoints

Method

URL

Description

GET

https://e1.envoke.com/v2/reports/messages/{ID}/contacts?event={event}

Return the contacts with one event for this message.

Authentication is via HTTP Basic Auth using your API ID and API Key.


List a Message's Contacts

GET https://e1.envoke.com/v2/reports/messages/{ID}/contacts?event={event}

Returns a JSON array with one row per contact: the contact's id, email, and remote_id, when the event happened, and a count. Replace {ID} with the Envoke message ID and {event} with one of the events below. Rows are slim on purpose: to read anything else about a contact, request it from the Contact API by id.

The report answers for the same messages as the single-message report: 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 returns [] with HTTP 200, as does an event that no contact has.

Each event lists exactly the contacts that one of the ten metrics counts, so the number of rows for event=opened is the opens the single-message report gives for the message with the same activity window, and so on for every event; the exceptions are stated under Events and Contacts no longer in the account.

Accounts restricted from contact-level email activity. On an account restricted from seeing contacts associated with email activity, this endpoint returns HTTP 403 for every event, before any parameter is checked, with the same body as the contact activity report. The message reports are not affected.

Query Parameters

Parameter

Type

Default

Description

event

string

—

Required. Which contacts to list: sent, delivered, bounced, opened, clicked, unsubscribed, spam_reported, skipped, not_opened, or not_clicked. See Events.

link_id

integer

—

With event=clicked, only contacts who clicked this link; with event=not_clicked, contacts sent the message who did not click this link. One value: the id of a tracked link of the message, as in the single-message report's links. Not accepted with any other event.

bounce_type

string or array

—

With event=bounced, only contacts with this bounce reason: hard_bounce, soft_bounce, deferred, rejected, invalid_address, or other, the type keys of the single-message report's bounces. Several values (bounce_type[]=hard_bounce&bounce_type[]=invalid_address) list contacts with any of them. Not accepted with any other event.

activity_from

string

—

Only activity on or after this date or time. See Date parameters and Activity window.

activity_to

string

—

Only activity on or before this date or time.

fields

string

(all)

Comma-separated list of the row fields to return, from id, email, remote_id, event_time, and count. Also lifts the limit cap; see fields and the 10,000 limit.

limit

integer

10

Maximum number of rows. Values above 100 return 100, unless fields is given.

skip

integer

0

Number of rows to skip.

Any other parameter, including sent_from, sent_to, sort, filter, and result_type, returns HTTP 400 (Parameter 'sent_from' is not valid); see Unknown parameters.

Events

Every event lists distinct contacts: a contact appears once, however many records they have for the event.

event

Contacts listed

Rows equal the single-message report's

event_time

count

sent

Contacts the message was sent to (type 1).

totals.sends

The contact's send.

1

delivered

Contacts sent the message who have no bounce for it (see delivered).

totals.delivered

The contact's send.

1

bounced

Contacts with any bounce (type 2), or with the reasons given in bounce_type.

totals.bounces; with one bounce_type, that entry's contacts in bounces

The contact's first bounce (of the reasons asked for).

1

opened

Contacts who opened the message (type 3).

totals.opens

The contact's first open.

The number of email clients the contact opened the message in.

clicked

Contacts who clicked a tracked link (type 4), or the link given in link_id.

totals.clicks; with link_id, that link's clicks in links

The contact's first click (on that link, with link_id).

The number of different links the contact clicked; 1 with link_id.

unsubscribed

Contacts who revoked consent or used one-click unsubscribe from the message (type 5, sub-types 11 and 30).

totals.unsubscribes

The earlier of the two records.

1

spam_reported

Contacts who reported the message as spam (type 5, sub-types 12 and 13).

totals.spam_complaints

The earlier of the two records.

1

skipped

Contacts selected for the message but not sent it (type 6, any reason).

totals.skips

The contact's first skip record.

1

not_opened

Contacts in sent who are not in opened.

totals.sends minus the sent contacts who opened

null

1

not_clicked

Contacts in sent who are not in clicked (with link_id, who did not click that link).

totals.sends minus the sent contacts who clicked

null

1

sent and delivered. event_time for both is the time the message was handed off to the contact, the only time Envoke records before an open; there is no separate delivery time yet. 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, exactly as the delivered metric says. When Envoke switches delivered to its confirmed-delivery records, this event switches with it and event_time becomes the delivery time. Lifetime, the delivered rows equal totals.delivered when every contact with a bounce record for the message was also sent it. Some messages sent before 2015 have bounce records for contacts with no send record for the message; totals.delivered (sends − bounces) subtracts those contacts too, while this event lists every sent contact without a bounce, so on such a message the rows can be more than totals.delivered.

bounced and bounce_type. With no bounce_type, every bounce reason counts, deferrals included. bounce_type takes the keys of the single-message report's bounces block, so bounce_type=hard_bounce lists the contacts behind its hard_bounce entry. Several values list contacts with any of the reasons, each once: bounce_type[]=hard_bounce&bounce_type[]=invalid_address lists exactly the contacts counted in hard_bounces. other includes the retired sub-type 13, so soft_bounce, deferred, rejected, and other together can list more contacts than soft_bounces on old messages.

opened and count. count is the number of email clients the contact's opens came from, as in the contact activity report's opens: one open record exists per contact per email client, so a contact who opened ten times in one client has count 1. An open by Apple Mail Privacy Protection is one of those email clients, so a contact with only a machine open is listed with count 1, and a contact who also opened in another client has count 2. Summed over all rows, count gives the Sent message report's Opens row for the message, less any contacts deleted since (see Contacts no longer in the account).

clicked and link_id. count is the number of different links the contact clicked, including the view-online and preferences links, not the number of clicks. link_id must be a tracked link of this message; the IDs are the id values of the single-message report's links and of the contact activity report's links. A tracked link nobody has clicked is accepted and gives [] (the links block lists only links with clicks, so its ID is not shown there). Identify a link by its id, not its url: the url in links is the link as written in the message, with merge tags unreplaced, so it can carry a merge tag such as {@customfield-url_test}. Poll answers are not links for this purpose and are never accepted as link_id. A click can also be recorded against a link that is not one of this message's tracked links (a link belonging to another message, for example after content was reused); such a click is in clicked and its count, and in the contact activity report with url "", but its ID is not accepted as link_id.

Anonymous polls. Envoke never shows which contacts answered an anonymous poll. When a message contains an anonymous poll, clicks on any poll answer in that message are left out of clicked and out of its count, as the Sent message report's own drill-down does: a contact who also clicked a regular link is still listed, and a contact whose only clicks were poll answers is not. For such a message the clicked rows can therefore be fewer than totals.clicks, which counts every click. Those contacts stay in not_clicked, so the two lists together never single out the people who answered. A message without an anonymous poll lists poll-answer clicks like any other click. The poll results themselves are in the single-message report's polls.

unsubscribed and spam_reported. These are the contacts of the unsubscribes and spam_complaints metrics. There is no parameter to separate revoked consent from one-click unsubscribe, or Envoke's report-abuse link from a mailbox provider's complaint, and the other preference changes of the single-message report (subscription, language, express consent) have no event here.

skipped. Contacts in the message's recipient list who were not sent it, for any reason (no consent, invalid or unverified address, API error, invalid parameters). Skips have been recorded since 17 June 2014, so the list is empty for older messages. There is no parameter to choose a skip reason. A contact skipped once and sent the message later, which can happen with an automated message, is in both skipped and sent.

Did not open and did not click

not_opened and not_clicked are computed as a difference: the contacts in sent, minus the contacts in opened or clicked. This is how the "didn't open" and "didn't click" campaign activity conditions of Envoke's segments work, so the two always agree. A contact whose message bounced is therefore listed in not_opened and not_clicked; to leave bounced addresses out (for example, before a re-send), remove the contacts of event=bounced yourself.

  • Lifetime, not_opened plus the openers who were sent the message equals sent, and the same for clicks. Envoke can record an open or a click for a contact with no send record for the message (a contact who was skipped, or who received the message another way, and then opened it); such a contact is in opened or clicked and in neither sent nor not_opened, so not_opened plus opened can exceed sent by those contacts. A contact who was deferred (a bounce) and then delivered and opened is in bounced and opened, and not in not_opened.

  • Opens recorded by mail privacy proxies count as opens, so a contact whose only open was a machine open is in opened, not in not_opened.

  • not_clicked with link_id is the sent contacts who did not click that link, whether or not they clicked another.

  • Rows have event_time null and count 1.

Activity window

With activity_from or activity_to, each event lists the contacts with a matching record inside the window, the same contacts the metric counts for that window:

  • sent: contacts sent the message inside the window.

  • delivered: contacts sent the message inside the window who have no bounce for it at any time. A contact sent the message on the last day of the window whose bounce arrived the next day is in neither delivered nor bounced for that window.

  • opened, clicked, bounced, unsubscribed, spam_reported, skipped: contacts with that record inside the window, whenever they were sent the message. A contact sent the message before the window who opened it inside the window is in opened but not in sent or delivered.

  • not_opened and not_clicked: the window's sent contacts with no open (or click) record inside the window, bounced contacts included. A contact sent inside the window who opened after it is in not_opened for that window.

event_time is then the contact's first record of the event inside the window, and count counts only email clients or links with a record inside the window.

fields and the 10,000 limit

fields chooses which fields each row carries: fields=id returns rows of { "id": "1004" } only, fields=id,email rows of id and email. The fields always come back in the order of Response fields, whatever their order in fields.

As on the Contact API, giving fields lifts the limit: with fields and no limit, up to 10,000 rows are returned, and a limit above 100 is honoured up to 10,000. Larger values return 10,000. This applies to every event, including not_opened and not_clicked. limit=0, an empty limit and a limit of several zeros (limit=00) all give 10,000 with fields. To read a list longer than 10,000, page with skip. Without fields, the cap is 100 as on every other Reports API list.

Ordering and pagination

Rows are ordered by event_time, newest first, then by contact id, highest first, so contacts with the same time always come back in the same order. not_opened and not_clicked rows, which have no time, are ordered by id, highest first. The order cannot be changed and is the same whether or not event_time is among the fields.

The response does not say how many rows exist in all. To read every row, request pages with skip until one returns fewer than limit rows. limit=1 returns an array of one row. For a message that is still sending, rows are added between requests, so pages read a few minutes apart can overlap or miss a contact.

Contacts no longer in the account

Envoke keeps a message's activity records when a contact is deleted, and the metrics still count them; this report can only list contacts that still exist. So the rows for an event can be fewer than the metric by the number of contacts deleted since, and a deleted contact never appears with an empty email. The Sent message report's drill-down lists only existing contacts in the same way.

email and remote_id are the contact's values now, not the address the message was sent to: a contact who changed their email address since is listed with the new one.

Relation to the Sent message report

Every count in the Sent message report in Envoke links to a list of contacts (https://e1.envoke.com/search_contacts.php?camp={id}&type=…&sub_type=…). For the same message and dates, each event lists the same contacts as that page:

event

Drill-down on the Sent message report

sent

Sent

delivered

Delivered (lifetime; see the note below for a window)

bounced

Bounced; with bounce_type, the Hard bounce, Soft bounce, Invalid email address, and Other rows (the report's Other is sub-type 10 only, where other here also holds the retired 13)

opened

Opens

clicked

Clicks; with link_id, the link's count in the links table

unsubscribed

Revoked consent and One-click unsubscribe together

spam_reported

Envoke spam reported and External spam reported together

skipped

Skipped

not_opened, not_clicked

No drill-down on the report; the contacts follow the "didn't open" and "didn't click" campaign activity conditions of segments (sent minus the event)

The report's own numbers count some events per email client or per bounce reason (see How the counts work); the list behind each number has one entry per contact, as this report does. Two differences remain. Inside a date range, the report's Delivered list leaves out only contacts whose bounce falls inside the range, where delivered here leaves out a contact whose bounce arrived at any time (see delivered). And the report's links table merges links with the same URL into one row, where link_id takes one link at a time.

Response fields

Field

Type

Description

id

string

Envoke contact ID; the Contact API id.

email

string

The contact's email address now.

remote_id

string

The contact's remote ID now, or "" when it has none.

event_time

string or null

When the event first happened for this contact and message, YYYY-MM-DD HH:MM:SS in your account's time zone (see Times and time zone); inside an activity window, the first time inside the window. null for not_opened and not_clicked.

count

integer

For opened, the number of email clients; for clicked without link_id, the number of different links; otherwise 1.

Response

[   {     "id": "1008",     "email": "p.nguyen@example.org",     "remote_id": "MRCT-0501",     "event_time": "2026-05-16 19:44:02",     "count": 2   },   {     "id": "1005",     "email": "l.ferreira@example.org",     "remote_id": "",     "event_time": "2026-05-15 07:12:55",     "count": 1   },   {     "id": "1004",     "email": "maya.chen@example.org",     "remote_id": "MRCT-0412",     "event_time": "2026-05-14 12:02:44",     "count": 1   } ]

Errors

Invalid requests return HTTP 400 with the validation envelope, every problem listed together (see Error Responses):

{   "success": false,   "result_data": {     "errors": [       "Parameter 'event' must be provided",       "Invalid 'activity_from': '05/14/2026' (expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)"     ]   },   "result_text": "Validation failed, see 'result_data' -> 'errors'" }

Besides the shared date, limit, skip, and unknown-parameter errors:

Error

Cause

Parameter 'event' must be provided

No event parameter.

Invalid 'event': 'opens' (possible values: 'sent', 'delivered', 'bounced', 'opened', 'clicked', 'unsubscribed', 'spam_reported', 'skipped', 'not_opened', 'not_clicked')

event is not one of the ten events, or is empty.

Invalid 'link_id': 'abc'

link_id is not a whole number.

Invalid 'link_id': '88199'

link_id is not a tracked link of this message (another message's link, a poll answer, or no link at all). Checked only when the message is found.

Parameter 'link_id' requires event 'clicked' or 'not_clicked'

link_id with any other event.

Invalid 'bounce_type': 'hard' (possible values: 'hard_bounce', 'soft_bounce', 'deferred', 'rejected', 'invalid_address', 'other')

A bounce_type value that is not one of the six keys; one error per value.

Parameter 'bounce_type' requires event 'bounced'

bounce_type with any other event.

Invalid 'fields': 'first_name' (possible values: 'id', 'email', 'remote_id', 'event_time', 'count')

A name in fields that is not a row field, one error per name, or an empty fields (Invalid 'fields': '' (possible values: 'id', 'email', 'remote_id', 'event_time', 'count')).

Parameter 'event' accepts one value

event, link_id, or fields sent as an array (event[]=opened), as for every single-value parameter; only bounce_type accepts several values.

The requires event errors are given only when event is itself valid. A message that is not found returns [] once every parameter is valid, whatever link_id says.

Response

Cause

[] with HTTP 200

The message does not exist, belongs to another account, is deleted, or has not started sending; or no contact has the event.

HTTP 404, 404 Not Found

{ID} is not a number.

HTTP 403, Contact-level email activity is not available on this account

The account is restricted from seeing contacts associated with email activity. Returned for every event, before the parameters are checked.

The 403 body is the same as the contact activity report's:

{   "success": false,   "result_text": "Contact-level email activity is not available on this account",   "result_data": [] }

Examples

The ten most recent openers

curl -X GET -gs -u "$API_ID:$API_KEY" \   "https://e1.envoke.com/v2/reports/messages/2004/contacts?event=opened"

Everyone who clicked one link

curl -X GET -gs -u "$API_ID:$API_KEY" \   "https://e1.envoke.com/v2/reports/messages/2004/contacts?event=clicked&link_id=88125&limit=100"

Response (first rows; 39 rows in all, the link's clicks in the single-message report):

[   {     "id": "1009",     "email": "s.gill@example.org",     "remote_id": "MRCT-0533",     "event_time": "2026-05-19 21:05:37",     "count": 1   },   {     "id": "1004",     "email": "maya.chen@example.org",     "remote_id": "MRCT-0412",     "event_time": "2026-05-14 12:03:10",     "count": 1   } ]

Sent but did not open, IDs only

curl -X GET -gs -u "$API_ID:$API_KEY" \   "https://e1.envoke.com/v2/reports/messages/2004/contacts?event=not_opened&fields=id"

Response (up to 10,000 rows, highest ID first; 2004 was sent to 386 contacts, of whom 201 opened and 185 did not):

[   { "id": "1006" },   { "id": "1003" },   { "id": "1002" } ]

Hard bounces and invalid addresses

curl -X GET -gs -u "$API_ID:$API_KEY" \   "https://e1.envoke.com/v2/reports/messages/2002/contacts?event=bounced&bounce_type%5B%5D=hard_bounce&bounce_type%5B%5D=invalid_address"

Response (the contacts counted in hard_bounces):

[   {     "id": "1007",     "email": "r.osei@example.org",     "remote_id": "",     "event_time": "2026-03-10 18:02:13",     "count": 1   } ]

Openers in the first week, every row

curl -X GET -gs -u "$API_ID:$API_KEY" \   "https://e1.envoke.com/v2/reports/messages/2004/contacts?event=opened&activity_from=2026-05-14&activity_to=2026-05-20&fields=id,email,event_time"

Second page of 100 senders

curl -X GET -gs -u "$API_ID:$API_KEY" \   "https://e1.envoke.com/v2/reports/messages/2004/contacts?event=sent&limit=100&skip=100"

A message that is not found

curl -X GET -gs -u "$API_ID:$API_KEY" \   "https://e1.envoke.com/v2/reports/messages/2099/contacts?event=opened"

Response:

[]

Note: The curl examples above are formatted for readability. All requests must use properly encoded URLs.

For code examples and interactive testing, visit api.envoke.com.

Did this answer your question?