# Message API - Read

Use the read endpoints to retrieve messages from your account. You can list messages with filtering, sorting, and pagination, or retrieve a single message by ID, optionally with its HTML.

---

## Endpoints

| Method | URL | Description |
|---|---|---|
| GET | `https://e1.envoke.com/v2/messages` | Return a list of messages. |
| GET | `https://e1.envoke.com/v2/messages/{ID}` | Return a single message. |

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

---

## List Messages

```
GET https://e1.envoke.com/v2/messages
```

Returns a JSON array of messages, most recently modified first. Supports filtering, sorting, pagination, and field selection.

The list contains every message in your account that has not been deleted, in every status: drafts, scheduled and sent messages, messages published to the archive, and the `Nurture` and `Transactional` messages set up in Envoke. Templates and deleted messages are not included. There is no filter by message type.

### Query Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `limit` | integer | `10` | Maximum number of results to return. Hard cap is `100` unless `?fields=` or `?result_type=kvp` is also set (see note below). `limit=1` returns a single object, not an array (see below). |
| `skip` | integer | `0` | Number of records to skip. Use with `limit` and `sort` to paginate. |
| `fields` | string | *(all)* | Comma-separated list of fields to include in each result, e.g. `id,name,status`. Use names from the [Fields reference](https://envoke-developers.pages.dev/v2/messages/fields/). |
| `result_type` | string | *(array)* | Set to `kvp` to receive an object of message ID to name (`{ "2001": "Spring Season Announcement" }`). |
| `filter[{field}]` | string | — | Filter on a field. See [Filterable and sortable fields](#filterable-and-sortable-fields) and [Filter Operators](#filter-operators). |
| `sort[{field}]` | string | `modified_time` descending | Sort on a field. Value: `asc` or `desc` (case-insensitive), or `1` or `-1`. Repeat for a secondary sort. A `sort` replaces the default order. |

**Note — limit behaviour with `?fields=` and `?result_type=kvp`:** When either of these parameters is present, the default limit increases to 10,000 (returning every record up to that number if no explicit limit is specified) and a `?limit=` value above 100 is honoured as-is.

**Note — `limit=1` returns an object.** With `limit=1` the response is the matching message as a bare JSON object, not an array of one. `result_type=kvp` has no effect when `limit=1`; `fields` still applies. When nothing matches, the response is `[]`.

**Note — filters without `filter[...]`.** When the request has no `filter` parameter, every query parameter other than `limit`, `skip`, `sort`, `fields`, `result_type`, and `include_html` is treated as a filter, so `?status=Sent` is the same as `?filter[status]=Sent`. Any other parameter is rejected as an unknown filter field (for example `Field 'folder' is not valid for filtering`). When `filter` is present, the other parameters are ignored as filters.

`include_html` has no effect on the list; HTML is returned only for a [single message](#get-a-single-message).

### Filterable and sortable fields

| Field | Filter operators | Sortable |
|---|---|---|
| `id`, `external_id`, `name`, `subject` | equals, `(ne)` | Yes |
| `status` | equals, `(ne)` | Yes, by status code |
| `language`, `recipient_language` | equals, `(ne)` | Yes, by language ID |
| `publish_archive`, `utm_tag_links` | equals, `(ne)` | Yes |
| `create_time`, `modified_time` | — | Yes |

Fields not in this table cannot be filtered or sorted, including `group`, `send_time`, `sender`, `reply_to`, `recipient_subscription`, `recipient_tag`, and `recipient_segment`. To find messages by one of these, list with `fields` and select on your side.

- **`name` and `subject`** match the whole value. Case is ignored.
- **`status`** takes a status name or code; see [Filter by status](#filter-by-status).
- **`language` and `recipient_language`** take the language ID, not the code: `1` for English, `3` for French. `filter[language]=en` does not match English messages.
- **`publish_archive`** takes the stored value: `2` for messages published to the archive, `1` for the rest. `true` and `false` do not work in filters.
- **`utm_tag_links`** takes `1` for messages with UTM tagging on and `0` for off. `true` and `false` do not work in filters; use `1` or `0`.

### Filter Operators

| Operator | Example | Description |
|---|---|---|
| *(none)* | `?filter[subject]=Our spring season is here` | Equals. |
| `(ne)` | `?filter[status][(ne)]=Draft` | Not equal. |
| `(or)` | `?filter[(or)][0][status]=Sent&filter[(or)][1][status]=Scheduled` | Matches if any of the grouped conditions match. |
| `(not)` | `?filter[(not)][status]=Sent` | Matches if the grouped conditions do not all match. |

*Note: URL-encode brackets in production requests (e.g. `%5B` for `[`).*

Each operator also has a `$` spelling (`$ne`, `$or`, `$not`) that behaves the same way. These docs use the `(op)` spelling.

The `(in)`, `(regex)`, `(gt)`, `(lt)`, `(gte)`, and `(lte)` operators are not supported on any message field. They fail with an error that names the SQL operator, e.g. `'IN' is not a valid operator for the field 'status'` (`REGEXP` for `(regex)`; `>`, `<`, `>=`, `<=` for the range operators). To match one of several values, use `(or)`.

**Combining conditions.**
- Separate filters are ANDed: `?filter[status]=Draft&filter[language]=3`.
- A query string cannot repeat a key, so to use the same field twice, put each condition in its own numbered group: `?filter[0][status][(ne)]=Draft&filter[1][status][(ne)]=Sent`. Groups inside `(or)` are numbered the same way.
- `(not)` wraps everything inside it: `?filter[(not)][status]=Sent&filter[(not)][language]=3` excludes messages that are both sent *and* French.

### Filter by status

`status` can be filtered by name or by code.

| Name | Code |
|---|---|
| `Draft` | `0` |
| `Scheduled` | `1` |
| `AB test in progress` | `2` |
| `Queuing` | `4` |
| `Sending` | `5` |
| `Transactional` | `7` |
| `Nurture` | `8` |
| `Sent` | `9` |

- Names are matched ignoring case and surrounding spaces, and a space and an underscore are treated as the same: `Sent`, `sent`, `AB test in progress`, and `ab_test_in_progress` all work.
- A code is used as given. A number that is not a status code (e.g. `3`) is accepted and matches nothing.
- An unknown name fails with the list of names: `Invalid 'status' provided (possible values: 'Draft', 'Scheduled', 'AB test in progress', 'Queuing', 'Sending', 'Transactional', 'Nurture', 'Sent')`.
- `(ne)` works with names and codes.
- To match several statuses, use `(or)`: `?filter[(or)][0][status]=Sent&filter[(or)][1][status]=Scheduled`. `(in)` is not supported.

### Errors

Invalid list parameters return HTTP `400`. The `limit`, `skip`, and `sort` format errors are returned first; filter and sort field errors are then listed together:

```json
{
  "success": false,
  "result_text": "Validation failed, see 'result_data' -> 'errors'",
  "result_data": {
    "errors": [
      "Field 'folder' is not valid for filtering",
      "'IN' is not a valid operator for the field 'status'"
    ]
  }
}
```

| Error | Cause |
|---|---|
| `Invalid 'limit': 'ten'` | `limit` is not a whole number. |
| `Invalid 'skip': 'abc'` | `skip` is not a whole number. |
| `Invalid 'sort': 'name'` | `sort` was sent without a field, e.g. `?sort=name`. |
| `'up' is not a valid sort direction` | Sort direction is not `asc`, `desc`, `1`, or `-1`. |
| `Field 'folder' is not valid for sorting` | The field does not exist. |
| `Field 'folder' is not valid for filtering` | The field does not exist. Also returned for an unrecognised operator: `?filter[name][(foo)]=x` gives `Field '(foo)' is not valid for filtering`. |
| `'IN' is not a valid operator for the field 'status'` | The operator is not supported. The message names the SQL operator: `IN`, `REGEXP`, `>`, `<`, `>=`, or `<=`. |
| `Invalid 'status' provided (possible values: ...)` | Not a status name. The message lists the valid names. |
| `Field 'name' requires a scalar value` | An operator is given a list or group where a single value is expected, e.g. `filter[name][(ne)][]=Gala`. |

### Response

A JSON array of message objects. Returns an empty array (`[]`) if no messages match.

```json
[
  {
    "id": "2001",
    "external_id": "3e8b1f6c9a2d4e7b0c5f8a1d3e6b9c2f",
    "preview_url": "https://engage.envoke.com/m/a7c3e9f1b5d2/m/3e8b1f6c9a2d4e7b0c5f8a1d3e6b9c2f",
    "create_time": "2026-10-06 10:15:42",
    "modified_time": "2026-10-06 11:02:08",
    "name": "Spring Season Announcement",
    "group": "Productions",
    "status": "Scheduled",
    "send_time": "2026-10-20 09:00:00",
    "sender": "Box Office <boxoffice@example.org>",
    "reply_to": "Front of House <frontofhouse@example.org>",
    "subject": "Our spring season is here",
    "language": "en",
    "recipient_subscription": "Season News",
    "recipient_tag": "",
    "recipient_segment": "",
    "recipient_language": "",
    "publish_archive": true,
    "utm_tag_links": true
  }
]
```

Times are in the account time zone with no offset (see [Dates and time zones](https://envoke-developers.pages.dev/v2/messages/fields/#dates-and-time-zones)). `html` is never included in list responses.

---

## Get a Single Message

```
GET https://e1.envoke.com/v2/messages/{ID}
```

Returns a single message object (the same shape as one item in the list response). Replace `{ID}` with the Envoke message ID. `fields` can be used to select fields; filter, sort, paging, and `result_type` parameters are ignored.

Add `?include_html=1` to include the message HTML. It is added as the last key, `html`. With `include_html=0` or without the parameter, the HTML is left out. When `fields` is also set, list `html` in it to keep the HTML.

If the ID is not found, or belongs to a deleted message or a template, the API returns an empty array (`[]`).

### Response with `?include_html=1`

```json
{
  "id": "2003",
  "external_id": "9c4d2a7e1f5b3d8a6c0e4f2b7d9a1c5e",
  "preview_url": "https://engage.envoke.com/m/a7c3e9f1b5d2/m/9c4d2a7e1f5b3d8a6c0e4f2b7d9a1c5e",
  "create_time": "2026-10-06 10:40:11",
  "modified_time": "2026-10-06 10:40:11",
  "name": "Pirates of Penzance - Opening Night",
  "group": "",
  "status": "Draft",
  "send_time": "",
  "sender": "Box Office <boxoffice@example.org>",
  "reply_to": null,
  "subject": "",
  "language": "en",
  "recipient_subscription": "",
  "recipient_tag": "",
  "recipient_segment": "",
  "recipient_language": "",
  "publish_archive": false,
  "utm_tag_links": false,
  "html": "<html><body><h1>The Pirates of Penzance</h1><p>Curtain at 7:30 p.m.</p></body></html>"
}
```

---

## Examples

### Return the first page of messages

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages"
```

Returns the 10 most recently modified messages. Use `limit` (up to 100) and `skip` to page through the rest.

### Paginate through results

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages?sort%5Bid%5D=asc&limit=100&skip=100"
```

### Sort by name

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages?sort%5Bname%5D=asc&limit=5"
```

### Find a message by name

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages?filter%5Bname%5D=Spring%20Season%20Announcement"
```

### Filter by subject

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages?filter%5Bsubject%5D=Our%20spring%20season%20is%20here"
```

### Sent messages, by status name

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages?filter%5Bstatus%5D=Sent"
```

The same filter by status code:

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages?filter%5Bstatus%5D=9"
```

### Everything except drafts

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages?filter%5Bstatus%5D%5B%28ne%29%5D=Draft"
```

### Sent or scheduled messages

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages?filter%5B%28or%29%5D%5B0%5D%5Bstatus%5D=Sent&filter%5B%28or%29%5D%5B1%5D%5Bstatus%5D=Scheduled"
```

### Messages published to the archive

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages?filter%5Bpublish_archive%5D=2"
```

### Return only specific fields

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages?fields=id,name,status,send_time"
```

Response:

```json
[
  { "id": "2002", "name": "Auditions Open", "status": "Draft", "send_time": "" },
  { "id": "2001", "name": "Spring Season Announcement", "status": "Scheduled", "send_time": "2026-10-20 09:00:00" }
]
```

With `fields` set and no `limit`, this returns up to 10,000 messages.

### Return as a key-value map

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages?result_type=kvp"
```

Response:

```json
{ "2002": "Auditions Open", "2001": "Spring Season Announcement" }
```

### Look up one message's ID by name

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages?name=Auditions%20Open&fields=id,status&limit=1"
```

Response (an object, because `limit=1`):

```json
{ "id": "2002", "status": "Draft" }
```

### Get a single message by ID

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages/2001"
```

### Get a single message with its HTML

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v2/messages/2003?include_html=1"
```

---

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