Skip to content

Reports API - Message Contacts

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.


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.


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.

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.

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.

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.

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

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.

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.

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.

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.
[
{
"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
}
]

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": []
}

Terminal window
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages/2004/contacts?event=opened"
Terminal window
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
}
]
Terminal window
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" }
]
Terminal window
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
}
]
Terminal window
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"
Terminal window
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages/2004/contacts?event=sent&limit=100&skip=100"
Terminal window
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.

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