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

---

## 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](#events) below. Rows are slim on purpose: to read anything else about a contact, request it from the [Contact API](https://envoke-developers.pages.dev/v1/contacts/read/) by `id`.

The report answers for the same messages as the [single-message report](https://envoke-developers.pages.dev/v2/reports/message-stats/#get-a-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](https://envoke-developers.pages.dev/v2/reports/fields/#fields) 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](#events) and [Contacts no longer in the account](#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](https://envoke-developers.pages.dev/v2/reports/contacts/#errors). 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](#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`](https://envoke-developers.pages.dev/v2/reports/message-stats/#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`](https://envoke-developers.pages.dev/v2/reports/message-stats/#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](https://envoke-developers.pages.dev/v2/reports/fields/#date-parameters) and [Activity window](#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](#fields-and-the-10000-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](https://envoke-developers.pages.dev/v2/reports/fields/#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`](https://envoke-developers.pages.dev/v2/reports/fields/#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`](https://envoke-developers.pages.dev/v2/reports/fields/#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`](https://envoke-developers.pages.dev/v2/reports/fields/#hard_bounces). `other` includes the retired sub-type `13`, so `soft_bounce`, `deferred`, `rejected`, and `other` together can list more contacts than [`soft_bounces`](https://envoke-developers.pages.dev/v2/reports/fields/#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](#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`](https://envoke-developers.pages.dev/v2/reports/message-stats/#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`](https://envoke-developers.pages.dev/v2/reports/message-stats/#polls).

**`unsubscribed` and `spam_reported`.** These are the contacts of the [`unsubscribes`](https://envoke-developers.pages.dev/v2/reports/fields/#unsubscribes) and [`spam_complaints`](https://envoke-developers.pages.dev/v2/reports/fields/#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](#response-fields), whatever their order in `fields`.

As on the [Contact API](https://envoke-developers.pages.dev/v1/contacts/read/#query-parameters), 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](https://envoke-developers.pages.dev/v2/reports/fields/#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`](https://envoke-developers.pages.dev/v2/reports/fields/#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.
<!-- VERIFY: for each positive event, lifetime, the drill-down page lists the same number of contacts and the same ids as this report; T-87 compares -->

### 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](https://envoke-developers.pages.dev/v2/reports/fields/#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

```json
[
  {
    "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](https://envoke-developers.pages.dev/v2/reports/fields/#error-responses)):

```json
{
  "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:

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

---

## Examples

### The ten most recent openers

```bash
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

```bash
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):

```json
[
  {
    "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

```bash
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):

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

### Hard bounces and invalid addresses

```bash
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`):

```json
[
  {
    "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

```bash
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

```bash
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

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

Response:

```json
[]
```

---

*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](https://developers.envoke.com).*
