# Envoke Developers
> Reference documentation for Envoke's REST APIs: contacts and consent, tags and subscriptions, templates, messages, reporting, and webhooks. All requests go to https://e1.envoke.com with the API version in the path (/v1 or /v2) and use HTTP Basic Auth with your API ID and API Key. Every page is also available as plain Markdown at its URL with .md appended, and the merged OpenAPI 3.1 description of every published endpoint is at /openapi.json.
---
URL: https://envoke-developers.pages.dev/guides/authentication/
Markdown: https://envoke-developers.pages.dev/guides/authentication.md
# Authentication
Every request to the Envoke REST APIs is authenticated with HTTP Basic Auth: your API ID is the username and your API Key is the password. This article explains where to find them, what must be in place before they work, how API limits apply, and what the API returns when it refuses a request.
---
## Credentials
| Credential | Basic Auth role | Format |
|---|---|---|
| API ID | Username | 32 hexadecimal characters |
| API Key | Password | 32 characters, which can include `/`, `-`, `_`, `*`, and `?` |
The API ID and API Key belong to one Envoke account, and every request made with them reads and changes that account's data only. If your organisation has more than one Envoke account, each account has its own API ID and API Key.
There is one API Key per account. It is not tied to an Envoke user, and anyone who has it has full API access to the account, so keep it out of source control, browser code, and shared documents. Envoke support can replace a key; there is no way to replace it yourself.
The same credentials work for every API in this reference, on both `/v1` and `/v2`.
---
## Find Your API ID and API Key
1. In Envoke, open the settings menu and choose **Account settings**.
2. Choose the **Integrations** tab.
3. In the **API** section, choose **Reveal Credentials**. The `API_ID` line is your API ID and the `API_KEY` line is your API Key.
The API section is shown only to users who can manage the API. Other users see "Only administrators can view API connection details."
---
## Before Your Key Works
All of the following must be true. Until they are, requests are refused even with the correct API ID and API Key.
- **Your plan includes the API.** If it does not, the Integrations tab shows an **Upgrade your plan** link instead of the API section.
- **The API is enabled for your account.** If the Integrations tab shows an **Enable API** button, choose it. Enabling the API sets your account's limits to 1,000 requests a day and 3 requests a second (see [API Limits](#api-limits)) and shows the API section with your credentials. Until the API is enabled, every request returns `429` with `API access is not enabled for your account.`
- **Your account has a current key.** If the API section says "Please contact support in order to use the API" instead of offering **Reveal Credentials**, your account has an older key that Envoke support must replace before you can use the API.
- **Your account is active.** A closed or inactive account returns `403` (see [Error Responses](#error-responses)).
The Send API has one more prerequisite: the Transactional API preference on the same Integrations tab. See the [Send API overview](https://envoke-developers.pages.dev/v1/send/fields/#overview).
---
## Make a Request
Pass the API ID and API Key with curl's `-u` option, which sends them in an `Authorization: Basic` header. Keep the quotes: the key can contain characters your shell would otherwise interpret.
```bash
export API_ID="your API ID"
export API_KEY="your API Key"
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/tags?limit=1"
```
Any other HTTP client works the same way: send the header `Authorization: Basic ` followed by the Base64 encoding of `API_ID:API_KEY`.
A `200` response confirms that the credentials work. This request returns a JSON array holding at most one tag, or `[]` if your account has no tags.
---
## API Limits
Every authenticated request counts towards your account's API limits. Enabling the API sets two limits:
| Limit | Default | Resets |
|---|---|---|
| Daily | 1,000 requests | At midnight Eastern time |
| Per second | 3 requests | At the start of each second |
The Integrations tab shows your daily limit, the number of requests counted so far today, and the per-second limit. To raise a limit, or to reset the daily count early, contact Envoke support.
- **What counts.** Each request that passes authentication counts once, whatever its result, including requests refused with `429` because a limit was exceeded. Requests refused with `401`, or with `403` because the account is not active, do not count.
- **Webhook deliveries count too.** Each event Envoke delivers to your webhook endpoints adds one to the same daily and per-second counts. Deliveries are never refused for being over a limit, but a high webhook volume can use up the daily limit and cause your own API requests to be refused with `429`.
- **Email alerts.** When your account reaches 80% of its daily limit, and again when it exceeds the limit, Envoke emails the account administrator who signed in most recently. Each alert is sent at most once a day.
- **No limit headers.** Responses carry no rate-limit headers. Track your own request rate, or check the usage on the Integrations tab.
When a request is refused with `429` for the per-second limit, wait a second and retry. When it is refused for the daily limit, every further request is refused until midnight Eastern time, unless Envoke support resets the count or raises the limit.
---
## Error Responses
### 401 — Invalid credentials
Returned when the `Authorization` header is missing, or the API ID and API Key do not match an Envoke account.
```json
{
"success": false,
"result_data": [],
"result_text": "Invalid credentials"
}
```
The response carries a `WWW-Authenticate: Basic` header. Its `Content-Type` is `text/html`, although the body is JSON.
### 403 — Account not active
Returned when the API ID and API Key are correct but the account cannot use the API: it has been closed, or it is cancelled or suspended. The body is one of:
```json
{
"success": false,
"result_data": [],
"result_text": "This account has been closed."
}
```
```json
{
"success": false,
"result_data": [],
"result_text": "This account is not active."
}
```
Some endpoints also return `403` for reasons of their own, such as the Reports API on an account restricted from contact-level email activity; each is documented with its endpoint, for example [Contact Activity errors](https://envoke-developers.pages.dev/v2/reports/contacts/#errors).
### 429 — API not enabled or limit exceeded
When the API has not been enabled for the account:
```json
{
"success": false,
"result_data": [],
"result_text": "API access is not enabled for your account."
}
```
When a limit is exceeded, `result_text` names the limit, `result_data.exceeded` is the period that was exceeded (`day` or `second`), and `result_data.limits` gives each of the account's limits with its current usage:
```json
{
"success": false,
"result_data": {
"exceeded": "day",
"limits": {
"day": { "limit": 1000, "current_usage": 1001 },
"second": { "limit": 3, "current_usage": 0 }
}
},
"result_text": "Daily API limit of 1000 exceeded."
}
```
For the per-second limit, `result_text` is `Per second API limit of 3 exceeded.` and `exceeded` is `second`.
---
*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).*
---
URL: https://envoke-developers.pages.dev/guides/which-api/
Markdown: https://envoke-developers.pages.dev/guides/which-api.md
# Which API do I use?
Envoke's REST API is a set of small APIs, one per resource. This article explains which APIs answer on `/v1` and which on `/v2`, how tags, subscriptions, and mandatory consent relate to each other, when to send with the Send API or the Message API, how the Reports API relates to the reports in Envoke, and what your account needs before its first send.
---
## The APIs and Their Versions
Every API is on the same host, `https://e1.envoke.com`, and uses the same API ID and API Key (see [Authentication](https://envoke-developers.pages.dev/guides/authentication/)). Each API answers on one version only, so the version is part of the API's address rather than a choice:
| API | Path | What it does |
|---|---|---|
| [Contacts](https://envoke-developers.pages.dev/v1/contacts/fields/) | `/v1/contacts` | Read, create, upsert, update, and delete contacts, including their consent, custom field values, tags, and subscriptions. |
| [Send API](https://envoke-developers.pages.dev/v1/send/fields/) | `/v1/send` | Send one message immediately to up to 1,000 listed recipients per request. |
| [Senders](https://envoke-developers.pages.dev/v1/senders/fields/) | `/v1/senders` | Manage sender identities, the From name and address of your messages. |
| [Hooks](https://envoke-developers.pages.dev/v1/hooks/fields/) | `/v1/hooks.json` | Manage webhook subscriptions. |
| [Tags](https://envoke-developers.pages.dev/v2/tags/fields/) | `/v2/tags` | Manage tags, and add or remove contacts in bulk. |
| [Subscriptions](https://envoke-developers.pages.dev/v2/subscriptions/fields/) | `/v2/subscriptions` | Manage subscriptions, and add or remove contacts in bulk. |
| [Custom fields](https://envoke-developers.pages.dev/v2/customfields/fields/) | `/v2/customfields` | Define custom fields. Values are written to contacts through the Contact API. |
| [Groups](https://envoke-developers.pages.dev/v2/groups/fields/) | `/v2/groups/tags`, `/v2/groups/subscriptions`, `/v2/groups/messages`, `/v2/groups/templates` | Manage the groups (folders) that organise tags, subscriptions, messages, and templates. |
| [Templates](https://envoke-developers.pages.dev/v2/templates/fields/) | `/v2/templates` | Manage email templates. |
| [Messages](https://envoke-developers.pages.dev/v2/messages/fields/) | `/v2/messages` | Create, schedule, send, and cancel messages to a tag, subscription, or segment, and read or delete messages. |
| [Reports](https://envoke-developers.pages.dev/v2/reports/fields/) | `/v2/reports/messages`, `/v2/reports/contacts` | Read the email activity counts of messages and contacts. |
The conventions differ from one API to another rather than by version:
- **IDs.** Most APIs return IDs as strings, such as `"42"`. The Send API's response and the Message API's create-and-send response return the message `id` as an integer, as does the `PATCH /v1/contacts/{id}` response for the contact `id`. Webhook subscription IDs are 32-character hexadecimal strings.
- **Errors.** Every API uses the validation envelope `Validation failed, see 'result_data' -> 'errors'` for most errors, but the empty-body error and the response for an ID that is not found differ between APIs. Each is listed under Error Responses at the end of the API's fields reference, for example [Contact API errors](https://envoke-developers.pages.dev/v1/contacts/fields/#error-responses).
- **Hooks.** The Hooks API's documented paths end in `.json`, and creating a subscription returns HTTP `201` with a body of `{"id": "..."}` instead of the success envelope (see [Hooks API - Create](https://envoke-developers.pages.dev/v1/hooks/insert/#response)).
The Contact API's error for an unknown tag or subscription name suggests the endpoint `/interests`. That endpoint is not part of this reference; list names with the Tag and Subscription APIs instead.
---
## Tags, Subscriptions, and Mandatory Consent
A contact can hold three kinds of list membership. They are separate resources in Envoke but share one field on the Contact API.
| Kind | What it is for | Seen by contacts | Managed with |
|---|---|---|---|
| Tag | Segmenting contacts by topic, preference, or area of interest. Also called an interest: tag groups have the type `interest`. | Where tags are displayed publicly, such as website subscription forms, under their `display_names`. | [Tag API](https://envoke-developers.pages.dev/v2/tags/fields/) |
| Subscription | A type of communication contacts opt in to or out of, such as a weekly newsletter. | On your public preference page, unless its [`prefpage_access`](https://envoke-developers.pages.dev/v2/subscriptions/fields/#prefpage_access) is `"private"`. | [Subscription API](https://envoke-developers.pages.dev/v2/subscriptions/fields/) |
| Mandatory consent subscription | Your account's one subscription for mandatory (consent-exempt) messages. Only contacts who hold it are sent a mandatory message. | Its description is given in each mandatory message as the reason it was sent. | Not returned by the Subscription API. Contacts are given it through the Contact API's `interests`, or when a mandatory Send API request creates them. |
On the Contact API, the single field [`interests`](https://envoke-developers.pages.dev/v1/contacts/fields/#interests) holds the names of all three kinds; there are no separate `tags` or `subscriptions` fields.
### How Each API Identifies Them
The Contact API writes a contact's tags, subscriptions, and mandatory consent through one field, so it identifies each item by its name. The Tag and Subscription APIs each manage one kind of item and identify it by its `id` in the URL. Both refer to the same items: a tag's `name` from the Tag API is what goes in `interests`, and its `id` is the numeric ID the Contact API accepts as an object key.
| API | How an item is identified |
|---|---|
| Contact API, create and update | By name in `interests`. Matching ignores case and treats a space and an underscore as the same. In the object form (`{ "Volunteer": "Set" }`), a key can also be the item's numeric ID. |
| Contact API, read filters | `filter[interests]` takes the exact name, case-sensitive. IDs are not accepted (see [Filterable and sortable fields](https://envoke-developers.pages.dev/v1/contacts/read/#filterable-and-sortable-fields)). |
| Tag and Subscription APIs | By `id` in the URL, for example `POST https://e1.envoke.com/v2/tags/{ID}/contacts`, which adds up to 1,000 contacts by `contact_ids` or `contact_emails` in one request ([Tag API - Add and Remove Contacts](https://envoke-developers.pages.dev/v2/tags/contacts/)). |
| Message API | `recipient_tag` and `recipient_subscription` take a name or an ID. `recipient_subscription` also accepts the mandatory consent subscription. |
Use the Contact API's `interests` to set the tags and subscriptions of one contact as you create or update it. A list of names only adds; to remove an item, use the object form with `"Unset"`. Use the Tag and Subscription APIs to add or remove many contacts for one tag or subscription.
### Consent Is Separate
A contact's `consent_status` decides whether Envoke may send them email at all; tags and subscriptions decide which messages they are in the audience for. Adding a contact to a subscription through the Contact API does not check their consent: a contact whose consent is `Not Provided` is added to the subscription like any other contact, and is then excluded when a message is sent to that subscription. See [Consent fields](https://envoke-developers.pages.dev/v1/contacts/fields/#consent-fields) for the consent types and when a change takes effect.
---
## Send API or Message API
Both APIs send email, with different models.
The **Send API** (`POST https://e1.envoke.com/v1/send`) creates a message and sends it in the same request to recipients you list one by one. An address that matches no contact becomes a new contact and is sent the message. There is no draft, schedule, review, or cancel step.
The **Message API** (`https://e1.envoke.com/v2/messages`) creates a message as a `Draft` addressed to an audience Envoke already holds: a tag, a subscription, or a segment. Sending it, now or at a scheduled time, is a separate request that returns an estimate of the recipients, and a scheduled message can be cancelled.
| | Send API | Message API |
|---|---|---|
| Recipients | Up to 1,000 email addresses or contact IDs per request, listed one by one. Further batches can be sent to the same message. | A tag, subscription, or segment, optionally narrowed by a subscription and by language. |
| Addresses that are not contacts | Created as contacts with your Transactional API consent, and sent the message. | Not applicable: only contacts in the audience are sent the message. |
| When it sends | Immediately. | Immediately, or at a `send_time` in your account's time zone. |
| Review and cancel | None. A sent message cannot be changed or cancelled. | A `Draft` can be read, previewed at its `preview_url`, changed, or deleted. A `Scheduled` message can be cancelled back to `Draft`. |
| Counts in the response | The outcome for each recipient, decided when the request is made. | An estimate taken when the request is made; recipients are selected again when the send starts. |
| Personalisation | Per-recipient `replace` values, applied as each email is delivered. Envoke merge fields when `enable_merge_fields` is `true`. | `replace` values applied once when the message is saved, the same for every recipient. Envoke merge tags in the HTML for per-recipient content. |
| Tracking defaults | No open or click tracking unless `track_recipient_activity` is `true`. UTM parameters are never added. | `utm_tag_links` follows your account's link analytics setting. |
| Needs before sending | The Transactional API preference (see [Before Your First Send](#before-your-first-send)). | A subject, a sender, and at least one of `recipient_subscription`, `recipient_tag`, or `recipient_segment`. |
**Use the Send API** when your system decides who receives a message, one person or a list of addresses, and the email goes out now: for example a confirmation or a notice to the people named in a request. Read the full rules in [Send API - Fields Reference](https://envoke-developers.pages.dev/v1/send/fields/) and [Send API - Send](https://envoke-developers.pages.dev/v1/send/send/).
**Use the Message API** when the audience is a tag, subscription, or segment in Envoke, or when the message needs a schedule, a review, or a cancel before it goes out. Sending with a future `send_time` returns the estimate and leaves the message `Scheduled` until then; see [Message API - Sending](https://envoke-developers.pages.dev/v2/messages/sending/).
Points that apply when you choose:
- **Neither API accepts an idempotency key.** Each Send API request and each `POST https://e1.envoke.com/v2/messages/send` (create and send) request creates a new message, so repeating one after a timeout can send the email twice. Sending an existing draft with `POST https://e1.envoke.com/v2/messages/{ID}/send` cannot send it twice: once sent, the message is no longer a `Draft` and a second request fails with `Only 'Draft' messages can be updated, this message has the status: 'Scheduled'` (or the message's current status).
- **Create and send can leave a draft behind.** When `POST https://e1.envoke.com/v2/messages/send` creates the draft but the send step fails, the draft is kept and its ID is not returned. Give each message a unique `name` so the draft can be found with a name filter (see [Create and send](https://envoke-developers.pages.dev/v2/messages/sending/#create-and-send)).
- **Send API messages are also messages.** A message sent through the Send API can be read, and deleted once its status is `Sent`, through the Message API, and its activity is in the Reports API under the `id` the Send API returned.
---
## The Reports API and the Reports in Envoke
The [Reports API](https://envoke-developers.pages.dev/v2/reports/fields/) returns the email activity counts behind the reports in Envoke as JSON. It is read-only, on `/v2`, and has no CSV format. The Message API returns no delivery, open, or click statistics; read them from the Reports API.
| In Envoke | In the Reports API |
|---|---|
| The Sent message report of one message | [Message Stats](https://envoke-developers.pages.dev/v2/reports/message-stats/): `GET https://e1.envoke.com/v2/reports/messages/{ID}` |
| The folder report | The [message activity list](https://envoke-developers.pages.dev/v2/reports/messages/) or the compare report with `group_id`, which includes subfolders at any depth |
| Clicking a count in the Sent message report to see its contacts | [Message Contacts](https://envoke-developers.pages.dev/v2/reports/message-contacts/): `GET https://e1.envoke.com/v2/reports/messages/{ID}/contacts` |
| A contact's Email sending history | [Contact Activity](https://envoke-developers.pages.dev/v2/reports/contacts/): `GET https://e1.envoke.com/v2/reports/contacts/{ID}` |
The numbers are not always the same as the Sent message report's rows:
- **Every metric counts distinct contacts.** The Sent message report's Opens and Bounced rows count one per contact per email client or bounce type, so they can be larger than the API's `opens` and `bounces` (see [How the counts work](https://envoke-developers.pages.dev/v2/reports/fields/#how-the-counts-work)).
- **A rate with nothing to divide by is `null`**, where the Sent message report shows `0` (see [Rates](https://envoke-developers.pages.dev/v2/reports/fields/#rates)).
- **Counts are read from a reporting copy of the database**, so activity from the last few moments may not appear yet.
- **Which messages are listed.** The message activity list includes sent campaigns and automated messages. Drafts, scheduled messages, and messages with the `Transactional` status are not included (see [Which messages are included](https://envoke-developers.pages.dev/v2/reports/messages/#which-messages-are-included)).
- **Times** are in your account's time zone, the zone the Envoke app and the Message API use.
On an account restricted from seeing contacts associated with email activity (the setting that hides a contact's Email sending history in Envoke), the Contact Activity and Message Contacts endpoints return HTTP `403` (see [Contact Activity errors](https://envoke-developers.pages.dev/v2/reports/contacts/#errors)), and the `email_open` and `email_click` webhook types cannot be subscribed to: they fail with `Invalid webhook type: 'email_open'`.
To receive each event as it happens instead of reading counts, subscribe to the email activity webhooks; see [Hooks API - Webhook Payloads](https://envoke-developers.pages.dev/v1/hooks/payloads/).
---
## Before Your First Send
A send can be refused, or reach no one, because of account settings that are made in Envoke rather than through the API. Check each of these before your first send.
1. **The API is enabled and your key works.** See [Before Your Key Works](https://envoke-developers.pages.dev/guides/authentication/#before-your-key-works).
2. **The Transactional API preference is set (Send API only).** In Envoke, go to **Account settings → Integrations → Transactional API** and choose a **Consent status** and enter a **Consent description**. They are given to the contacts the Send API creates. Until they are set, every Send API request fails with `A default consent status and description is required in order to use this API`. The Message API does not use this preference.
3. **A sender identity.** Both APIs send from the account's default sender identity unless the request names another one with `sender`. A Message API draft whose sender has been cleared cannot be sent: `The field 'sender' must be set`. Manage identities with the [Sender API](https://envoke-developers.pages.dev/v1/senders/fields/), which refuses addresses at free or personal email providers. Envoke does not check that an identity is verified before it sends; the Sender API's [`dkim_status`](https://envoke-developers.pages.dev/v1/senders/fields/#dkim_status) reports whether DKIM is configured for the identity's domain, which improves deliverability.
4. **Recipients whose consent allows sending.** Apart from mandatory messages, existing contacts are sent a message only when their consent allows sending (for example `Express`, not `Not Provided`, `Revoked`, or `Expired`), their address is valid and has not been marked undeliverable, and, when your account requires email confirmation, they have confirmed it. The responses count each excluded recipient by reason (see [Consent](https://envoke-developers.pages.dev/v1/send/fields/#consent) and [Recipient counts](https://envoke-developers.pages.dev/v2/messages/sending/#recipient-counts)).
5. **The mandatory consent subscription (mandatory messages only).** A mandatory message, sent with `is_mandatory: true` on the Send API or to the mandatory consent subscription as `recipient_subscription` on the Message API, reaches only contacts who hold that subscription, whatever their consent status, except contacts who have reported spam. Contacts who do not hold it are excluded as `excluded_mandatory_consent` on the Send API.
6. **Credits.** A prepaid account needs a credit balance at least as large as the number of emails to be sent; otherwise the Send API fails with `Message not sent, the account is out of credits` and the Message API with `Email can't be sent, the account is out of credits`. A monthly-billed account that is not yet on a paid plan, and has too few credits, gets `Message not sent, the account has not upgraded` or `Email can't be sent, the account has not upgraded`.
For a walkthrough that creates a contact, a template, and a webhook subscription and then sends, see [Your first integration](https://envoke-developers.pages.dev/guides/first-integration/).
---
*Full reference, OpenAPI description, and Postman collection: [developers.envoke.com](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/guides/first-integration/
Markdown: https://envoke-developers.pages.dev/guides/first-integration.md
# Your first integration
This walkthrough builds a small integration end to end with curl: it defines a custom field and a tag, creates or updates a contact that uses them, creates a template from HTML, subscribes a webhook to the result, sends the template to the contact, and receives the webhook. The examples use a fictional organisation, the Birchwood Community Garden, which assigns garden plots to its members. Each step links to the full reference for the endpoint it uses.
---
## Before You Start
- **Credentials.** You need your API ID and API Key, and the API must be enabled for your account; see [Authentication](https://envoke-developers.pages.dev/guides/authentication/). The examples read the credentials from the `API_ID` and `API_KEY` shell variables set up in [Make a Request](https://envoke-developers.pages.dev/guides/authentication/#make-a-request).
- **Send prerequisites.** Step 5 uses the Send API, which needs the Transactional API preference set in Envoke, a sender identity, and credits. See [Before Your First Send](https://envoke-developers.pages.dev/guides/which-api/#before-your-first-send).
- **Real data.** Every request in this walkthrough changes your account, and step 5 sends a real email. Use an email address you control in place of `ama.osei@example.org`.
- **Limits.** Every request counts towards your account's [API limits](https://envoke-developers.pages.dev/guides/authentication/#api-limits).
---
## Step 1: Create the Custom Field and the Tag
Custom fields and tags are defined once for the account, then set on each contact through the Contact API. Skip this step for any that already exist; to check, filter by name with `GET https://e1.envoke.com/v2/customfields?filter%5Bname%5D=Plot_Number` and `GET https://e1.envoke.com/v2/tags?filter%5Bname%5D=Volunteer`, which return `[]` when there is no match.
Create a custom field to hold each member's plot number:
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Plot_Number", "field_type": "text", "description": "Garden plot assigned to the member"}' \
"https://e1.envoke.com/v2/customfields"
```
```json
{
"success": true,
"result_text": "Created",
"result_data": {
"id": "9201",
"name": "Plot_Number",
"description": "Garden plot assigned to the member",
"field_type": "text"
}
}
```
A custom field name can contain only letters, digits, and underscores. A name that is already used fails with `A custom field named 'Plot_Number' already exists`. See [Custom Field API - Insert](https://envoke-developers.pages.dev/v2/customfields/insert/).
Create a tag for members who volunteer:
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Volunteer"}' \
"https://e1.envoke.com/v2/tags"
```
```json
{
"success": true,
"result_text": "Created",
"result_data": {
"id": "9101",
"name": "Volunteer",
"group": ""
}
}
```
*(`result_data` shortened; the response contains every tag field.)*
See [Tag API - Insert](https://envoke-developers.pages.dev/v2/tags/insert/). The Contact API refers to the custom field and the tag by name, not by these IDs.
---
## Step 2: Create or Update the Contact
Create the contact, or update it if it already exists, with one upsert request. `upsert=1` turns a create into an update when a contact already matches, and `key_field=remote_id` matches on your own identifier instead of the email address:
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "ama.osei@example.org", "remote_id": "BCG-0417", "first_name": "Ama", "last_name": "Osei", "consent_status": "Express", "consent_description": "Signed the member volunteer form, 2026-04-18", "custom_fields": {"Plot_Number": "B-14"}, "interests": ["Volunteer"]}' \
"https://e1.envoke.com/v1/contacts?upsert=1&key_field=remote_id"
```
The first time, no contact has the remote ID `BCG-0417`, so the contact is created and returned in full:
```json
{
"success": true,
"result_text": "The contact was successfully created.",
"result_data": {
"id": "9001",
"external_id": "5c1e9a3f7b2d4e6a8c0f1b3d5e7a9c2e",
"date_created": "2026-10-28T13:05:12+00:00",
"remote_id": "BCG-0417",
"first_name": "Ama",
"last_name": "Osei",
"email": "ama.osei@example.org",
"consent_status": "Express",
"consent_description": "Signed the member volunteer form, 2026-04-18",
"consent_effective_date": "2026-10-28T13:05:12+00:00",
"consent_expiry": "",
"custom_fields": { "Plot_Number": "B-14" },
"interests": ["Volunteer"],
"autoresponders": []
}
}
```
*(`result_data` shortened; the response contains every field, with `""` for those not set.)*
Sending the same request again matches the contact by `remote_id` and updates it: `result_text` is `The contact was successfully updated.` and `result_data` is the full, updated contact. The Send API sends only to existing contacts whose consent allows sending, which is why the contact is given `Express` consent here; see [Consent fields](https://envoke-developers.pages.dev/v1/contacts/fields/#consent-fields).
Points to know about this request:
- **The parameter is `key_field`, not `keyfield`.** `?keyfield=remote_id` is ignored without an error, so the request matches by `email` instead.
- **`email` is required** even when `key_field=remote_id` is used. If your account checks email addresses for duplicates, an email that belongs to another contact fails with `contact with email = ama.osei@example.org already exists`.
- **More than one match is an error**, even with `upsert`: `Multiple contacts already exist with 'remote_id' value 'BCG-0417'. (IDs: 9001, 9002)`.
- **An upsert that updates differs from a create.** Empty values are ignored, a missing `consent_status` leaves the contact's current status unchanged, and a consent downgrade is recorded in the contact's history without becoming current unless `consent_force_downgrade` is `true`. See [Upsert: update when the contact exists](https://envoke-developers.pages.dev/v1/contacts/insert/#upsert-update-when-the-contact-exists).
- **`interests` as a list only adds.** To remove the tag, send the object form, `{"Volunteer": "Unset"}`.
- **Custom field keys.** On write, `custom_fields` keys match the field name ignoring case, with a space and an underscore treated as the same, so `"plot number"` writes to `Plot_Number`. In read filters the name is case-sensitive: `filter[custom_fields.Plot_Number]=B-14`. See [`custom_fields`](https://envoke-developers.pages.dev/v1/contacts/fields/#custom_fields).
- **Warnings are prose.** An unrecognised `country` or `language` value does not fail the request; a warning is appended to `result_text`, after a second period on an upsert that updates: `The contact was successfully updated.. 'Narnia' is not a valid value for country; field will not be updated`. See [Warnings](https://envoke-developers.pages.dev/v1/contacts/fields/#warnings).
- **Errors come in three shapes**, depending on which check fails: the validation envelope with `result_data.errors`, a field-validation envelope with `result_data.fields` keyed by database column, and a plain envelope for an empty or unreadable body. See [Contact API errors](https://envoke-developers.pages.dev/v1/contacts/fields/#error-responses).
---
## Step 3: Create a Template
Create a template from your own HTML:
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Plot Assignment", "html": "
Hi {@first_name},
Your plot for the season is {PLOT_NUMBER}. The garden opens on Saturday at 9:00 a.m.
"}' \
"https://e1.envoke.com/v2/templates"
```
```json
{
"success": true,
"result_text": "Created",
"result_data": {
"id": "9301",
"external_id": "8d2f4b6a1c3e5f7a9b0d2c4e6f8a1b3d",
"preview_url": "https://engage.envoke.com/m/b4d6f8a0c2e4/m/8d2f4b6a1c3e5f7a9b0d2c4e6f8a1b3d",
"name": "Plot Assignment",
"status": "published",
"language": "en",
"group": "",
"create_time": "2026-10-28 09:07:40",
"modified_time": "2026-10-28 09:07:40"
}
}
```
The HTML holds two placeholders: `{@first_name}` is an Envoke merge field, filled in from the contact, and `{PLOT_NUMBER}` is your own text, filled in by the send request in step 5. The HTML is stored as sent, up to 200 kB; it is not returned in this response, only by `GET https://e1.envoke.com/v2/templates/9301?include_html=1`. `create_time` and `modified_time` are in your account's time zone. See [Template API - Insert](https://envoke-developers.pages.dev/v2/templates/insert/).
The HTML has no unsubscribe link of its own, so the Send API adds Envoke's standard unsubscribe footer to the end of the email (see [Tracking and merge fields](https://envoke-developers.pages.dev/v1/send/fields/#tracking-and-merge-fields)).
---
## Step 4: Subscribe to the Result
Subscribe a webhook before sending, so that your endpoint hears about the email. An `email_send` event fires when an email is sent to a contact:
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "email_send", "url": "https://hooks.example.org/envoke/email-send", "filter": {"contact_remote_id": "BCG-0417"}}' \
"https://e1.envoke.com/v1/hooks.json"
```
Envoke returns HTTP `201` with the subscription's ID:
```json
{"id": "4f6a8c0e2b4d6f8a1c3e5b7d9f1a3c5e"}
```
The filter limits deliveries to events for the contact with the remote ID `BCG-0417`. Without it, your endpoint receives `email_send` events for all of your account's contacts. A filter key must be a field of the delivered payload, or every event is skipped.
Subscriptions cannot be changed; delete one and create another instead. Creating a subscription with the same `type` and `url` as an existing one fails with HTTP `400`, `A webhook subscription with this type and URL already exists`, and the existing subscription's ID in `result_data.id`. To hear about failed emails, subscribe to `email_bounce` the same way. See [Hooks API - Create](https://envoke-developers.pages.dev/v1/hooks/insert/) and [`filter`](https://envoke-developers.pages.dev/v1/hooks/fields/#filter).
---
## Step 5: Send the Template to One Address
Send the template to the contact with the Send API:
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Plot Assignment BCG-0417 2026-10-28", "subject": "Your garden plot is ready", "template_id": 9301, "enable_merge_fields": true, "recipients": [{"email": "ama.osei@example.org", "replace": {"{PLOT_NUMBER}": "B-14"}}]}' \
"https://e1.envoke.com/v1/send"
```
```json
{
"success": true,
"result_text": "Message is sending",
"result_data": {
"id": 9401,
"new_contacts_created": 0,
"summary": {
"total_selected": 1,
"emails_to_be_sent": 1,
"excluded_unsubscribed_contacts": 0,
"excluded_invalid_emails": 0,
"excluded_bounced_emails": 0,
"excluded_unverified_emails": 0
},
"recipients": [
{ "id": 9001, "email": "ama.osei@example.org", "emails_to_be_sent": true }
]
}
}
```
The request creates a message from the template's HTML and queues the email at once. `enable_merge_fields` fills in `{@first_name}`, and the recipient's `replace` value fills in `{PLOT_NUMBER}` as the email is delivered. The message is sent from your account's default sender identity, filed in the `API Messages` group, and sent without open or click tracking. `result_data.id` is the new message's ID, as an integer; the Message API returns the same message with `id` `"9401"`. See [Send API - Send](https://envoke-developers.pages.dev/v1/send/send/).
Points to know about this request:
- **Check each recipient, not only `success`.** `success` is `true` when at least one recipient is sent the message. A recipient who is not sent it is listed with the reason, such as `excluded_unsubscribed_contacts`, and counted in `summary` (see [Response](https://envoke-developers.pages.dev/v1/send/send/#response)). When no recipient can be sent it, the response is HTTP `400` with `Message not sent, there are no valid recipients` and no message is created.
- **An address that matches no contact becomes a contact.** It is created with the consent from your Transactional API preference and sent the message. Recipients are not de-duplicated, and an address in a different letter case from the stored one is treated as a different recipient. A request refused for credits (`Message not sent, the account is out of credits` or `Message not sent, the account has not upgraded`) has already created contacts for any addresses that matched none.
- **The subject is not copied from the template**, so `subject` is required, at least 6 characters.
- **`replace` values are inserted as written**, without escaping, so a value can contain HTML. An invalid `replace` excludes that recipient as `excluded_invalid_replace_data` instead of failing the request (see [`replace`](https://envoke-developers.pages.dev/v1/send/fields/#replace)).
- **Do not repeat a send blindly.** The Send API has no idempotency key: each request that succeeds creates a new message and sends it, so repeating a request whose response you did not receive can send the email twice. Give each send a unique `name`, and look it up before sending again: `GET https://e1.envoke.com/v2/messages?filter%5Bname%5D=Plot%20Assignment%20BCG-0417%202026-10-28` returns `[]` when no message has that name. A message with that name means the earlier request created it; read its `status` before deciding to send again.
- **There is no test mode.** Every successful request sends real email.
To follow the message itself, read it with `GET https://e1.envoke.com/v2/messages/9401`: its `status` is `Sending`, then `Sent` once every email has been handed off for delivery.
---
## Step 6: Receive the Webhook
When the email is sent, Envoke sends an HTTP POST to the subscription's URL with a JSON array of events:
```json
[
{
"activity_type": "email_send",
"timestamp": 1793193131,
"message_id": "9401",
"message_external_id": "e1a3c5b7d9f1a3c5e7b9d1f3a5c7e9b1",
"message_name": "Plot Assignment BCG-0417 2026-10-28",
"message_subject": "Your garden plot is ready",
"sender": "Birchwood Community Garden ",
"contact_id": "9001",
"contact_external_id": "5c1e9a3f7b2d4e6a8c0f1b3d5e7a9c2e",
"contact_email": "ama.osei@example.org",
"contact_remote_id": "BCG-0417"
}
]
```
`message_id` matches the `id` from step 5, and `contact_remote_id` is the identifier you gave the contact in step 2. `message_subject` is the stored subject, with any merge fields not filled in. The fields of every event type are in [Hooks API - Webhook Payloads](https://envoke-developers.pages.dev/v1/hooks/payloads/#email_send).
Your endpoint should handle these rules:
- **Read the body as an array.** `email_send` events are batched, so one POST can carry several events (see [Delivery Format](https://envoke-developers.pages.dev/v1/hooks/payloads/#delivery-format)).
- **Return a `2xx` status.** On any other status, Envoke retries once after 10 minutes. A `410` response removes the subscription.
- **Deliveries carry no signature or secret.** The request has no header that identifies it as coming from Envoke.
- **Deliveries count towards your API limits.** Each event Envoke delivers counts as one request towards your account's [API limits](https://envoke-developers.pages.dev/guides/authentication/#api-limits), alongside the requests you make.
Without `"track_recipient_activity": true`, a Send API message has no open-tracking image and its links are not rewritten to record clicks, so it produces no `email_open` or `email_click` events.
---
## Next Steps
- [Which API do I use?](https://envoke-developers.pages.dev/guides/which-api/) compares the Send API with the Message API, which sends to a tag, subscription, or segment with a draft, an estimate, a schedule, and a cancel.
- [Tag API - Add and Remove Contacts](https://envoke-developers.pages.dev/v2/tags/contacts/) adds up to 1,000 contacts to a tag in one request.
- [Message Stats](https://envoke-developers.pages.dev/v2/reports/message-stats/) returns the sends, opens, clicks, and bounces of the message from step 5.
---
*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).*
---
URL: https://envoke-developers.pages.dev/v1/contacts/fields/
Markdown: https://envoke-developers.pages.dev/v1/contacts/fields.md
# Contact API - Fields Reference
This article describes all fields available through the Contact API. Use this as a reference when reading, creating, or updating contacts.
---
## Overview
The Contact API lets you read, create, update, and delete the contacts in your Envoke account. A contact is one person: their email address and profile fields, their consent status, the values of your account's custom fields, and the tags, subscriptions, and nurture campaigns (autoresponders) they belong to.
The Contact API answers on `/v1` only. Related resources have their own APIs: custom fields are defined through the [Custom Field API](https://envoke-developers.pages.dev/v2/customfields/fields/), and tags and subscriptions through the [Tag API](https://envoke-developers.pages.dev/v2/tags/fields/) and [Subscription API](https://envoke-developers.pages.dev/v2/subscriptions/fields/). Those APIs also offer bulk add/remove endpoints for contacts; on the Contact API, tags, subscriptions, and mandatory consent are all written through the single `interests` field.
All endpoints require [HTTP Basic Auth](https://envoke-developers.pages.dev/guides/authentication/) using your API ID and API Key.
---
## Fields
| Field | Type | Read-only | Required on Create | Description |
|---|---|---|---|---|
| `id` | string | Yes | — | Envoke contact ID, assigned on creation. |
| `external_id` | string | Yes | — | Externally visible identifier used in public-facing links. |
| `date_created` | datetime | Yes | — | When the contact was created, in UTC. |
| `remote_id` | string | No | Only with `key_field=remote_id` | Your own identifier for the contact, e.g. a CRM or membership ID. |
| `first_name` | string | No | No | First name. |
| `last_name` | string | No | No | Last name. |
| `title` | string | No | No | Job title or honorific. |
| `email` | string | No | Yes | Email address. |
| `company` | string | No | No | Company or organisation. |
| `phone` | string | No | No | Phone number (free text). |
| `address_1` | string | No | No | Street address, line 1. |
| `address_2` | string | No | No | Street address, line 2. |
| `city` | string | No | No | City. |
| `country` | string | No | No | Country, returned as a 2-letter ISO code. |
| `province` | string | No | No | Province or state (free text). |
| `postal_code` | string | No | No | Postal or ZIP code. |
| `website` | string | No | No | Website URL (free text). |
| `language` | string | No | No | Preferred language, `en` or `fr`. |
| `feedback` | string | No | No | Free-text notes. HTML is stripped. |
| `consent_status` | string | No | No | Current consent type. Defaults to `Not Provided` on create. |
| `consent_description` | string | No | No | How and when consent was obtained. |
| `consent_effective_date` | datetime | No | No | When the current consent took effect, in UTC. |
| `consent_expiry` | string | Yes | — | Expiry date of the current consent (`YYYY-MM-DD`), or `""`. |
| `consent_force_downgrade` | boolean | Write-only | No | Forces a consent change that would otherwise be ignored as a downgrade. Never returned. |
| `custom_fields` | object | No | No | Custom field values keyed by custom field name. |
| `interests` | array | No | No | Names of the contact's tags, subscriptions, and mandatory consent items. |
| `autoresponders` | array | No | No | Names of the nurture campaigns the contact is on. |
---
## Field Details
Every field is present in a read response unless `fields` is used to select some. An unset value is returned as `""` (never `null`); this also applies to a stored value of `"0"`. `custom_fields` is returned as `{}` and `interests` and `autoresponders` as `[]` when empty. Text fields have the maximum lengths shown below; a longer value fails validation with the message given (see [Error Responses](#error-responses)). On some older accounts, columns are shorter than the limits given here; there, a value that passes validation is cut to the column's length without an error.
### `id`
- **Type:** String
- **Read-only:** Yes — assigned automatically on creation.
- **Returned value (read):** The contact ID as a string.
- **Example:** `"1001"`
Ignored if sent on create. On update, it identifies the contact (see [Update](https://envoke-developers.pages.dev/v1/contacts/update/)). The exceptions to the string type are the `PATCH /v1/contacts/{id}` response and the per-contact results of a bulk update, whose `result_data.id` is an integer (see [Update](https://envoke-developers.pages.dev/v1/contacts/update/)).
---
### `external_id`
- **Type:** String (32 characters)
- **Read-only:** Yes — assigned automatically on creation.
- **Example:** `"9f2c4e7a1b3d5f6a8c0e2b4d6f8a1c3e"`
An externally visible identifier used in public-facing contexts such as preference-page links. Not the same as `id`. Silently ignored if sent on create or update.
---
### `date_created`
- **Type:** Datetime
- **Read-only:** Yes — set automatically on creation.
- **Returned value (read):** W3C format in UTC.
- **Example:** `"2026-10-05T14:03:11+00:00"`
Silently ignored if sent on create or update. Supports the range filter operators (see [Read](https://envoke-developers.pages.dev/v1/contacts/read/)).
---
### `remote_id`
- **Type:** String, max 60 characters
- **Read-only:** No
- **Required on create:** Only when `key_field=remote_id` is set on the request.
- **Returned value (read):** The stored value, or `""` when unset.
- **Example:** `"HCS-0002"`
Your own identifier for the contact. It can be used instead of `email` to find a contact on create (`key_field=remote_id`) and on update (`key_field=remote_id`). Too long: `Remote ID must not exceed 60 characters`.
---
### `first_name`
- **Type:** String, max 50 characters
- **Read-only:** No
- **Required on create:** No
- **Example:** `"Maya"`
Too long: `First name must not exceed 50 characters`.
---
### `last_name`
- **Type:** String, max 50 characters
- **Read-only:** No
- **Required on create:** No
- **Example:** `"Okafor"`
Too long: `Last name must not exceed 50 characters`.
---
### `title`
- **Type:** String, max 50 characters
- **Read-only:** No
- **Required on create:** No
- **Example:** `"Section Lead"`
Too long: `Title must not exceed 50 characters`.
---
### `email`
- **Type:** String, max 60 characters
- **Read-only:** No
- **Required on create:** Yes, including when `key_field=remote_id` is used.
- **Accepted values (write):** A valid email address, sent as a JSON string.
- **Example:** `"alto.lead@example.com"`
Errors on create: `Email is required` (missing or empty), `Email must be a string` (any non-string JSON value), `Email invalid` (bad format), `Email must not exceed 60 characters`. On update, `email` can be changed but not cleared: `Email must not be empty`.
Each account has one duplicate-check field (usually `email`). A create or update that would give two contacts the same value fails with `contact with email = {value} already exists`. Envoke support can change this setting for accounts that keep several contacts per email address and key on `remote_id`.
---
### `company`
- **Type:** String, max 80 characters
- **Read-only:** No
- **Required on create:** No
- **Example:** `"Harbourfront Choral Society"`
Too long: `Company must not exceed 80 characters`.
---
### `phone`
- **Type:** String, max 25 characters
- **Read-only:** No
- **Required on create:** No
- **Example:** `"416-555-0142"`
Free text; no format check. Too long: `Phone must not exceed 25 characters`.
---
### `address_1`
- **Type:** String, max 100 characters
- **Read-only:** No
- **Required on create:** No
- **Example:** `"48 Harbour Lane"`
Too long: `Address must not exceed 100 characters`.
---
### `address_2`
- **Type:** String, max 100 characters
- **Read-only:** No
- **Required on create:** No
- **Example:** `"Unit 5"`
Too long: `Address 2 must not exceed 100 characters`.
---
### `city`
- **Type:** String, max 50 characters
- **Read-only:** No
- **Required on create:** No
- **Example:** `"Toronto"`
Too long: `City must not exceed 50 characters`.
---
### `country`
- **Type:** String
- **Read-only:** No
- **Required on create:** No
- **Accepted values (write):** A 2-letter ISO code (`"CA"`) or a recognised country name (`"Canada"`, `"France"`, `"U.S.A."`, `"UK"`).
- **Returned value (read):** The 2-letter ISO code, or `""`.
- **Example:** `"CA"`
A value that is not recognised is not saved and produces a warning (see [Warnings](#warnings)): `'Narnia' is not a valid value for country; field will not be updated`. The country decides which consent rules apply to the contact (see [Consent fields](#consent-fields)); a contact with no country is treated as being in your account's default consent country.
---
### `province`
- **Type:** String, max 50 characters
- **Read-only:** No
- **Required on create:** No
- **Example:** `"ON"`
Free text; no list check. Too long: `Province must not exceed 50 characters`.
---
### `postal_code`
- **Type:** String, max 15 characters
- **Read-only:** No
- **Required on create:** No
- **Example:** `"M5V 0Z9"`
Too long: `Postal / zip must not exceed 15 characters`.
---
### `website`
- **Type:** String, max 255 characters
- **Read-only:** No
- **Required on create:** No
- **Example:** `"https://example.org"`
Free text; no URL check. Too long: `Website must not exceed 255 characters`.
---
### `language`
- **Type:** String
- **Read-only:** No
- **Required on create:** No
- **Accepted values (write):** `en` or `fr`, or the names `English` or `French`. Case-insensitive.
- **Returned value (read):** `"en"`, `"fr"`, or `""`.
- **Example:** `"en"`
Any other value is not saved and produces a warning (see [Warnings](#warnings)): `'de' is not a valid value for language; field will not be updated`.
---
### `feedback`
- **Type:** String
- **Read-only:** No
- **Required on create:** No
- **Example:** `"Happy to help with ushering at the winter concert."`
HTML tags are removed before the value is saved. No error is returned when this happens.
---
### `consent_status`
- **Type:** String
- **Read-only:** No
- **Required on create:** No — a new contact without one is given `Not Provided`.
- **Accepted values (write):** One of the writable consent types in the [Consent fields](#consent-fields) table. Case-insensitive; a space and an underscore are treated as the same character.
- **Returned value (read):** The name of the contact's current consent type.
- **Example:** `"Express"`
An unknown value fails validation with the list of writable values: `Invalid 'consent_status': 'Opted In' (possible values: 'Express', 'Implied - Inquiry', 'Implied - Transaction', 'Implied - No Expiry', 'Not Provided', 'Revoked', 'Envoke Spam Reported')`. A valid value does not always become current; see the transition rules under [Consent fields](#consent-fields).
---
### `consent_description`
- **Type:** String, max 500 characters
- **Read-only:** No
- **Required on create:** No
- **Example:** `"Signed up at the spring concert, 2026-09-12"`
A record of how consent was obtained. Text beyond 500 characters is cut off without an error. Not required, even when `consent_status` changes. Sending only `consent_description` or `consent_effective_date` on update re-submits the contact's current status with the new details, subject to the same transition rules.
---
### `consent_effective_date`
- **Type:** Datetime
- **Read-only:** No
- **Required on create:** No — defaults to the time of the request.
- **Accepted values (write):** A date and time, e.g. `"2026-09-12T18:30:00Z"` or `"2026-09-12 18:30:00"`. Without a time zone, the time is read as UTC (see [Dates](#dates)).
- **Returned value (read):** W3C format in UTC.
- **Example:** `"2026-09-12T18:30:00+00:00"`
A date in the future fails with `consent_effective_date cannot be in the future.`; an unreadable value fails with `Invalid date format for consent_effective_date`. The expiry of implied consent is calculated from this date.
---
### `consent_expiry`
- **Type:** String (`YYYY-MM-DD`)
- **Read-only:** Yes — calculated from `consent_status` and `consent_effective_date`.
- **Returned value (read):** The expiry date, or `""` when the consent does not expire or expiry does not apply to the contact.
- **Example:** `"2027-03-12"`
Silently ignored if sent on create or update. See [Consent fields](#consent-fields) for when it is blank.
---
### `consent_force_downgrade`
- **Type:** Boolean
- **Read-only:** Write-only — never returned.
- **Required on create:** No
- **Accepted values (write):** `true`, `"true"`, `1`, `"1"`, `"yes"`, `"on"`, or `"Set"`. Anything else is treated as false.
- **Example:** `true`
Sent alongside `consent_status` to make a downgrade take effect (see [Consent fields](#consent-fields)). Sent on its own, it does nothing.
---
### `custom_fields`
- **Type:** Object
- **Read-only:** No
- **Required on create:** No
- **Format:** `{ "": "" }`
- **Returned value (read):** An object of every custom field that has a value for the contact, keyed by the field name exactly as defined in your account. `{}` when there are none.
- **Example:** `{ "Voice_Part": "Alto", "Season_Ticket_Number": "HCS-ST-0417" }`
| Aspect | Behaviour |
|---|---|
| Keys on write | The custom field name. Matching ignores case and treats a space and an underscore as the same, so `"voice part"` writes to `Voice_Part`. Custom field IDs are not accepted as keys. |
| Unknown key | `Invalid 'custom_field': 'Voice Range' (for a list of valid custom fields try the endpoint '/customfields')` |
| Not an object | `'custom_fields' must be an object` |
| Values | Stored as text. Values are not checked against the field's type. |
| `""` value | Deletes the contact's value for that field. On update this works without `write_empty_values` (see [Update](https://envoke-developers.pages.dev/v1/contacts/update/)). |
| Filter and sort | `custom_fields.{name}` (see [Read](https://envoke-developers.pages.dev/v1/contacts/read/)). The name is case-sensitive there. |
Fields you do not include are left unchanged; there is no way to replace the whole object in one request.
Note: A value that cannot be stored (for example `null`) is skipped, and the response still reports success.
Note: A custom field defined for conversion pages (lead scope) is accepted by name but never saved, and is not returned on read.
---
### Consent fields
The consent fields are `consent_status`, `consent_description`, `consent_effective_date`, `consent_expiry`, and `consent_force_downgrade`. Together they record the contact's current consent to receive email. Every change adds an entry to the contact's consent history; the transition rules below decide whether that entry becomes current.
#### Consent types
| Name | Sending allowed | Writable via API | Blocks delete | Notes |
|---|---|---|---|---|
| `Express` | Yes | Yes | No | Does not expire. |
| `Implied - Inquiry` | Yes | Yes | No | Expires 6 months after the effective date. Not applicable in the EU region. |
| `Implied - Transaction` | Yes | Yes | No | Expires 2 years after the effective date. Not applicable in the EU region. |
| `Implied - No Expiry` | Yes | Yes | No | Does not expire. |
| `Not Provided` | No | Yes | No | Default for new contacts. |
| `Revoked` | No | Yes | Yes | The contact unsubscribed. |
| `Envoke Spam Reported` | No | Yes | Yes | Also blocks subscriptions and later changes (see below). |
| `Expired` | No | No | No | Set automatically when implied consent expires. |
| `External Spam Reported` | No | No | Yes | Set automatically from a spam complaint. Blocks subscriptions and later changes. |
Older contacts can also be returned with one of the retired types `Implied - Existing Relationship`, `Legacy Subscribed`, `Legacy Confirmed`, or `Legacy Unknown`. These cannot be written.
#### When a change takes effect
| Change | Outcome |
|---|---|
| Upgrade or equal change (e.g. `Not Provided` → `Express`, `Implied - Inquiry` → `Implied - Transaction`) | Becomes current. |
| Downgrade, called *preempted* (e.g. `Express` → any implied type, a non-expiring type → an expiring one, a later expiry → a sooner one, any type → `Not Provided`) | Logged to history but not made current. Send `consent_force_downgrade: true` to make it current. |
| From `Envoke Spam Reported` or `External Spam Reported` to any other type | Blocked: nothing is recorded. `consent_force_downgrade` does not override this. |
| `Revoked` → `Not Provided` | Blocked: nothing is recorded. `consent_force_downgrade` does not override this. |
| `Revoked` → `Express` or another sending type | Becomes current. |
Note: A preempted or blocked change is not reported. The response is the normal success message, and the contact's `consent_status` is unchanged.
On update, consent is written only when `consent_status`, `consent_description`, or `consent_effective_date` differs from the stored value. Sending the current status again, with no other consent change, does nothing.
#### Expiry
`Implied - Inquiry` expires 6 months, and `Implied - Transaction` 2 years, after `consent_effective_date` (or after the time of the request when no date is sent). Other writable types do not expire. If the calculated expiry is already in the past, the contact's status is set to `Expired` straight away.
`consent_expiry` is returned as `""` on GET when the consent does not expire, when the contact's country is not `CA` (a contact with no country uses your account's default consent country), or when your account is set to ignore consent expiry. This blanking applies only to GET responses: the full contact returned by a create, an upsert, or `PATCH /v1/contacts` can show an expiry date that GET then returns as `""`.
#### EU contacts
`Implied - Inquiry` and `Implied - Transaction` do not apply to contacts in the EU consent region: AT, BE, BG, CY, CZ, DE, DK, EE, ES, FI, FR, GB, GF, GI, GP, GR, HR, HU, IE, IS, IT, LI, LT, LU, LV, MF, MQ, MT, NL, NO, PL, PT, RE, RO, SE, SI, SK, YT. Two cases add a sentence to the end of `result_text`; the request still succeeds.
| Case | Sentence appended to `result_text` |
|---|---|
| An implied type is written for an EU contact | ` Their consent status was not updated because Implied - Inquiry does not apply to contacts in the EU.` The status is left as it was. For a new contact, or when `consent_force_downgrade` is sent, the wording is `set to Not Provided` instead of `not updated`, and the status becomes `Not Provided`. |
| An existing contact with an implied type has `country` changed to an EU country, with no consent fields in the same request | ` Their consent status was updated because Implied - Inquiry consent does not apply to contacts in the EU.` The status becomes `Not Provided` with the description `Implied - Inquiry consent does not apply to contacts in the EU.` On `PATCH /v1/contacts/{id}`, `result_data` also gets `inapplicable_consent_updated: true` and `consent_fix_message` holding the same sentence. |
The type name in each sentence is the one that does not apply (`Implied - Inquiry` or `Implied - Transaction`).
---
### `interests` and `autoresponders`
#### `interests`
- **Type:** Array (read); array or object (write)
- **Read-only:** No
- **Required on create:** No
- **Returned value (read):** Array of the names of every tag, subscription, and mandatory consent item the contact has.
- **Example:** `["Volunteer", "Alumni", "Concert News"]`
`interests` is the single field on the v1 Contact API for tags, subscriptions, and mandatory consent; there are no separate `tags` or `subscriptions` keys. The [Tag API](https://envoke-developers.pages.dev/v2/tags/contacts/) and [Subscription API](https://envoke-developers.pages.dev/v2/subscriptions/contacts/) have dedicated endpoints for adding and removing contacts in bulk.
| Form | Example | Effect |
|---|---|---|
| List of names | `["Volunteer", "Concert News"]` | Adds each one. Nothing is removed. |
| Object of states | `{ "Volunteer": "Set", "Alumni": "Unset" }` | `"Set"` or `1` adds; `"Unset"` or `0` removes. |
- **Keys and names:** matched by name, ignoring case and treating a space and an underscore as the same. In the object form, a key can also be the item's numeric ID. The numeric states (`1`, `0`) must be JSON numbers; a string such as `"1"` is read as a name.
- **Unknown name:** `Invalid 'interest': 'Volunteers' (for a list of valid interests try the endpoint '/interests')`
- **Wrong type:** `'interests' must be an object or array`
Note: Adding a subscription does not check the contact's sending consent. A contact whose consent is `Not Provided` is added to the subscription like any other contact.
#### `autoresponders`
- **Type:** Array (read); array or object (write)
- **Read-only:** No
- **Required on create:** No
- **Returned value (read):** Array of the names of the nurture campaigns the contact is on.
- **Example:** `["Welcome Sequence"]`
| Form | Example | Effect |
|---|---|---|
| List of names | `["Welcome Sequence"]` | Adds and queues each one. |
| Object of states | `{ "Welcome Sequence": "Set" }` | `"Set"`, `1`, or `2` adds and queues. |
| | `{ "Welcome Sequence": "Unset" }` | `"Unset"` or `0` removes. |
| | `{ "Welcome Sequence": "Dequeue" }` | `"Dequeue"` or `-1` stops further messages but leaves the contact on the campaign. |
- **Keys and names:** as for `interests`; an object key can also be the campaign's numeric ID.
- **Unknown name:** `Invalid 'autoresponder': 'Welcome' (for a list of valid autoresponders try the endpoint '/autoresponders')`
- **Wrong type:** `'autoresponders' must be an object or array`
Note: A campaign is not added (the contact is not put on it, and nothing is queued) when the contact's consent does not allow sending, unless the campaign is marked transactional (and never for a spam-reported contact). The skipped campaign is not reported, and the response still reports success.
Nurture campaigns cannot be created through the API; create them in Envoke first.
---
## Dates
| Field | On write | On read | In filters |
|---|---|---|---|
| `date_created` | Ignored | W3C in UTC, `"2026-10-05T14:03:11+00:00"` | Read as UTC unless the value has a time zone |
| `consent_effective_date` | Read as UTC unless the value has a time zone | W3C in UTC | Read as UTC unless the value has a time zone |
| `consent_expiry` | Ignored | `"YYYY-MM-DD"` or `""` | Compared as a plain `YYYY-MM-DD` date, no time zone conversion |
Send times as `2026-09-12T18:30:00Z`, `2026-09-12T14:30:00-04:00`, or `2026-09-12 18:30:00` (UTC). A value with a date but no time is not converted from UTC; it is read as midnight in Envoke's server time zone.
---
## Error Responses
**Validation errors** (unknown fields, invalid values, missing required fields, bad filters):
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"'nickname' is not a valid contact field",
"Invalid 'consent_status': 'Opted In' (possible values: 'Express', 'Implied - Inquiry', 'Implied - Transaction', 'Implied - No Expiry', 'Not Provided', 'Revoked', 'Envoke Spam Reported')"
]
}
}
```
**Field validation errors** (field lengths, email format, duplicate check). These come from a second check that runs after the one above, and use a different shape: `result_data.fields` is keyed by database column name, and `result_text` holds the messages joined with `, ` instead of the standard sentence.
```json
{
"success": false,
"result_text": "Email invalid, First name must not exceed 50 characters",
"result_data": {
"fields": {
"email": "Email invalid",
"fname": "First name must not exceed 50 characters"
}
}
}
```
Most column names match the API field name. These differ:
| API field | Key in `result_data.fields` |
|---|---|
| `first_name` | `fname` |
| `last_name` | `lname` |
| `address_1` | `addr1` |
| `address_2` | `addr2` |
| `postal_code` | `postal` |
| `external_id` | `eid` |
A duplicate-check failure has no per-field entry: `result_data.fields` is `[]` and `result_text` is `contact with email = alto.lead@example.com already exists`. On update, this envelope also carries a `warnings` key.
**Empty or unreadable body.** POST and PATCH with no body, an empty body, `{}`, or `[]` return `{"success":false,"result_data":[],"result_text":"Invalid request"}`. A body that is not valid JSON returns the same shape with `result_text` `Invalid JSON`.
All error responses above use HTTP status `400`. An invalid API ID or key returns `401` with a `WWW-Authenticate: Basic` header and the body `{"success":false,"result_data":[],"result_text":"Invalid credentials"}` (sent with `Content-Type: text/html`). 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 `"This account is not active."`) Exceeding your account's API limit returns `429` with `success: false` and a `result_text` naming the limit.
### Warnings
A `country` or `language` value that is not recognised does not fail the request. The field is left unchanged, the rest of the request is applied, and the response reports success with a warning such as `'Narnia' is not a valid value for country; field will not be updated`. Where the warning appears depends on the endpoint:
| Endpoint | Where warnings appear |
|---|---|
| `POST /v1/contacts` (create) | Appended to `result_text` after a space, joined with `. ` |
| `PATCH /v1/contacts/{id}` | `result_data.warnings`, an array of strings (`[]` when none) |
| `PATCH /v1/contacts` with an object body, and `POST /v1/contacts?upsert=1` when it updates | Appended to `result_text` after `. `, which follows the success message's own period: `The contact was successfully updated.. 'Narnia' is not a valid value for country; field will not be updated` |
| `PATCH /v1/contacts` with a list body | `result_data["{id}"].warnings` for each contact |
### Not found
**Not-found behaviour varies by method:**
- **GET** `/contacts/{id}` — when the ID is not found, returns an empty array (`[]`).
- **PATCH** `/contacts/{id}` — when the ID is not found, returns a validation error:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["No contact was found with ID: '999999'"]
}
}
```
- **PATCH** `/contacts` (object body) — when no contact matches the key field, returns `{"success":false,"result_data":[],"result_text":"No contact was found with the 'email': 'nobody@example.com'"}`. A body `id` sent without `key_field` is not looked up first, so a missing ID gives the validation error shown for `PATCH /contacts/{id}`.
- **PATCH** `/contacts` (list body) — the request returns HTTP `400` and the missing contact is reported in `result_data` under `CONTACT_OBJECT_{n}` (see [Update](https://envoke-developers.pages.dev/v1/contacts/update/)).
- **DELETE** `/contacts/{id}` — when the ID is not found, returns `{"success":false,"result_text":"1 contact not deleted, 1 - no matching contact found"}` with no `result_data` key.
---
*Full reference, OpenAPI description, and Postman collection: [developers.envoke.com](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/v1/contacts/read/
Markdown: https://envoke-developers.pages.dev/v1/contacts/read.md
# Contact API - Read
Use the read endpoints to retrieve contacts from your account. You can list contacts with filtering, sorting, and pagination, or retrieve a single contact by ID.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| GET | `https://e1.envoke.com/v1/contacts` | Return a list of contacts. |
| GET | `https://e1.envoke.com/v1/contacts/{ID}` | Return a single contact. |
Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## List Contacts
```
GET https://e1.envoke.com/v1/contacts
```
Returns a JSON array of contacts. Supports filtering, sorting, pagination, and field selection. With no `sort`, the order is not defined; use `sort` when you page through results.
### 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,email`. Use names from the [Fields reference](https://envoke-developers.pages.dev/v1/contacts/fields/). |
| `result_type` | string | *(array)* | Set to `kvp` to receive an object of contact ID to email address (`{ "1001": "alto.lead@example.com" }`). |
| `filter[{field}]` | string | — | Filter on a field. See [Filterable and sortable fields](#filterable-and-sortable-fields) and [Filter Operators](#filter-operators). |
| `sort[{field}]` | string | — | Sort on a field. Value: `asc` or `desc` (case-insensitive), or `1` or `-1`. Repeat for a secondary sort. |
**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 contact 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`, and `result_type` is treated as a filter, so `?email=alto.lead%40example.com` is the same as `?filter[email]=alto.lead%40example.com`. Any other parameter is rejected as an unknown filter field (for example `Field 'key_field' is not valid for filtering`). When `filter` is present, the other parameters are ignored as filters. Use `filter[...]` for custom fields: dots and spaces in a bare parameter name are not preserved.
### Filterable and sortable fields
| Field | Filter operators | Sortable |
|---|---|---|
| `id`, `external_id`, `remote_id`, `first_name`, `last_name`, `title`, `email`, `company`, `phone`, `address_1`, `address_2`, `city`, `country`, `province`, `postal_code`, `website`, `language`, `feedback` | equals, `(ne)` | Yes |
| `date_created` | equals, `(ne)`, `(gt)`, `(lt)`, `(gte)`, `(lte)` | Yes |
| `consent_status` | equals, `(ne)` | Yes |
| `consent_description` | equals, `(ne)` | No |
| `consent_effective_date`, `consent_expiry` | equals, `(ne)`, `(gt)`, `(lt)`, `(gte)`, `(lte)` | No |
| `custom_fields.{name}` | equals, `(ne)`, `(regex)` | Yes |
| `interests` | equals, `(ne)` | No |
| `autoresponders` | equals, `(ne)` | No |
- **`country`** is matched against the stored 2-letter code, so filter with `CA`, not `Canada`.
- **`custom_fields.{name}`** uses the custom field's name exactly as defined in your account, including case (names contain only letters, digits, and underscores): `filter[custom_fields.Voice_Part]=Alto`.
- **`interests`** takes the exact name (case-sensitive) of a tag, subscription, or mandatory consent item, and matches contacts that have it; `(ne)` matches contacts that don't. IDs are not accepted. An unknown name gives `Invalid 'interest' provided: 'Volunteers'`.
- **`autoresponders`** works the same way with a nurture campaign name. An unknown name gives `Invalid 'autoresponder' provided: 'Welcome'`.
- **Date fields:** `date_created` and `consent_effective_date` values are read as UTC unless they include a time zone. `consent_expiry` is compared as a plain `YYYY-MM-DD` date. See [Dates](https://envoke-developers.pages.dev/v1/contacts/fields/#dates).
### Filter Operators
| Operator | Example | Description |
|---|---|---|
| *(none)* | `?filter[city]=Toronto` | Equals. |
| `(ne)` | `?filter[city][(ne)]=Toronto` | Not equal. |
| `(regex)` | `?filter[custom_fields.Season_Ticket_Number][(regex)]=^HCS-ST-` | Matches a MySQL regular expression. Custom fields only. |
| `(gt)` | `?filter[date_created][(gt)]=2026-01-01T00:00:00Z` | Greater than. Date fields only. |
| `(lt)` | `?filter[date_created][(lt)]=2026-07-01T00:00:00Z` | Less than. Date fields only. |
| `(gte)` | `?filter[consent_effective_date][(gte)]=2026-09-01` | Greater than or equal. Date fields only. |
| `(lte)` | `?filter[consent_expiry][(lte)]=2027-03-31` | Less than or equal. Date fields only. |
| `(or)` | `?filter[(or)][0][email]=a%40example.com&filter[(or)][1][email]=b%40example.com` | Matches if any of the grouped conditions match. |
| `(not)` | `?filter[(not)][consent_status]=Express` | 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`, `$regex`, `$gt`, `$or`, and so on) that behaves the same way. These docs use the `(op)` spelling.
The `(in)` operator is not supported on any contacts field. To match one of several values, use `(or)`.
**Combining conditions.**
- Separate filters are ANDed: `?filter[city]=Toronto&filter[consent_status]=Express`.
- Two operators on one field are ANDed, which gives a range: `?filter[date_created][(gte)]=2026-01-01&filter[date_created][(lt)]=2026-07-01`.
- A query string cannot repeat a key, so to use the same field twice, put each condition in its own numbered group: `?filter[0][interests]=Volunteer&filter[1][interests]=Alumni` matches contacts with both tags. Groups inside `(or)` are numbered the same way.
- `(not)` wraps everything inside it: `?filter[(not)][city]=Toronto&filter[(not)][language]=fr` excludes contacts who are in Toronto *and* French-speaking.
Note: `(ne)` on a custom field matches only contacts that have a value for that field; contacts with no value are not returned.
### Errors
Invalid list parameters return HTTP `400` with every problem listed at once:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"Field 'nickname' is not valid for filtering",
"'REGEXP' is not a valid operator for the field 'email'"
]
}
}
```
| Error | Cause |
|---|---|
| `Invalid 'limit': 'ten'` | `limit` is not a whole number. |
| `Invalid 'skip': 'abc'` | `skip` is not a whole number. |
| `Invalid 'sort': 'email'` | `sort` was sent without a field, e.g. `?sort=email`. |
| `'up' is not a valid sort direction` | Sort direction is not `asc`, `desc`, `1`, or `-1`. |
| `Field 'interests' is not valid for sorting` | The field cannot be sorted, or does not exist. |
| `Field 'nickname' is not valid for filtering` | The field does not exist. Also returned for an unrecognised operator: `?filter[email][(foo)]=x` gives `Field '(foo)' is not valid for filtering`. |
| `'REGEXP' is not a valid operator for the field 'email'` | The operator is not supported on that field. The message names the SQL operator: `REGEXP` for `(regex)`, `IN` for `(in)`, and `>`, `<`, `>=`, `<=` for the range operators. |
| `Invalid date format in filter condition for date_created` | A date filter value could not be read as a date. |
| `Invalid 'interest' provided: 'Volunteers'` | No tag, subscription, or mandatory consent item has that exact name. |
| `Invalid 'autoresponder' provided: 'Welcome'` | No nurture campaign has that exact name. |
| `Field 'email' requires a scalar value` | An operator is given a list or group where a single value is expected, e.g. `filter[email][(ne)][]=alto.lead@example.com`. |
| `Field '0' is not valid for filtering` | A field is given a list without an operator, e.g. `filter[email][]=alto.lead@example.com`; the list position is read as the field name. |
### Response
A JSON array of contact objects. Returns an empty array (`[]`) if no contacts match.
```json
[
{
"id": "1001",
"external_id": "9f2c4e7a1b3d5f6a8c0e2b4d6f8a1c3e",
"date_created": "2026-10-05T14:03:11+00:00",
"remote_id": "",
"first_name": "Maya",
"last_name": "Okafor",
"title": "Section Lead",
"email": "alto.lead@example.com",
"company": "",
"phone": "416-555-0142",
"address_1": "48 Harbour Lane",
"address_2": "Unit 5",
"city": "Toronto",
"country": "CA",
"province": "ON",
"postal_code": "M5V 0Z9",
"website": "",
"language": "en",
"feedback": "",
"consent_status": "Express",
"consent_description": "Signed up at the spring concert",
"consent_effective_date": "2026-09-12T18:30:00+00:00",
"consent_expiry": "",
"custom_fields": { "Voice_Part": "Alto", "Season_Ticket_Number": "HCS-ST-0417" },
"interests": ["Volunteer", "Alumni", "Concert News"],
"autoresponders": []
}
]
```
`consent_expiry` is `""` when the consent does not expire or when expiry does not apply to the contact (see [Consent fields](https://envoke-developers.pages.dev/v1/contacts/fields/#consent-fields)).
---
## Get a Single Contact
```
GET https://e1.envoke.com/v1/contacts/{ID}
```
Returns a single contact object (the same shape as one item in the list response). Replace `{ID}` with the Envoke contact ID. `fields` can be used to select fields; filter, sort, and paging parameters are ignored.
If the ID is not found, the API returns an empty array (`[]`).
---
## Examples
### Return the first page of contacts
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts"
```
Returns up to 10 contacts. 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/v1/contacts?sort%5Bid%5D=asc&limit=100&skip=100"
```
### Sort by email
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?sort%5Bemail%5D=asc"
```
### Find a contact by email
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?filter%5Bemail%5D=alto.lead%40example.com"
```
### Find a contact by remote ID
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?filter%5Bremote_id%5D=HCS-0002"
```
### Exclude a city
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?filter%5Bcity%5D%5B%28ne%29%5D=Toronto"
```
### Filter by a custom field value
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?filter%5Bcustom_fields.Voice_Part%5D=Alto"
```
With `(regex)`, contacts whose season ticket number starts with `HCS-ST-`:
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?filter%5Bcustom_fields.Season_Ticket_Number%5D%5B%28regex%29%5D=%5EHCS-ST-"
```
### Contacts with a tag, and without it
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?filter%5Binterests%5D=Volunteer"
```
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?filter%5Binterests%5D%5B%28ne%29%5D=Volunteer"
```
### Contacts with two tags
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?filter%5B0%5D%5Binterests%5D=Volunteer&filter%5B1%5D%5Binterests%5D=Alumni"
```
### Either of two email addresses
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?filter%5B%28or%29%5D%5B0%5D%5Bemail%5D=alto.lead%40example.com&filter%5B%28or%29%5D%5B1%5D%5Bemail%5D=tenor.remote%40example.com"
```
### Everyone without express consent
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?filter%5B%28not%29%5D%5Bconsent_status%5D=Express"
```
### Contacts created in the first half of 2026
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?filter%5Bdate_created%5D%5B%28gte%29%5D=2026-01-01T00:00:00Z&filter%5Bdate_created%5D%5B%28lt%29%5D=2026-07-01T00:00:00Z"
```
### Consent given after a date
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?filter%5Bconsent_effective_date%5D%5B%28gt%29%5D=2026-09-01T00:00:00Z"
```
### Return only specific fields
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?fields=id,email,consent_status"
```
Response:
```json
[
{ "id": "1001", "email": "alto.lead@example.com", "consent_status": "Express" },
{ "id": "1002", "email": "tenor.remote@example.com", "consent_status": "Express" }
]
```
With `fields` set and no `limit`, this returns up to 10,000 contacts.
### Return as a key-value map
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?result_type=kvp"
```
Response:
```json
{ "1001": "alto.lead@example.com", "1002": "tenor.remote@example.com" }
```
### Look up one contact's ID by email
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?email=alto.lead%40example.com&fields=id&limit=1"
```
Response (an object, because `limit=1`):
```json
{ "id": "1001" }
```
### Get a single contact by ID
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts/1001"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v1/contacts/insert/
Markdown: https://envoke-developers.pages.dev/v1/contacts/insert.md
# Contact API - Insert
Use this endpoint to create a new contact in your account, or, with `upsert`, to create the contact or update it if it already exists.
---
## Endpoint
```
POST https://e1.envoke.com/v1/contacts
```
Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be a single JSON object.
### Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| `key_field` | string | `email` | The field used to check whether the contact already exists: `email` or `remote_id`. Any other value is a validation error. |
| `upsert` | boolean | `false` | When `1`, `true`, `yes`, or `on`, an existing contact matched by `key_field` is updated instead of rejected. An empty value (`?upsert=`) is false. |
---
## Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string | Yes | Email address. Required even when `key_field=remote_id`. |
| `remote_id` | string | With `key_field=remote_id` | Your own identifier for the contact. Max 60 characters. |
| `first_name` | string | No | Max 50 characters. |
| `last_name` | string | No | Max 50 characters. |
| `title` | string | No | Max 50 characters. |
| `company` | string | No | Max 80 characters. |
| `phone` | string | No | Max 25 characters. |
| `address_1` | string | No | Max 100 characters. |
| `address_2` | string | No | Max 100 characters. |
| `city` | string | No | Max 50 characters. |
| `country` | string | No | ISO code or country name. Unrecognised values produce a warning. |
| `province` | string | No | Max 50 characters. |
| `postal_code` | string | No | Max 15 characters. |
| `website` | string | No | Max 255 characters. |
| `language` | string | No | `en` or `fr` (or `English`, `French`). Other values produce a warning. |
| `feedback` | string | No | Free-text notes. HTML is stripped. |
| `consent_status` | string | No | A writable consent type. Defaults to `Not Provided`. |
| `consent_description` | string | No | How consent was obtained. Max 500 characters. |
| `consent_effective_date` | datetime | No | When consent was given; not in the future. Defaults to now. |
| `consent_force_downgrade` | boolean | No | Has no effect on a new contact. Applies when an upsert updates an existing contact (see [Update](https://envoke-developers.pages.dev/v1/contacts/update/)). |
| `custom_fields` | object | No | Values keyed by custom field name. |
| `interests` | array or object | No | Tags, subscriptions, and mandatory consent items to set. |
| `autoresponders` | array or object | No | Nurture campaigns to add and queue. |
`id`, `external_id`, `date_created`, and `consent_expiry` are read-only and silently ignored. See the [Fields reference](https://envoke-developers.pages.dev/v1/contacts/fields/) for accepted values and the full rules for each field.
Note: Custom field values that cannot be stored and nurture campaigns that are skipped for lack of consent are not reported; the contact is still created and the response reports success.
---
## Response
On success, the API returns the new contact in full, in the same shape as a [read](https://envoke-developers.pages.dev/v1/contacts/read/#response):
```json
{
"success": true,
"result_text": "The contact was successfully created.",
"result_data": {
"id": "1005",
"external_id": "4b8d2f6a0c1e3a5b7d9f1e3c5a7b9d0f",
"date_created": "2026-10-05T15:20:44+00:00",
"first_name": "Priya",
"last_name": "Nair",
"email": "upsert.target@example.com",
"consent_status": "Not Provided",
"consent_effective_date": "2026-10-05T15:20:44+00:00",
"consent_expiry": "",
"custom_fields": {},
"interests": [],
"autoresponders": []
}
}
```
*(`result_data` shortened; the response contains every field, with `""` for those not set.)*
`result_data.id` is the Envoke contact ID. Store it if you need to update or delete the contact later.
**Warnings.** An unrecognised `country` or `language` value does not fail the request: the field is left empty, and a warning is appended to `result_text` after a space. Several warnings are joined with `. `:
```
The contact was successfully created. 'Narnia' is not a valid value for country; field will not be updated. 'de' is not a valid value for language; field will not be updated
```
When `consent_status` is an implied type that does not apply to an EU contact, a consent sentence is appended in the same way (see [Consent fields](https://envoke-developers.pages.dev/v1/contacts/fields/#eu-contacts) and the example below).
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"Email is required"
]
}
}
```
Common validation errors:
| Error | Cause |
|---|---|
| `Email is required` | `email` was missing or empty. |
| `Email must be a string` | `email` was a number, list, object, or boolean. |
| `Invalid 'key_field' value: 'external_id'` | `key_field` is not `email` or `remote_id`. |
| `Specified key_field 'remote_id' field was not provided.` | `key_field=remote_id` was set but the body has no `remote_id`. |
| `email value not unique ('alto.lead@example.com' duplicates existing contact with id: '1001')` | A contact with that email already exists and `upsert` is not set. |
| `Specified key_field 'remote_id' field value not unique ('HCS-0002' duplicates existing contact with id: '1002')` | As above, with `key_field=remote_id`. |
| `Multiple contacts already exist with 'remote_id' value 'HCS-0002'. (IDs: 1002, 1010)` | More than one contact has the key value. Returned even with `upsert`. More than 10 matches are shown as a count, e.g. `(12 records)`. Note the two spaces before the bracket. |
| `'nickname' is not a valid contact field` | The body has a key that is not a contact field. |
| `Invalid 'consent_status': 'Opted In' (possible values: ...)` | Not a writable consent type. The message lists the valid values. |
| `consent_effective_date cannot be in the future.` | The effective date is later than now. |
| `Invalid date format for consent_effective_date` | The effective date could not be read. |
| `Invalid 'custom_field': 'Voice Range' (for a list of valid custom fields try the endpoint '/customfields')` | No custom field has that name. |
| `'custom_fields' must be an object` | `custom_fields` is not an object. |
| `Invalid 'interest': 'Volunteers' (for a list of valid interests try the endpoint '/interests')` | No tag, subscription, or mandatory consent item has that name. |
| `Invalid 'autoresponder': 'Welcome' (for a list of valid autoresponders try the endpoint '/autoresponders')` | No nurture campaign has that name. |
| `'interests' must be an object or array` | `interests` is a string or number. `autoresponders` gives `'autoresponders' must be an object or array`. |
The email, `key_field`, and duplicate checks run first and return on their own. The remaining body errors in the table are then collected and returned together. Field lengths, email format, and the account's duplicate check run afterwards and use the field-validation envelope, keyed by database column:
```json
{
"success": false,
"result_text": "Email invalid",
"result_data": {
"fields": {
"email": "Email invalid"
}
}
}
```
| `result_text` | Cause |
|---|---|
| `Email invalid` | `email` is not a valid address. |
| `First name must not exceed 50 characters` | A field is too long. Key `fname`; see the [column-name table](https://envoke-developers.pages.dev/v1/contacts/fields/#error-responses) for other keys. |
| `contact with email = alto.lead@example.com already exists` | With `key_field=remote_id`, the email belongs to another contact and your account checks emails for duplicates. `result_data.fields` is `[]`. |
A missing, empty, or `{}` body returns `{"success":false,"result_data":[],"result_text":"Invalid request"}`. Invalid JSON returns `Invalid JSON` in the same shape.
---
## Examples
### Create a contact with only an email address
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "to.delete@example.com"}' \
"https://e1.envoke.com/v1/contacts"
```
The contact is created with `consent_status` `Not Provided`.
### Create a full contact with express consent
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "alto.lead@example.com", "first_name": "Maya", "last_name": "Okafor", "title": "Section Lead", "phone": "416-555-0142", "address_1": "48 Harbour Lane", "address_2": "Unit 5", "city": "Toronto", "province": "ON", "postal_code": "M5V 0Z9", "country": "Canada", "language": "English", "consent_status": "Express", "consent_description": "Signed up at the spring concert", "consent_effective_date": "2026-09-12T18:30:00Z"}' \
"https://e1.envoke.com/v1/contacts"
```
`country` is returned as `"CA"` and `language` as `"en"`.
### Create with custom fields, tags, and a subscription
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "alto.lead@example.com", "consent_status": "Express", "custom_fields": {"Voice_Part": "Alto", "Season_Ticket_Number": "HCS-ST-0417"}, "interests": ["Volunteer", "Alumni", "Concert News"], "autoresponders": ["Welcome Sequence"]}' \
"https://e1.envoke.com/v1/contacts"
```
### Create keyed on your own ID
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "tenor.remote@example.com", "remote_id": "HCS-0002", "first_name": "Daniel", "last_name": "Reyes"}' \
"https://e1.envoke.com/v1/contacts?key_field=remote_id"
```
The existence check uses `remote_id`. If your account checks emails for duplicates, an email that belongs to another contact still fails with `contact with email = ... already exists`.
**Caution: the parameter is `key_field`, not `keyfield`.** An older version of the support article spelled it `keyfield`. That parameter is ignored without an error, so `?keyfield=remote_id` checks for an existing contact by `email`, and with `upsert` it updates whichever contact has that email.
### Duplicate email
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "alto.lead@example.com"}' \
"https://e1.envoke.com/v1/contacts"
```
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["email value not unique ('alto.lead@example.com' duplicates existing contact with id: '1001')"]
}
}
```
### Upsert: create when the contact does not exist
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "upsert.target@example.com", "first_name": "Priya", "last_name": "Nair"}' \
"https://e1.envoke.com/v1/contacts?upsert=1"
```
No contact has that email, so it is created: `result_text` is `The contact was successfully created.` and `result_data` is the new contact (see [Response](#response)).
### Upsert: update when the contact exists
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "upsert.target@example.com", "company": "Harbourfront Choral Society", "consent_status": "Express"}' \
"https://e1.envoke.com/v1/contacts?upsert=1"
```
The same request now matches the existing contact and updates it: `result_text` is `The contact was successfully updated.` and `result_data` is the full, updated contact. An upsert that updates behaves like an [update](https://envoke-developers.pages.dev/v1/contacts/update/), with these differences:
- Empty values are ignored and `write_empty_values` has no effect on POST. A custom field value of `""` still deletes that value.
- A missing `consent_status` is not defaulted to `Not Provided`; the contact keeps its current status.
- `id` in the body is ignored, so an upsert never matches by `id`. It matches by `email`, or by `remote_id` with `key_field=remote_id`.
- Warnings are appended after `. `, giving a double period: `The contact was successfully updated.. 'Narnia' is not a valid value for country; field will not be updated`.
### EU contact with implied consent
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "paris.member@example.org", "first_name": "Camille", "last_name": "Laurent", "country": "France", "consent_status": "Implied - Inquiry"}' \
"https://e1.envoke.com/v1/contacts"
```
Implied consent does not apply in the EU region, so the contact is created with `Not Provided` instead:
```json
{
"success": true,
"result_text": "The contact was successfully created. Their consent status was set to Not Provided because Implied - Inquiry does not apply to contacts in the EU.",
"result_data": {
"id": "1003",
"email": "paris.member@example.org",
"country": "FR",
"consent_status": "Not Provided",
"consent_description": "Implied - Inquiry consent does not apply to contacts in the EU.",
"consent_expiry": ""
}
}
```
*(`result_data` shortened; the response contains every field.)*
---
*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).*
---
URL: https://envoke-developers.pages.dev/v1/contacts/update/
Markdown: https://envoke-developers.pages.dev/v1/contacts/update.md
# Contact API - Update
Use these endpoints to update existing contacts. You can update one contact by its ID, one contact found by email or another key field, or several contacts in one request.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| PATCH | `https://e1.envoke.com/v1/contacts/{ID}` | Update one contact by ID. |
| PATCH | `https://e1.envoke.com/v1/contacts` | Update one contact found by key field (object body), or several contacts (list body). |
Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
### Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| `write_empty_values` | boolean | `false` | When `1`, `true`, `yes`, or `on`, an empty string (`""`) clears a field. Otherwise empty values are ignored (see [Clearing Values](#clearing-values)). |
| `key_field` | string | *(see below)* | `id`, `email`, or `remote_id`. Used only by `PATCH /v1/contacts` with an object body. |
### Which update is performed
| URL | Body | Update |
|---|---|---|
| `/v1/contacts/{ID}` | JSON object | [Update by ID](#update-by-id) |
| `/v1/contacts` | JSON list of objects | [Update several contacts](#update-several-contacts) |
| `/v1/contacts` | JSON object | [Update one contact found by key field](#update-one-contact-found-by-key-field) |
A list body sent to `/v1/contacts/{ID}` is treated as an update of several contacts, and the `{ID}` in the URL is ignored.
---
## Request Body
Send only the fields you want to change. Any writable field from the [Fields reference](https://envoke-developers.pages.dev/v1/contacts/fields/) can be included, with the same accepted values as on [create](https://envoke-developers.pages.dev/v1/contacts/insert/). Each field is compared with the stored value, and only fields that differ are written; a request that changes nothing still succeeds.
`external_id`, `date_created`, and `consent_expiry` are read-only and silently ignored. `id` is used to identify the contact, never written.
Note: Custom field values that cannot be stored, nurture campaigns skipped for lack of consent, and consent changes that are preempted or blocked are not reported; the response still reports success.
---
## Update by ID
```
PATCH https://e1.envoke.com/v1/contacts/{ID}
```
**Request.** A JSON object. Replace `{ID}` with the Envoke contact ID. `key_field` is ignored. If the body includes `id`, it must match `{ID}` (see [ID Mismatch](#id-mismatch)).
**Response.** A short result, not the full contact. `result_data.id` is an integer on this endpoint, and `result_data.warnings` lists any `country` or `language` warnings (`[]` when none):
```json
{
"success": true,
"result_text": "The contact was successfully updated.",
"result_data": {
"id": 1001,
"submit_type": "update",
"warnings": []
}
}
```
When no profile column changed (only custom fields, tags, nurture campaigns, or consent, or nothing at all), `result_data` also contains `"no_affected_rows": true`. When an EU country change resets the contact's consent, `result_data` also contains `inapplicable_consent_updated` and `consent_fix_message` (see [example](#change-country-to-an-eu-country)).
**Errors.**
- **Not found:** validation envelope with `No contact was found with ID: '999999'`.
- **Body errors** (unknown field, invalid value): validation envelope with `result_data.errors`.
- **Field validation** (length, email format, duplicate): the field-validation envelope, with an extra `warnings` key: `{"success":false,"result_data":{"fields":{"fname":"First name must not exceed 50 characters"},"warnings":[]},"result_text":"First name must not exceed 50 characters"}`.
- **ID mismatch:** `{"success":false,"result_data":[],"result_text":"Contact 'id': 1001 does not match 'id' in request body (1002)"}`.
---
## Update several contacts
```
PATCH https://e1.envoke.com/v1/contacts
```
**Request.** A JSON list of contact objects. Each object is matched on its own:
- If it has an `id`, that ID is used directly (it is not looked up first).
- Otherwise it is matched by `email`.
`key_field` is ignored for list bodies, so contacts cannot be matched by `remote_id` this way. Each object is then updated exactly as in [Update by ID](#update-by-id).
**Response.** When every contact is updated, HTTP `200`. The envelope has two extra top-level keys, `successes` and `failures`, listing contact IDs, and `result_data` holds each contact's result keyed by ID:
```json
{
"success": true,
"successes": ["1006", "1007"],
"failures": [],
"result_data": {
"1006": { "id": 1006, "submit_type": "update", "warnings": [] },
"1007": { "id": 1007, "submit_type": "update", "warnings": [] }
},
"result_text": "All contacts were successfully updated"
}
```
**Partial failure.** If any contact fails, the whole response is HTTP `400` with `success: false`, even though the other contacts were updated. The failed ones are listed in `failures`:
- A contact that could not be found is keyed `CONTACT_OBJECT_{n}`, where `{n}` is its zero-based position in the list, with the lookup error in `errors`.
- A contact that was found but failed validation is keyed by its ID, with the same `errors` or `fields` and `warnings` content as [Update by ID](#update-by-id).
```json
{
"success": false,
"successes": ["1006", "1007"],
"failures": ["CONTACT_OBJECT_2"],
"result_data": {
"1006": { "id": 1006, "submit_type": "update", "warnings": [] },
"1007": { "id": 1007, "submit_type": "update", "warnings": [] },
"CONTACT_OBJECT_2": {
"errors": ["No contact was found with the 'email': 'missing.member@example.com'"]
}
},
"result_text": "2 contacts were successfully updated; 1 contacts were not updated"
}
```
**Errors** (per contact, in `result_data`):
- `No contact was found with the 'email': '{email}'`
- `No contact was found with ID: '{id}'` (an `id` that does not exist)
- `Multiple contacts were found with the 'email': '{email}'. (IDs: 1006, 1012)` (two spaces before the bracket)
- `An 'id' or 'email' (or alternate field specified by key_field query parameter) must be provided in order to update a contact`
---
## Update one contact found by key field
```
PATCH https://e1.envoke.com/v1/contacts
```
**Request.** A JSON object. The contact is found by `key_field`:
| `key_field` | Lookup |
|---|---|
| *(not set)* | By `id` if the body has one (used directly, not looked up first); otherwise by `email`. |
| `id` | By `id`, looked up. |
| `email` | By `email`. |
| `remote_id` | By `remote_id`. |
Because the key value is used only to find the contact, a body with `id` and a new `email` (and no `key_field`) changes that contact's email address.
**Response.** The full, updated contact, in the same shape as a [read](https://envoke-developers.pages.dev/v1/contacts/read/). Warnings are appended to `result_text` after `. `, which follows the success message's own period:
```json
{
"success": true,
"result_text": "The contact was successfully updated.. 'Narnia' is not a valid value for country; field will not be updated",
"result_data": {
"id": "1002",
"email": "tenor.remote@example.com",
"remote_id": "HCS-0002",
"first_name": "Dan",
"country": ""
}
}
```
*(`result_data` shortened; the response contains every field.)*
**Errors.** Lookup failures use a plain envelope with `result_data: []`, not the validation envelope:
| `result_text` | Cause |
|---|---|
| `No contact was found with the 'email': 'nobody@example.com'` | No contact has that key value. With `key_field=id`, the message names `'id'`. |
| `Multiple contacts were found with the 'remote_id': 'HCS-0002'. (IDs: 1002, 1010)` | More than one contact has the key value. |
| `Specified key_field 'remote_id' field was not provided.` | `key_field` is set but the body does not include that field. |
| `An 'id' or 'email' (or alternate field specified by key_field query parameter) must be provided in order to update a contact` | No `key_field`, and the body has neither `id` nor `email`. |
| `Invalid 'key_field' value: 'external_id'` | `key_field` is not `id`, `email`, or `remote_id`. |
| `The specified contact ID 1002 does not match record identified by key_field 'email': 'alto.lead@example.com' (ID: 1001)` | See [ID Mismatch](#id-mismatch). |
Once the contact is found, update errors are the same as for [Update by ID](#update-by-id). When the body has an `id` and no `key_field`, a missing ID gives the validation envelope with `No contact was found with ID: '{id}'`.
---
## Clearing Values
| Value sent | Without `write_empty_values` | With `write_empty_values=1` |
|---|---|---|
| `null` | Ignored | Ignored |
| `""`, `0`, `"0"`, `false`, `[]` | Ignored | `""` clears a text field |
| Custom field value `""` | Deletes the custom field value | Deletes the custom field value |
- A custom field is cleared by sending `""` as its value inside `custom_fields`. This works with or without `write_empty_values`.
- `email` cannot be cleared: `Email must not be empty`.
- `country` and `language` cannot be cleared; an empty value leaves them unchanged.
- Tags, subscriptions, and nurture campaigns are removed with the object form (`{ "Alumni": "Unset" }`), not by sending an empty list.
---
## Consent Changes
Consent is written only when `consent_status`, `consent_description`, or `consent_effective_date` differs from the stored value. When one of them changes, all the consent fields in the body are applied together, and the contact's current status is used if `consent_status` was not sent. `consent_force_downgrade` on its own does nothing.
Whether a new status becomes current follows the transition rules in [Consent fields](https://envoke-developers.pages.dev/v1/contacts/fields/#consent-fields). A downgrade (for example `Express` to an implied type) is recorded in the contact's history but not made current unless `consent_force_downgrade` is `true`. A change away from a spam-reported status, or from `Revoked` to `Not Provided`, is blocked.
Note: A preempted or blocked consent change is not reported. The response is the normal success message, and the contact's `consent_status` is unchanged.
---
## ID Mismatch
- **`PATCH /v1/contacts/{ID}`:** if the body includes `id` and it differs from `{ID}`, the request fails with `{"success":false,"result_data":[],"result_text":"Contact 'id': 1001 does not match 'id' in request body (1002)"}`. `"1001"` and `1001` count as the same.
- **`PATCH /v1/contacts` with `key_field=email` or `remote_id`:** if the body includes `id` and it differs from the contact the key field found, the request fails with `{"success":false,"result_data":[],"result_text":"The specified contact ID 1002 does not match record identified by key_field 'email': 'alto.lead@example.com' (ID: 1001)"}`.
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"No contact was found with ID: '999999'"
]
}
}
```
Common errors:
| Error | Cause |
|---|---|
| `Invalid request` | No body, an empty body, `{}`, or `[]`. Plain envelope with `result_data: []`. |
| `Invalid JSON` | The body is not valid JSON. Plain envelope with `result_data: []`. |
| `No contact was found with ID: '{id}'` | The ID does not match any contact in your account. |
| `'nickname' is not a valid contact field` | The body has a key that is not a contact field. |
| `Email must not be empty` | `email` was sent as `""` with `write_empty_values=1`. Field-validation envelope. |
| `contact with email = alto.lead@example.com already exists` | The new email belongs to another contact. Field-validation envelope. |
The body validation errors listed for [create](https://envoke-developers.pages.dev/v1/contacts/insert/#validation-errors) (consent status, effective date, custom fields, interests, autoresponders) apply to updates too.
---
## Examples
### Update by ID
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Alto Section Lead", "phone": "416-555-0199"}' \
"https://e1.envoke.com/v1/contacts/1001"
```
### Update by email
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "alto.lead@example.com", "company": "Harbourfront Choral Society"}' \
"https://e1.envoke.com/v1/contacts"
```
Returns the full contact in `result_data`.
### Update by remote ID
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"remote_id": "HCS-0002", "first_name": "Dan"}' \
"https://e1.envoke.com/v1/contacts?key_field=remote_id"
```
### Clear a value
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"address_2": ""}' \
"https://e1.envoke.com/v1/contacts/1001?write_empty_values=1"
```
Without `write_empty_values=1` the same request succeeds but leaves `address_2` unchanged.
### Clear a custom field
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"custom_fields": {"Season_Ticket_Number": ""}}' \
"https://e1.envoke.com/v1/contacts/1001"
```
### Remove a tag
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"interests": {"Alumni": "Unset"}}' \
"https://e1.envoke.com/v1/contacts/1001"
```
Other tags and subscriptions are left as they are.
### Stop a nurture campaign
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"autoresponders": {"Welcome Sequence": "Dequeue"}}' \
"https://e1.envoke.com/v1/contacts/1001"
```
### Force a consent downgrade
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"consent_status": "Implied - Transaction", "consent_description": "Ticket purchase only; express consent withdrawn by phone", "consent_force_downgrade": true}' \
"https://e1.envoke.com/v1/contacts/1001"
```
The contact had `Express` consent. Without `consent_force_downgrade` the request returns the same success message but `consent_status` stays `Express`.
### Update several contacts, one of which is missing
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '[{"email": "bulk.a@example.com", "city": "Hamilton"}, {"email": "bulk.b@example.com", "city": "Hamilton"}, {"email": "missing.member@example.com", "city": "Hamilton"}]' \
"https://e1.envoke.com/v1/contacts"
```
The first two contacts are updated; the response is HTTP `400` (see [Partial failure](#update-several-contacts)).
### Change country to an EU country
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"country": "Germany"}' \
"https://e1.envoke.com/v1/contacts/1004"
```
The contact had `Implied - Inquiry` consent, which does not apply in the EU region, so it is set to `Not Provided`:
```json
{
"success": true,
"result_text": "The contact was successfully updated. Their consent status was updated because Implied - Inquiry consent does not apply to contacts in the EU.",
"result_data": {
"id": 1004,
"submit_type": "update",
"inapplicable_consent_updated": true,
"consent_fix_message": " Their consent status was updated because Implied - Inquiry consent does not apply to contacts in the EU.",
"warnings": []
}
}
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v1/contacts/delete/
Markdown: https://envoke-developers.pages.dev/v1/contacts/delete.md
# Contact API - Delete
Use this endpoint to delete a contact from your account.
---
## Endpoint
```
DELETE https://e1.envoke.com/v1/contacts/{ID}
```
Replace `{ID}` with the Envoke contact ID you want to delete. Authentication is via HTTP Basic Auth using your API ID and API Key. No request body is needed.
---
## Important
**Deletion is permanent.** Deleting a contact removes the contact record, its consent history, its custom field values, its tag, subscription, and mandatory consent memberships, its nurture campaign memberships and queued campaign actions, and its form submissions. There is no undo, and the contact's ID is never reused. To keep a contact but stop sending to it, set its `consent_status` to `Revoked` with an [update](https://envoke-developers.pages.dev/v1/contacts/update/) instead; note that a `Revoked` contact can no longer be deleted (see below).
**Some consent statuses block deletion.** A contact whose current `consent_status` is `Revoked`, `Envoke Spam Reported`, or `External Spam Reported` cannot be deleted.
**Delete responses have no `result_data`.** Unlike the other write endpoints, success and error responses contain only `success` and `result_text`.
---
## Response
### Success
```json
{
"success": true,
"result_text": "1 contact deleted"
}
```
### Contact not found
Returned with HTTP status `400` when the ID does not match any contact in your account:
```json
{
"success": false,
"result_text": "1 contact not deleted, 1 - no matching contact found"
}
```
### Blocked by consent status
Returned with HTTP status `400` when the contact's consent status blocks deletion:
```json
{
"success": false,
"result_text": "1 contact not deleted, 1 - can't delete due to consent status"
}
```
### Database error
If the delete fails on the server, `result_text` is `1 contact not deleted, 1 - delete error: ` followed by the error detail, with HTTP status `400`. Retry the request; if it keeps failing, contact Envoke support.
---
## Example
```bash
curl -X DELETE -gs \
-u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts/1009"
```
Replace `1009` with the ID of the contact you want to delete, and substitute your actual API ID and API Key.
A `GET https://e1.envoke.com/v1/contacts/1009` afterwards returns `[]`, and a second `DELETE` returns the not-found response above.
To find the ID of a contact from its email address first:
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v1/contacts?email=to.delete%40example.com&fields=id&limit=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).*
---
URL: https://envoke-developers.pages.dev/v2/tags/fields/
Markdown: https://envoke-developers.pages.dev/v2/tags/fields.md
# Tag API - Fields Reference
This article describes all fields available through the Tag API. Use this as a reference when reading, creating, or updating tags.
---
## Overview
The Tag API lets you manage tags (also called interests) in your Envoke account programmatically. Tags are used to segment contacts based on topics, preferences, or areas of interest. Contacts can be assigned to or removed from tags in bulk via the [contacts endpoints](https://envoke-developers.pages.dev/v2/tags/contacts/).
Tags can be organised into groups, given a plain-text description for internal reference, and configured with translated display names for public-facing contexts such as website subscription forms.
All endpoints require [HTTP Basic Auth](https://envoke-developers.pages.dev/guides/authentication/) using your API ID and API Key.
---
## Fields
| Field | Type | Read-only | Required on Create | Description |
|---|---|---|---|---|
| `id` | string | Yes | — | Unique identifier assigned on creation. |
| `external_id` | string | Yes | — | Externally visible identifier used in public-facing integrations. |
| `name` | string | No | Yes | The tag name. |
| `group` | string | No | No | Group the tag belongs to. Empty string (`""`) when unassigned. |
| `description` | string | No | No | Internal description of the tag. |
| `display_names` | object | No | No | Translated display labels keyed by language ISO code. |
---
## Field Details
### `id`
- **Type:** String
- **Read-only:** Yes — assigned automatically on creation.
- **Example:** `"42"`
Used to identify the tag in update, delete, and contact assignment requests.
---
### `external_id`
- **Type:** String
- **Read-only:** Yes — assigned automatically on creation.
- **Example:** `"a1b2c3d4e5f6"`
An externally visible identifier used in public-facing contexts. Not the same as `id`.
---
### `name`
- **Type:** String
- **Read-only:** No — updatable via PATCH.
- **Required on create:** Yes
- **Example:** `"Newsletter Subscribers"`
The tag name as it appears within your Envoke account.
---
### `group`
- **Type:** String
- **Read-only:** No — updatable via PATCH.
- **Required on create:** No
- **Accepted values (write):** Group name (string) or group ID (integer). The group must be of type `interest`.
- **Returned value (read):** The group name as a string. Returns `""` (empty string) when no group is assigned.
- **Example:** `"Member Programs"`, `""`
Pass `""` (empty string) or `null` to remove an existing group assignment via PATCH.
---
### `description`
- **Type:** String
- **Read-only:** No — updatable via PATCH.
- **Required on create:** No
- **Example:** `"Members interested in our annual trade conference"`
An internal description visible only to account administrators. Not shown to contacts.
---
### `display_names`
- **Type:** Object
- **Read-only:** No — updatable via PATCH.
- **Required on create:** No
- **Format:** `{ "": "" }`
- **Example:** `{ "en": "Organic Certification", "fr": "Certification biologique" }`
Translated display labels for the tag, keyed by ISO language code. Used where tags are displayed publicly (such as on website subscription forms). A value of `""` means no translation has been set for that language. All enabled account languages are included as keys even if no translation exists.
---
## Error Responses
**Validation errors** (missing required fields, invalid values):
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'name' is required"
]
}
}
```
**Not-found behaviour varies by method:**
- **GET** `/tags/{id}` — when the ID is not found, returns an empty array (`[]`).
- **PATCH** `/tags/{id}` — when the ID is not found, returns a validation error:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '999999' not found"]
}
}
```
- **DELETE** `/tags/{id}` — when the ID is not found, returns a validation error (same shape as PATCH above).
---
*Full reference, OpenAPI description, and Postman collection: [developers.envoke.com](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/v2/tags/read/
Markdown: https://envoke-developers.pages.dev/v2/tags/read.md
# Tag API - Read
Use the read endpoints to retrieve tags for your account. You can list all tags with filtering and sorting, or retrieve a single tag by ID.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| GET | `https://e1.envoke.com/v2/tags` | Return a list of tags. |
| GET | `https://e1.envoke.com/v2/tags/{ID}` | Return a single tag. |
Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## List Tags
```
GET https://e1.envoke.com/v2/tags
```
Returns a JSON array of tags for your account, sorted alphabetically by name by default. Supports filtering, sorting, pagination, and field selection.
### 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). |
| `skip` | integer | `0` | Number of records to skip. Use with `limit` to paginate. |
| `fields` | string | *(all)* | Comma-separated list of fields to include in each result. See limit note below. |
| `result_type` | string | *(array)* | Set to `kvp` to receive results as a key-value object (`{ "id": "name" }`). See limit note below. |
| `filter[id]` | string | — | Filter by tag ID. |
| `filter[name]` | string | — | Filter by tag name. |
| `filter[external_id]` | string | — | Filter by external ID. |
| `filter[group]` | string | — | Filter by group name. |
| `filter[description]` | string | — | Filter by description. |
| `sort[name]` | string | — | Sort by name. Value: `asc` or `desc`. |
**Note — limit behaviour with `?fields=` and `?result_type=kvp`:** When either of these parameters is present, the default limit increases to 10,000 (returning all records if no explicit limit is specified) and a `?limit=` value above 100 is honoured as-is. This allows full exports and dropdown population without pagination.
### Filter Operators
All `filter[field]` parameters support exact match and the not-equal operator:
| Operator | Example | Description |
|---|---|---|
| *(none)* | `?filter[name]=Newsletter` | Exact match |
| `(ne)` | `?filter[group][(ne)]=Archived` | Not equal |
*Note: URL-encode brackets in production requests (e.g. `%5B` for `[`).*
### Response
A JSON array of tag objects. Returns an empty array (`[]`) if no tags exist or no results match the filter.
```json
[
{
"id": "42",
"external_id": "a1b2c3d4e5f6",
"name": "Newsletter Subscribers",
"group": "Engagement",
"description": "Contacts who opted in to the monthly newsletter",
"display_names": { "en": "Newsletter", "fr": "Infolettre" }
}
]
```
`display_names` is always present in list responses.
---
## Get a Single Tag
```
GET https://e1.envoke.com/v2/tags/{ID}
```
Returns a single tag object. Replace `{ID}` with the Envoke tag ID.
If the ID is not found, the API returns an empty array (`[]`).
### Response
```json
{
"id": "42",
"external_id": "a1b2c3d4e5f6",
"name": "Newsletter Subscribers",
"group": "Engagement",
"description": "Contacts who opted in to the monthly newsletter",
"display_names": { "en": "Newsletter", "fr": "Infolettre" }
}
```
---
## Examples
### Return all tags (default pagination)
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/tags"
```
### Paginate through results
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/tags?limit=10&skip=10"
```
### Filter by group
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/tags?filter%5Bgroup%5D=Member+Programs"
```
### Filter by name
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/tags?filter%5Bname%5D=Newsletter+Subscribers"
```
### Filter excluding a group
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/tags?filter%5Bgroup%5D%5B%28ne%29%5D=Archived"
```
### Sort by name descending
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/tags?sort%5Bname%5D=desc"
```
### Return only specific fields
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/tags?fields=id,name,group"
```
### Return as a key-value map (for dropdowns)
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/tags?result_type=kvp"
```
Response:
```json
{ "42": "Newsletter Subscribers", "43": "Event Attendees" }
```
### Get a single tag by ID
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/tags/42"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/tags/insert/
Markdown: https://envoke-developers.pages.dev/v2/tags/insert.md
# Tag API - Insert
Use this endpoint to create a new tag in your account.
---
## Endpoint
```
POST https://e1.envoke.com/v2/tags
```
Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
---
## Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | The tag name. |
| `group` | string or integer | No | Group name or group ID to assign the tag to. The group must be of type `interest`. |
| `description` | string | No | Internal description of the tag. |
| `display_names` | object | No | Translated display labels keyed by ISO language code (e.g. `{ "en": "...", "fr": "..." }`). |
---
## Response
On success, the API returns the newly created tag object:
```json
{
"success": true,
"result_text": "Created",
"result_data": {
"id": "42",
"external_id": "a1b2c3d4e5f6",
"name": "Newsletter Subscribers",
"group": "Engagement",
"description": "Contacts who opted in to the monthly newsletter",
"display_names": { "en": "Newsletter" }
}
}
```
`result_data.id` is the Envoke tag ID. Store this if you need to update, delete, or assign contacts to the tag later.
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'name' is required"
]
}
}
```
Common validation errors:
| Error | Cause |
|---|---|
| `Request body may not be empty` | No JSON body was provided. |
| `The field 'name' is required` | `name` was missing or blank. |
| `Invalid 'group': '{value}'` | The group name or ID was not found, or is not of type `interest`. |
---
## Examples
### Create a basic tag
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Newsletter Subscribers"}' \
"https://e1.envoke.com/v2/tags"
```
### Create with a group and description
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Annual Conference Attendees", "group": "Events", "description": "Members who attend our annual conference"}' \
"https://e1.envoke.com/v2/tags"
```
### Create with translated display names
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Newsletter Subscribers", "display_names": {"en": "Newsletter", "fr": "Infolettre"}}' \
"https://e1.envoke.com/v2/tags"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/tags/update/
Markdown: https://envoke-developers.pages.dev/v2/tags/update.md
# Tag API - Update
Use this endpoint to update an existing tag. You can update the name, group assignment, description, and display names.
---
## Endpoint
```
PATCH https://e1.envoke.com/v2/tags/{ID}
```
Replace `{ID}` with the Envoke tag ID you want to update. Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
---
## Request Body
At least one field must be provided.
| Field | Type | Description |
|---|---|---|
| `name` | string | New tag name. |
| `group` | string or integer | Group name or group ID to assign. Pass `""` (empty string) or `null` to remove the group assignment. |
| `description` | string | Updated internal description. |
| `display_names` | object | Translated display labels keyed by ISO language code. |
`external_id` is silently ignored if included in the request body. If `id` is included, it must match the `{ID}` in the URL — if they differ, a validation error is returned: `"Provided 'id': {url_id} does not match 'id' in request body ({body_id})"`.
---
## Response
On success, the API returns the updated tag object:
```json
{
"success": true,
"result_text": "Updated",
"result_data": {
"id": "42",
"external_id": "a1b2c3d4e5f6",
"name": "Updated Tag Name",
"group": "Events",
"description": "Updated description",
"display_names": { "en": "Updated Tag" }
}
}
```
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"Item with id '999999' not found"
]
}
}
```
Common errors:
| Error | Cause |
|---|---|
| `Request body may not be empty` | No JSON body was provided. |
| `Item with id '{id}' not found` | The `{ID}` in the URL does not match any tag in your account. |
| `Invalid 'group': '{value}'` | The group was not found or is not of type `interest`. |
| `Provided 'id': {url_id} does not match 'id' in request body ({body_id})` | `id` in the request body differs from `{ID}` in the URL. |
---
## Examples
### Rename a tag
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Organic Certification Members"}' \
"https://e1.envoke.com/v2/tags/42"
```
### Assign to a group
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"group": "Certification Programs"}' \
"https://e1.envoke.com/v2/tags/42"
```
### Remove group assignment
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"group": ""}' \
"https://e1.envoke.com/v2/tags/42"
```
### Update display names
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"display_names": {"en": "Organic", "fr": "Biologique"}}' \
"https://e1.envoke.com/v2/tags/42"
```
### Update multiple fields at once
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Organic Members", "group": "Certification Programs", "description": "Members pursuing organic certification"}' \
"https://e1.envoke.com/v2/tags/42"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/tags/delete/
Markdown: https://envoke-developers.pages.dev/v2/tags/delete.md
# Tag API - Delete
Use this endpoint to delete a tag from your account.
---
## Endpoint
```
DELETE https://e1.envoke.com/v2/tags/{ID}
```
Replace `{ID}` with the Envoke tag ID you want to delete. Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## Important
**Deletion is permanent.** Deleting a tag removes the tag record and all contact-to-tag associations. Contacts assigned to the deleted tag are not deleted — they remain in your account and simply lose their assignment to this tag. The deletion cannot be undone via the API.
---
## Response
On success:
```json
{
"success": true,
"result_text": "Deleted",
"result_data": []
}
```
If the tag ID is not found:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '999999' not found"]
}
}
```
---
## Example
```bash
curl -X DELETE -gs \
-u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/tags/42"
```
Replace `42` with the ID of the tag you want to delete, and substitute your actual API ID and API Key.
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/tags/contacts/
Markdown: https://envoke-developers.pages.dev/v2/tags/contacts.md
# Tag API - Add and Remove Contacts
Use these endpoints to assign contacts to a tag or remove them in bulk. Both operations are idempotent — adding a contact that is already tagged, or removing one that is not tagged, produces no error.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| POST | `https://e1.envoke.com/v2/tags/{ID}/contacts` | Add contacts to a tag. |
| DELETE | `https://e1.envoke.com/v2/tags/{ID}/contacts` | Remove contacts from a tag. |
Replace `{ID}` with the Envoke tag ID. Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
---
## Request Body
Provide exactly one of `contact_ids` or `contact_emails`. You cannot include both in the same request.
| Field | Type | Description |
|---|---|---|
| `contact_ids` | array of integers | Envoke contact IDs to add or remove. Maximum 1,000 items per request. |
| `contact_emails` | array of strings | Contact email addresses to add or remove. Maximum 1,000 items per request. |
---
## Response
### Success (all contacts found)
```json
{
"success": true,
"result_text": "Set to contacts",
"result_data": {
"processed": 3
}
}
```
### Success (some contacts not found)
Contacts that could not be matched are listed in `not_found`. The operation is still considered successful as long as no found contacts failed to update.
```json
{
"success": true,
"result_text": "Set to contacts. Some contacts not found",
"result_data": {
"processed": 3,
"not_found": ["ghost@example.com"]
}
}
```
### Failure
If one or more found contacts failed during the update, `success` is `false` and the failures are listed in `failed`.
```json
{
"success": false,
"result_text": "Error setting to contacts",
"result_data": {
"processed": 3,
"failed": [
{ "contact_id": "456", "reason": "error message" }
]
}
}
```
### `result_data` fields
| Field | Type | Description |
|---|---|---|
| `processed` | integer | Total number of input identifiers submitted (found + not found). |
| `not_found` | array | IDs or emails that were not matched to any contact in the account. Only present when at least one was not found. |
| `failed` | array | Contacts that were found but failed during the update. Only present when failures occurred. |
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Either 'contact_ids' or 'contact_emails' must be provided"]
}
}
```
If the request body is completely empty (`{}`), a validation envelope is returned:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Request body may not be empty"]
}
}
```
Common validation errors (returned when a non-empty body is provided but is missing or invalid):
| Error | Cause |
|---|---|
| `Either 'contact_ids' or 'contact_emails' must be provided` | Body was provided but contained neither field. |
| `Provide either 'contact_ids' or 'contact_emails', not both` | Both fields were included. |
| `The field 'contact_ids' must be an array` | `contact_ids` was not an array. |
| `The field 'contact_ids' must not be empty` | `contact_ids` was an empty array. |
| `The field 'contact_ids' may not contain more than 1000 items` | More than 1,000 IDs were submitted. |
| `The field 'contact_emails' must be an array` | `contact_emails` was not an array. |
| `The field 'contact_emails' must not be empty` | `contact_emails` was an empty array. |
| `The field 'contact_emails' may not contain more than 1000 items` | More than 1,000 emails were submitted. |
| `Item with id '{id}' not found` | The tag ID in the URL does not match any tag in your account. |
---
## Examples
### Add contacts by ID
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_ids": [101, 102, 103]}' \
"https://e1.envoke.com/v2/tags/42/contacts"
```
### Add contacts by email
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_emails": ["alice@example.com", "bob@example.com"]}' \
"https://e1.envoke.com/v2/tags/42/contacts"
```
### Remove contacts by ID
```bash
curl -X DELETE -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_ids": [101, 102]}' \
"https://e1.envoke.com/v2/tags/42/contacts"
```
### Remove contacts by email
```bash
curl -X DELETE -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_emails": ["alice@example.com"]}' \
"https://e1.envoke.com/v2/tags/42/contacts"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/subscriptions/fields/
Markdown: https://envoke-developers.pages.dev/v2/subscriptions/fields.md
# Subscription API - Fields Reference
This article describes all fields available through the Subscription API. Use this as a reference when reading, creating, or updating subscriptions.
---
## Overview
The Subscription API lets you manage email subscriptions in your Envoke account programmatically. Subscriptions represent communication types that contacts can opt in to or out of — for example, a weekly newsletter, event announcements, or alumni updates. They appear on your public preference page where contacts manage their own communication preferences.
Subscriptions can be organised into groups, given translated display names and descriptions for public-facing contexts, and optionally hidden from the public preference page using `prefpage_access`.
All endpoints require [HTTP Basic Auth](https://envoke-developers.pages.dev/guides/authentication/) using your API ID and API Key.
---
## Fields
| Field | Type | Read-only | Required on Create | Description |
|---|---|---|---|---|
| `id` | string | Yes | — | Unique identifier assigned on creation. |
| `external_id` | string | Yes | — | Externally visible identifier used in public-facing integrations. |
| `name` | string | No | Yes | The subscription name. |
| `group` | string | No | No | Group the subscription belongs to. Empty string (`""`) when unassigned. |
| `prefpage_access` | string | No | No | Controls preference page visibility. `"public"` (default) or `"private"`. |
| `display_names` | object | No | No | Translated display labels keyed by ISO language code. |
| `display_descriptions` | object | No | No | Translated descriptions keyed by ISO language code. |
---
## Field Details
### `id`
- **Type:** String
- **Read-only:** Yes — assigned automatically on creation.
- **Example:** `"12"`
Used to identify the subscription in update, delete, and contact assignment requests.
---
### `external_id`
- **Type:** String
- **Read-only:** Yes — assigned automatically on creation.
- **Example:** `"a1b2c3d4e5f6"`
An externally visible identifier used in public-facing contexts such as preference page URLs. Not the same as `id`.
---
### `name`
- **Type:** String
- **Read-only:** No — updatable via PATCH.
- **Required on create:** Yes
- **Example:** `"Campus Weekly Newsletter"`
The subscription name as it appears within your Envoke account. This is the internal label used by account administrators.
---
### `group`
- **Type:** String
- **Read-only:** No — updatable via PATCH.
- **Required on create:** No
- **Accepted values (write):** Group name (string) or group ID (integer). The group must be of type `subscription`.
- **Returned value (read):** The group name as a string. Returns `""` (empty string) when no group is assigned.
- **Example:** `"Student Communications"`, `""`
Pass `""` (empty string) or `null` to remove an existing group assignment via PATCH.
---
### `prefpage_access`
- **Type:** String
- **Read-only:** No — updatable via PATCH.
- **Required on create:** No
- **Accepted values:** `"public"` or `"private"`
- **Default:** `"public"`
- **Example:** `"public"`
Controls whether this subscription appears on the public preference page.
- `"public"` — The subscription is visible to contacts on the preference page. This is the default.
- `"private"` — The subscription is hidden from the public preference page. It can still be assigned to contacts via the API or within your account. **Requires the private subscriptions feature.** Attempting to set this value without the feature enabled returns a validation error.
---
### `display_names`
- **Type:** Object
- **Read-only:** No — updatable via PATCH.
- **Required on create:** No
- **Format:** `{ "": "" }`
- **Example:** `{ "en": "Campus Weekly Newsletter", "fr": "Bulletin hebdomadaire du campus" }`
Translated display labels for the subscription, keyed by ISO language code. These are shown to contacts on the public preference page as the subscription's visible name. A value of `""` means no translation has been set for that language. All enabled account languages are included as keys in responses even if no translation exists.
---
### `display_descriptions`
- **Type:** Object
- **Read-only:** No — updatable via PATCH.
- **Required on create:** No
- **Format:** `{ "": "" }`
- **Example:** `{ "en": "A weekly roundup of campus news, events, and announcements.", "fr": "Un résumé hebdomadaire des nouvelles et événements du campus." }`
Translated descriptions for the subscription, keyed by ISO language code. These are shown alongside the display name on the public preference page to help contacts understand what they are subscribing to. A value of `""` means no description has been set for that language. All enabled account languages are included as keys in responses even if no description exists.
---
## Error Responses
**Validation errors** (missing required fields, invalid values):
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'name' is required"
]
}
}
```
**Not-found behaviour varies by method:**
- **GET** `/subscriptions/{id}` — when the ID is not found, returns an empty array (`[]`).
- **PATCH** `/subscriptions/{id}` — when the ID is not found, returns a validation error:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '999999' not found"]
}
}
```
- **DELETE** `/subscriptions/{id}` — when the ID is not found, returns a validation error:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '999999' not found"]
}
}
```
---
*Full reference, OpenAPI description, and Postman collection: [developers.envoke.com](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/v2/subscriptions/read/
Markdown: https://envoke-developers.pages.dev/v2/subscriptions/read.md
# Subscription API - Read
Use the read endpoints to retrieve subscriptions for your account. You can list all subscriptions with filtering and sorting, or retrieve a single subscription by ID.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| GET | `https://e1.envoke.com/v2/subscriptions` | Return a list of subscriptions. |
| GET | `https://e1.envoke.com/v2/subscriptions/{ID}` | Return a single subscription. |
Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## List Subscriptions
```
GET https://e1.envoke.com/v2/subscriptions
```
Returns a JSON array of subscriptions for your account, sorted alphabetically by name by default. Supports filtering, sorting, pagination, and field selection.
### 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). |
| `skip` | integer | `0` | Number of records to skip. Use with `limit` to paginate. |
| `fields` | string | *(all)* | Comma-separated list of fields to include in each result. See limit note below. |
| `result_type` | string | *(array)* | Set to `kvp` to receive results as a key-value object (`{ "id": "name" }`). See limit note below. |
| `filter[id]` | string | — | Filter by subscription ID. |
| `filter[name]` | string | — | Filter by subscription name. |
| `filter[external_id]` | string | — | Filter by external ID. |
| `filter[group]` | string | — | Filter by group name. |
| `filter[prefpage_access]` | string | — | Filter by preference page access (`public` or `private`). |
| `sort[name]` | string | — | Sort by name. Value: `asc` or `desc`. |
**Note — limit behaviour with `?fields=` and `?result_type=kvp`:** When either of these parameters is present, the default limit increases to 10,000 (returning all records if no explicit limit is specified) and a `?limit=` value above 100 is honoured as-is. This allows full exports and dropdown population without pagination.
### Filter Operators
All `filter[field]` parameters support exact match and the not-equal operator:
| Operator | Example | Description |
|---|---|---|
| *(none)* | `?filter[name]=Campus Weekly` | Exact match |
| `(ne)` | `?filter[group][(ne)]=Archived` | Not equal |
*Note: URL-encode brackets in production requests (e.g. `%5B` for `[`).*
### Response
A JSON array of subscription objects. Returns an empty array (`[]`) if no subscriptions exist or no results match the filter.
```json
[
{
"id": "12",
"external_id": "a1b2c3d4e5f6",
"name": "Campus Weekly Newsletter",
"group": "Student Communications",
"prefpage_access": "public",
"display_names": { "en": "Campus Weekly Newsletter", "fr": "Bulletin hebdomadaire du campus" },
"display_descriptions": { "en": "A weekly roundup of campus news and events.", "fr": "" }
}
]
```
`display_names` and `display_descriptions` are always present in list responses. All enabled account languages appear as keys, with empty strings where no translation has been set.
---
## Get a Single Subscription
```
GET https://e1.envoke.com/v2/subscriptions/{ID}
```
Returns a single subscription object. Replace `{ID}` with the Envoke subscription ID.
If the ID is not found, the API returns an empty array (`[]`).
### Response
```json
{
"id": "12",
"external_id": "a1b2c3d4e5f6",
"name": "Campus Weekly Newsletter",
"group": "Student Communications",
"prefpage_access": "public",
"display_names": { "en": "Campus Weekly Newsletter", "fr": "Bulletin hebdomadaire du campus" },
"display_descriptions": { "en": "A weekly roundup of campus news and events.", "fr": "" }
}
```
---
## Examples
### Return all subscriptions (default pagination)
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/subscriptions"
```
### Paginate through results
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/subscriptions?limit=10&skip=10"
```
### Filter by group
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/subscriptions?filter%5Bgroup%5D=Student+Communications"
```
### Filter by name
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/subscriptions?filter%5Bname%5D=Campus+Weekly+Newsletter"
```
### Filter by prefpage_access
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/subscriptions?filter%5Bprefpage_access%5D=public"
```
### Filter excluding a group
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/subscriptions?filter%5Bgroup%5D%5B%28ne%29%5D=Alumni+Relations"
```
### Sort by name descending
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/subscriptions?sort%5Bname%5D=desc"
```
### Return only specific fields
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/subscriptions?fields=id,name,group"
```
### Return as a key-value map (for dropdowns)
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/subscriptions?result_type=kvp"
```
Response:
```json
{ "12": "Campus Weekly Newsletter", "13": "Alumni Quarterly" }
```
### Get a single subscription by ID
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/subscriptions/12"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/subscriptions/insert/
Markdown: https://envoke-developers.pages.dev/v2/subscriptions/insert.md
# Subscription API - Insert
Use this endpoint to create a new subscription in your account.
---
## Endpoint
```
POST https://e1.envoke.com/v2/subscriptions
```
Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
---
## Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | The subscription name. |
| `group` | string or integer | No | Group name or group ID to assign the subscription to. The group must be of type `subscription`. |
| `prefpage_access` | string | No | Preference page visibility: `"public"` (default) or `"private"`. Setting to `"private"` requires the private subscriptions feature. |
| `display_names` | object | No | Translated display labels keyed by ISO language code (e.g. `{ "en": "...", "fr": "..." }`). |
| `display_descriptions` | object | No | Translated descriptions keyed by ISO language code (e.g. `{ "en": "...", "fr": "..." }`). |
---
## Response
On success, the API returns the newly created subscription object:
```json
{
"success": true,
"result_text": "Created",
"result_data": {
"id": "12",
"external_id": "a1b2c3d4e5f6",
"name": "Campus Weekly Newsletter",
"group": "Student Communications",
"prefpage_access": "public",
"display_names": { "en": "Campus Weekly Newsletter", "fr": "" },
"display_descriptions": { "en": "A weekly roundup of campus news and events.", "fr": "" }
}
}
```
`result_data.id` is the Envoke subscription ID. Store this if you need to update, delete, or assign contacts to the subscription later.
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'name' is required"
]
}
}
```
Common validation errors:
| Error | Cause |
|---|---|
| `Request body may not be empty` | No JSON body was provided. |
| `The field 'name' is required` | `name` was missing or blank. |
| `Invalid 'group': '{value}'` | The group name or ID was not found, or is not of type `subscription`. |
| `'prefpage_access' must be 'public' or 'private'` | An unrecognised value was provided for `prefpage_access`. |
| `Setting 'prefpage_access' to 'private' requires the private subscriptions feature` | The account does not have the private subscriptions feature enabled. |
---
## Examples
### Create a basic subscription
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Campus Weekly Newsletter"}' \
"https://e1.envoke.com/v2/subscriptions"
```
### Create with a group
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Alumni Quarterly", "group": "Alumni Relations"}' \
"https://e1.envoke.com/v2/subscriptions"
```
### Create with translated display names and descriptions
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Campus Weekly Newsletter",
"group": "Student Communications",
"display_names": {"en": "Campus Weekly Newsletter", "fr": "Bulletin hebdomadaire du campus"},
"display_descriptions": {"en": "A weekly roundup of campus news and events.", "fr": "Un résumé hebdomadaire des nouvelles et événements du campus."}
}' \
"https://e1.envoke.com/v2/subscriptions"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/subscriptions/update/
Markdown: https://envoke-developers.pages.dev/v2/subscriptions/update.md
# Subscription API - Update
Use this endpoint to update an existing subscription. You can update the name, group assignment, preference page access, display names, and display descriptions.
---
## Endpoint
```
PATCH https://e1.envoke.com/v2/subscriptions/{ID}
```
Replace `{ID}` with the Envoke subscription ID you want to update. Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
---
## Request Body
At least one field must be provided.
| Field | Type | Description |
|---|---|---|
| `name` | string | New subscription name. |
| `group` | string or integer | Group name or group ID to assign. Pass `""` (empty string) or `null` to remove the group assignment. |
| `prefpage_access` | string | Preference page visibility: `"public"` or `"private"`. Setting to `"private"` requires the private subscriptions feature. |
| `display_names` | object | Translated display labels keyed by ISO language code. |
| `display_descriptions` | object | Translated descriptions keyed by ISO language code. |
`external_id` is silently ignored if included in the request body. If `id` is included, it must match the `{ID}` in the URL — if they differ, a validation error is returned: `"Provided 'id': {url_id} does not match 'id' in request body ({body_id})"`.
---
## Response
On success, the API returns the updated subscription object:
```json
{
"success": true,
"result_text": "Updated",
"result_data": {
"id": "12",
"external_id": "a1b2c3d4e5f6",
"name": "Campus Weekly (Updated)",
"group": "Student Communications",
"prefpage_access": "public",
"display_names": { "en": "Campus Weekly", "fr": "Campus hebdo" },
"display_descriptions": { "en": "Updated description.", "fr": "" }
}
}
```
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"Item with id '999999' not found"
]
}
}
```
Common errors:
| Error | Cause |
|---|---|
| `Request body may not be empty` | No JSON body was provided. |
| `Item with id '{id}' not found` | The `{ID}` in the URL does not match any subscription in your account. |
| `Invalid 'group': '{value}'` | The group was not found or is not of type `subscription`. |
| `'prefpage_access' must be 'public' or 'private'` | An unrecognised value was provided for `prefpage_access`. |
| `Setting 'prefpage_access' to 'private' requires the private subscriptions feature` | The account does not have the private subscriptions feature enabled. |
| `Provided 'id': {url_id} does not match 'id' in request body ({body_id})` | `id` in the request body differs from `{ID}` in the URL. |
---
## Examples
### Rename a subscription
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Campus Weekly (Updated)"}' \
"https://e1.envoke.com/v2/subscriptions/12"
```
### Assign to a group
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"group": "Student Communications"}' \
"https://e1.envoke.com/v2/subscriptions/12"
```
### Remove group assignment
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"group": ""}' \
"https://e1.envoke.com/v2/subscriptions/12"
```
### Update display names
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"display_names": {"en": "Campus Weekly", "fr": "Campus hebdo"}}' \
"https://e1.envoke.com/v2/subscriptions/12"
```
### Update display descriptions
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"display_descriptions": {"en": "Updated description for the weekly newsletter."}}' \
"https://e1.envoke.com/v2/subscriptions/12"
```
### Update multiple fields at once
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Campus Weekly (2025)", "group": "Student Communications", "display_names": {"en": "Campus Weekly 2025"}}' \
"https://e1.envoke.com/v2/subscriptions/12"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/subscriptions/delete/
Markdown: https://envoke-developers.pages.dev/v2/subscriptions/delete.md
# Subscription API - Delete
Use this endpoint to delete a subscription from your account.
---
## Endpoint
```
DELETE https://e1.envoke.com/v2/subscriptions/{ID}
```
Replace `{ID}` with the Envoke subscription ID you want to delete. Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## Important
**A subscription cannot be deleted while contacts are assigned to it.** If any contacts are currently subscribed, the API returns a validation error. You must remove all assigned contacts first using the [`DELETE /subscriptions/{ID}/contacts`](https://envoke-developers.pages.dev/v2/subscriptions/contacts/) endpoint, then retry the delete.
**Deletion is permanent.** Once all contacts have been removed and the subscription is deleted, it cannot be recovered via the API.
---
## Response
### Success
```json
{
"success": true,
"result_text": "Deleted",
"result_data": []
}
```
### Subscription has contacts assigned
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Cannot delete a subscription that has contacts assigned to it"]
}
}
```
### Subscription not found
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '999999' not found"]
}
}
```
---
## Example
```bash
curl -X DELETE -gs \
-u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/subscriptions/12"
```
Replace `12` with the ID of the subscription you want to delete, and substitute your actual API ID and API Key.
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/subscriptions/contacts/
Markdown: https://envoke-developers.pages.dev/v2/subscriptions/contacts.md
# Subscription API - Add and Remove Contacts
Use these endpoints to subscribe contacts to a subscription or unsubscribe them in bulk. Both operations are idempotent — adding a contact that is already subscribed, or removing one that is not subscribed, produces no error.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| POST | `https://e1.envoke.com/v2/subscriptions/{ID}/contacts` | Add contacts to a subscription. |
| DELETE | `https://e1.envoke.com/v2/subscriptions/{ID}/contacts` | Remove contacts from a subscription. |
Replace `{ID}` with the Envoke subscription ID. Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
---
## Request Body
Provide exactly one of `contact_ids` or `contact_emails`. You cannot include both in the same request.
| Field | Type | Description |
|---|---|---|
| `contact_ids` | array of integers | Envoke contact IDs to add or remove. Maximum 1,000 items per request. |
| `contact_emails` | array of strings | Contact email addresses to add or remove. Maximum 1,000 items per request. |
---
## Response
### Success (all contacts found)
```json
{
"success": true,
"result_text": "Set to contacts",
"result_data": {
"processed": 3
}
}
```
### Success (some contacts not found)
Contacts that could not be matched are listed in `not_found`. The operation is still considered successful as long as no found contacts failed to update.
```json
{
"success": true,
"result_text": "Set to contacts. Some contacts not found",
"result_data": {
"processed": 3,
"not_found": ["ghost@example.com"]
}
}
```
### Failure
If one or more found contacts failed during the update, `success` is `false` and the failures are listed in `failed`.
```json
{
"success": false,
"result_text": "Error setting to contacts",
"result_data": {
"processed": 3,
"failed": [
{ "contact_id": "456", "reason": "error message" }
]
}
}
```
### `result_data` fields
| Field | Type | Description |
|---|---|---|
| `processed` | integer | Total number of input identifiers submitted (found + not found). |
| `not_found` | array | IDs or emails that were not matched to any contact in the account. Only present when at least one was not found. |
| `failed` | array | Contacts that were found but failed during the update. Only present when failures occurred. |
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Either 'contact_ids' or 'contact_emails' must be provided"]
}
}
```
If the request body is completely empty (`{}`), a validation envelope is returned:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Request body may not be empty"]
}
}
```
Common validation errors (returned when a non-empty body is provided but is missing or invalid):
| Error | Cause |
|---|---|
| `Either 'contact_ids' or 'contact_emails' must be provided` | Body was provided but contained neither field. |
| `Provide either 'contact_ids' or 'contact_emails', not both` | Both fields were included. |
| `The field 'contact_ids' must be an array` | `contact_ids` was not an array. |
| `The field 'contact_ids' must not be empty` | `contact_ids` was an empty array. |
| `The field 'contact_ids' may not contain more than 1000 items` | More than 1,000 IDs were submitted. |
| `The field 'contact_emails' must be an array` | `contact_emails` was not an array. |
| `The field 'contact_emails' must not be empty` | `contact_emails` was an empty array. |
| `The field 'contact_emails' may not contain more than 1000 items` | More than 1,000 emails were submitted. |
| `Item with id '{id}' not found` | The subscription ID in the URL does not match any subscription in your account. |
---
## Examples
### Add contacts by ID
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_ids": [101, 102, 103]}' \
"https://e1.envoke.com/v2/subscriptions/12/contacts"
```
### Add contacts by email
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_emails": ["alice@example.com", "bob@example.com"]}' \
"https://e1.envoke.com/v2/subscriptions/12/contacts"
```
### Remove contacts by ID
```bash
curl -X DELETE -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_ids": [101, 102]}' \
"https://e1.envoke.com/v2/subscriptions/12/contacts"
```
### Remove contacts by email
```bash
curl -X DELETE -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_emails": ["alice@example.com"]}' \
"https://e1.envoke.com/v2/subscriptions/12/contacts"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/customfields/fields/
Markdown: https://envoke-developers.pages.dev/v2/customfields/fields.md
# Custom Field API - Fields Reference
This article describes all fields available through the Custom Field API. Use this as a reference when reading, creating, or updating custom fields.
---
## Overview
Custom fields let you store additional data about your contacts beyond the standard Envoke contact fields. The Custom Field API allows you to manage these field definitions (create, read, update, delete). To store or retrieve values for a custom field on a specific contact, use the Contact API.
All endpoints in the Custom Field API require [HTTP Basic Auth](https://envoke-developers.pages.dev/guides/authentication/) using your API ID and API Key.
---
## Fields
| Field | Type | Read-only | Required on Create | Description |
|---|---|---|---|---|
| `id` | string | Yes | — | Unique identifier assigned on creation. |
| `name` | string | No | Yes | The field identifier used to reference this custom field. |
| `description` | string | No | No | Optional human-readable description of the field. |
| `field_type` | string (enum) | Yes | Yes | The data type for values stored in this field. Cannot be changed after creation. |
---
## Field Details
### `id`
- **Type:** String
- **Read-only:** Yes — assigned automatically on creation.
- **Example:** `"42"`
The `id` is used to identify the custom field in update and delete requests, and to associate values with the field in the Contact API.
---
### `name`
- **Type:** String
- **Read-only:** No — can be updated via PATCH.
- **Required on create:** Yes
- **Validation:** Must contain only alphanumeric characters and underscores (`a-z`, `A-Z`, `0-9`, `_`). Must be unique per account.
- **Example:** `phone_number`
The `name` is how you reference the custom field in other contexts (e.g., when reading or writing contact values). Choose a name that is descriptive and consistent with your existing naming conventions.
---
### `description`
- **Type:** String
- **Read-only:** No — can be updated via PATCH.
- **Required on create:** No
- **Example:** `"Customer's mobile phone number"`
An optional human-readable label for the field. Returns an empty string (`""`) when not set.
---
### `field_type`
- **Type:** String (enum)
- **Read-only:** Yes — **cannot be changed after creation.**
- **Required on create:** Yes
- **Allowed values:** `text`, `note`, `date`, `url`
Controls what kind of value is stored in this field for each contact and how it is displayed in the Envoke UI.
| Value | Description |
|---|---|
| `text` | A standard single-line text input. |
| `note` | A multi-line text area. |
| `date` | A date value. Displays with a date selector in the UI. |
| `url` | A URL. Can be used as a merge field link target in email campaigns. |
**Important:** `field_type` is immutable. If you need to change the type of an existing field, you must delete it and create a new one. Note that deleting a custom field also removes all stored values for that field across all contacts.
---
## Error Responses
All error responses follow this structure:
**Validation errors** (e.g., missing required field, invalid `field_type`):
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"field_type is required"
]
}
}
```
**Not-found behaviour varies by method:**
- **GET** `/customfields/{id}` — when the ID is not found, returns an empty array (`[]`).
- **PATCH / DELETE** `/customfields/{id}` — when the ID is not found, returns a validation error:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '42' not found"]
}
}
```
---
*Full reference, OpenAPI description, and Postman collection: [developers.envoke.com](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/v2/customfields/read/
Markdown: https://envoke-developers.pages.dev/v2/customfields/read.md
# Custom Field API - Read
Use the read endpoints to retrieve custom field definitions for your account. You can retrieve all fields at once or look up a single field by its ID.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| GET | `https://e1.envoke.com/v2/customfields` | Return a list of custom fields. |
| GET | `https://e1.envoke.com/v2/customfields/{ID}` | Return a single custom field. |
Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## List Custom Fields
```
GET https://e1.envoke.com/v2/customfields
```
Returns a JSON array of all contact-scoped custom fields for your account, sorted alphabetically by name by default. Supports filtering, sorting, pagination, and field selection.
### 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). |
| `skip` | integer | `0` | Number of records to skip. Use with `limit` to paginate through results. |
| `fields` | string | *(all)* | Comma-separated list of fields to include in each result object. See note below about limit behaviour. |
| `result_type` | string | *(array)* | Set to `kvp` to receive results as a key-value object (`{ "id": "name" }`) instead of an array. See note below about limit behaviour. |
| `filter[id]` | string | — | Filter by custom field ID (exact match). |
| `filter[name]` | string | — | Filter by field name (exact match). |
| `filter[field_type]` | string | — | Filter by field type (exact match). Values: `text`, `note`, `date`, `url`. |
| `filter[description]` | string | — | Filter by description (exact match). |
| `sort[name]` | string | — | Sort by field name. Value: `asc` or `desc`. |
| `sort[field_type]` | string | — | Sort by field type. Value: `asc` or `desc`. |
**Note — limit behaviour with `?fields=` and `?result_type=kvp`:** When either of these parameters is present, the request is treated as a "shaped" response. In this mode the default limit increases to 10,000 (returning all records if no limit is specified) and an explicit `?limit=` value above 100 is honoured as-is. This is intentional so that callers building dropdowns or full exports do not need to paginate.
### Response
A JSON array of custom field objects. Returns an empty array (`[]`) if no custom fields have been created.
```json
[
{
"id": "1",
"name": "phone_number",
"description": "Customer's mobile phone number",
"field_type": "text"
},
{
"id": "2",
"name": "birthday",
"description": "",
"field_type": "date"
}
]
```
---
## Get a Single Custom Field
```
GET https://e1.envoke.com/v2/customfields/{ID}
```
Returns a single custom field object. Replace `{ID}` with the Envoke custom field ID.
### Response
```json
{
"id": "1",
"name": "phone_number",
"description": "Customer's mobile phone number",
"field_type": "text"
}
```
If the ID is not found, the API returns an empty array (`[]`) rather than an error object.
---
## Examples
### Return all custom fields
```bash
curl -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/customfields"
```
### Return the second page of results (10 per page)
```bash
curl -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/customfields?limit=10&skip=10"
```
### Filter by field type
```bash
curl -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/customfields?filter%5Bfield_type%5D=text"
```
### Sort by name ascending
```bash
curl -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/customfields?sort%5Bname%5D=asc"
```
### Sort by name descending
```bash
curl -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/customfields?sort%5Bname%5D=desc"
```
### Sort by field type
```bash
curl -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/customfields?sort%5Bfield_type%5D=asc"
```
### Return only specific fields
```bash
curl -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/customfields?fields=id,name,field_type"
```
Response:
```json
[
{ "id": "1", "name": "phone_number", "field_type": "text" },
{ "id": "2", "name": "birthday", "field_type": "date" }
]
```
### Return results as a key-value map
Use `result_type=kvp` to get a flat object mapping each field's ID to its name. This is useful for populating UI dropdowns.
```bash
curl -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/customfields?result_type=kvp"
```
Response:
```json
{
"1": "phone_number",
"2": "birthday"
}
```
### Return a single field by ID
```bash
curl -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/customfields/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).*
---
URL: https://envoke-developers.pages.dev/v2/customfields/insert/
Markdown: https://envoke-developers.pages.dev/v2/customfields/insert.md
# Custom Field API - Insert
Use this endpoint to create a new contact-scoped custom field for your account.
---
## Endpoint
```
POST https://e1.envoke.com/v2/customfields
```
Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
---
## Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | The field identifier. Alphanumeric characters and underscores only. Must be unique per account. |
| `field_type` | string | Yes | The data type for this field. One of: `text`, `note`, `date`, `url`. Cannot be changed after creation. |
| `description` | string | No | A human-readable description of the field. |
### `field_type` Values
| Value | Description |
|---|---|
| `text` | Single-line text input. |
| `note` | Multi-line text area. |
| `date` | Date value, displayed with a date selector in the UI. |
| `url` | URL value. Can be used as a merge field link target in email campaigns. |
---
## Response
On success, the API returns the newly created custom field object:
```json
{
"success": true,
"result_text": "Created",
"result_data": {
"id": "42",
"name": "phone_number",
"description": "Customer's mobile phone number",
"field_type": "text"
}
}
```
The `result_data.id` value is the Envoke custom field ID. Store this if you need to reference the field in subsequent API calls.
---
## Validation Errors
If the request body is invalid, the API returns a `400` response with an `errors` array:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'name' is required",
"field_type is required"
]
}
}
```
Common validation errors:
| Error | Cause |
|---|---|
| `Request body may not be empty` | No JSON body was provided. |
| `The field 'name' is required` | `name` was missing or empty. |
| `Only alphanumeric and underscore characters are permitted in the name field` | `name` contained spaces or special characters. |
| `field_type is required` | `field_type` was missing. |
| `Invalid 'field_type'; allowed values are: text, note, date, url` | `field_type` was not one of the allowed values. |
| `A custom field named '{name}' already exists` | A custom field with that name already exists in your account. |
---
## Examples
### Create a text field
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "phone_number", "field_type": "text", "description": "Customer mobile number"}' \
"https://e1.envoke.com/v2/customfields"
```
### Create a date field
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "birthday", "field_type": "date"}' \
"https://e1.envoke.com/v2/customfields"
```
### Create a URL field
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "company_website", "field_type": "url", "description": "Company website URL"}' \
"https://e1.envoke.com/v2/customfields"
```
---
**Important:** The `field_type` of a custom field cannot be changed after creation. If you need to change a field's type, you must delete the existing field and create a new one. Deleting a custom field also permanently removes all stored values for that field across all of your contacts.
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/customfields/update/
Markdown: https://envoke-developers.pages.dev/v2/customfields/update.md
# Custom Field API - Update
Use this endpoint to update an existing custom field. You can update a field's `name` and/or `description`. The `field_type` cannot be changed after creation.
---
## Endpoint
```
PATCH https://e1.envoke.com/v2/customfields/{ID}
```
Replace `{ID}` with the Envoke custom field ID you want to update. Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
---
## Request Body
| Field | Type | Description |
|---|---|---|
| `name` | string | New name for the field. Alphanumeric characters and underscores only. Must be unique per account. |
| `description` | string | New description for the field. Pass an empty string (`""`) to clear an existing description. |
At least one of `name` or `description` must be provided.
**Note:** `field_type` is read-only and cannot be included in an update request. Attempting to change it will return a validation error. If `id` is included in the request body, it must match the `{ID}` in the URL — if they differ, a validation error is returned: `"Provided 'id': {url_id} does not match 'id' in request body ({body_id})"`.
---
## Response
On success, the API returns the updated custom field object:
```json
{
"success": true,
"result_text": "Updated",
"result_data": {
"id": "42",
"name": "mobile_number",
"description": "Customer's mobile phone number",
"field_type": "text"
}
}
```
---
## Validation Errors
If the request is invalid, the API returns a `400` response with an `errors` array:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'field_type' cannot be changed after creation"
]
}
}
```
Common errors:
| Error | Cause |
|-------------------------------------------------------------------------------|---|
| `Request body may not be empty` | No JSON body was provided. |
| `The field 'field_type' cannot be changed after creation` | `field_type` was included in the request body. |
| `Only alphanumeric and underscore characters are permitted in the name field` | New `name` contained spaces or special characters. |
| `A custom field named '{name}' already exists` | The new `name` conflicts with an existing custom field in your account. |
| `Item with id '{id}' not found` | The `{ID}` in the URL does not match any custom field in your account. |
| `Provided 'id': {url_id} does not match 'id' in request body ({body_id})` | `id` in the request body differs from `{ID}` in the URL. |
---
## Examples
### Update a field's description
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"description": "Customer primary mobile number"}' \
"https://e1.envoke.com/v2/customfields/42"
```
### Rename a field
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "mobile_number"}' \
"https://e1.envoke.com/v2/customfields/42"
```
### Update both name and description
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "mobile_number", "description": "Customer primary mobile number"}' \
"https://e1.envoke.com/v2/customfields/42"
```
### Clear a field's description
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"description": ""}' \
"https://e1.envoke.com/v2/customfields/42"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/customfields/delete/
Markdown: https://envoke-developers.pages.dev/v2/customfields/delete.md
# Custom Field API - Delete
Use this endpoint to permanently delete a custom field from your account.
---
## Endpoint
```
DELETE https://e1.envoke.com/v2/customfields/{ID}
```
Replace `{ID}` with the Envoke custom field ID you want to delete. Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## Important
**Deleting a custom field is permanent and cannot be undone.**
When a custom field is deleted, all stored values for that field are also deleted across every contact in your account. If you have contacts with values stored in this field, those values will be permanently lost.
If you only want to stop using a custom field, consider updating its `description` to mark it as deprecated rather than deleting it.
---
## Response
On success:
```json
{
"success": true,
"result_text": "Deleted",
"result_data": []
}
```
If the custom field ID is not found:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '42' not found"]
}
}
```
---
## Example
```bash
curl -X DELETE -gs \
-u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/customfields/42"
```
Replace `42` with the ID of the custom field you want to delete, and substitute your actual API ID and API Key.
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/groups/fields/
Markdown: https://envoke-developers.pages.dev/v2/groups/fields.md
# Group API - Fields Reference
This article describes all fields available through the Group API. Use this as a reference when reading, creating, or updating groups.
---
## Overview
The Group API lets you manage groups programmatically. Groups are used to organise tags, subscriptions, messages, and templates into named categories within your Envoke account. Each item type has its own group namespace, accessible via a different URL path.
Groups support **one level of nesting**: a top-level group can have subgroups, but a subgroup cannot itself have children.
All endpoints require [HTTP Basic Auth](https://envoke-developers.pages.dev/guides/authentication/) using your API ID and API Key.
---
## URL Paths by Item Type
The Group API exposes four separate paths — one for each type of item that can be grouped:
| URL path | Groups for... |
|---|---|
| `https://e1.envoke.com/v2/groups/tags` | Tags (interests) |
| `https://e1.envoke.com/v2/groups/subscriptions` | Subscriptions |
| `https://e1.envoke.com/v2/groups/messages` | Messages |
| `https://e1.envoke.com/v2/groups/templates` | Templates |
All four paths accept the same request and response format described in this article.
---
## Fields
| Field | Type | Read-only | Required on Create | Description |
|---|---|---|---|---|
| `id` | string | Yes | — | Unique identifier assigned on creation. |
| `name` | string | No | Yes | The group name. |
| `parent_group` | string | No | No | Name of the parent group. Empty string (`""`) when the group is top-level. |
---
## Field Details
### `id`
- **Type:** String
- **Read-only:** Yes — assigned automatically on creation.
- **Example:** `"7"`
Used to identify the group in update, delete, and filter requests.
---
### `name`
- **Type:** String
- **Read-only:** No — updatable via PATCH.
- **Required on create:** Yes
- **Example:** `"Academic Programs"`
The group name as it appears within your Envoke account.
---
### `parent_group`
- **Type:** String (read); String, integer, or null (write)
- **Read-only:** No — updatable via PATCH.
- **Required on create:** No
- **Returned value (read):** The parent group's name as a string. Returns `""` (empty string) when the group is top-level.
- **Accepted values (write):** Parent group name (string) or parent group ID (integer).
- **Example:** `"Academic Programs"`, `""`
**Nesting rules:**
- A top-level group has `parent_group: ""`.
- A subgroup has `parent_group: ""`.
- The parent must itself be a top-level group — you cannot nest a subgroup under another subgroup. The maximum depth is 1.
**Removing a parent:** On PATCH, pass `null` or `""` (empty string) to promote a subgroup to top-level. This removes the parent association.
---
## Error Responses
**Validation errors** (missing required fields, invalid values):
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'name' is required"
]
}
}
```
**Not-found behaviour varies by method:**
- **GET** `/groups/{type}/{id}` — when the ID is not found, returns an empty array (`[]`).
- **PATCH** and **DELETE** `/groups/{type}/{id}` — when the ID is not found, returns a validation error:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '999999' not found"]
}
}
```
**Delete constraint** — a group with subgroups or assigned items cannot be deleted:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Only empty groups can be deleted"]
}
}
```
---
*Full reference, OpenAPI description, and Postman collection: [developers.envoke.com](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/v2/groups/read/
Markdown: https://envoke-developers.pages.dev/v2/groups/read.md
# Group API - Read
Use the read endpoints to retrieve groups for your account. You can list all groups for a given item type with filtering and sorting, or retrieve a single group by ID.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| GET | `https://e1.envoke.com/v2/groups/{type}` | Return a list of groups. |
| GET | `https://e1.envoke.com/v2/groups/{type}/{ID}` | Return a single group. |
Replace `{type}` with one of: `tags`, `subscriptions`, `messages`, or `templates`.
Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## List Groups
```
GET https://e1.envoke.com/v2/groups/{type}
```
Returns a JSON array of groups for your account and the specified item type, sorted alphabetically by name. Supports filtering, sorting, pagination, and field selection.
### 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). |
| `skip` | integer | `0` | Number of records to skip. Use with `limit` to paginate. |
| `fields` | string | *(all)* | Comma-separated list of fields to include in each result. See limit note below. |
| `result_type` | string | *(array)* | Set to `kvp` to receive results as a key-value object (`{ "id": "name" }`). See limit note below. |
| `filter[id]` | string | — | Filter by group ID. |
| `filter[name]` | string | — | Filter by group name. |
| `filter[parent_group]` | string | — | Filter by parent group name or numeric ID. |
| `sort[name]` | string | — | Sort by name. Value: `asc` or `desc`. |
**Note — limit behaviour with `?fields=` and `?result_type=kvp`:** When either of these parameters is present, the default limit increases to 10,000 (returning all records if no explicit limit is specified) and a `?limit=` value above 100 is honoured as-is. This allows full exports and dropdown population without pagination.
### Filter Operators
All `filter[field]` parameters support exact match and the not-equal operator:
| Operator | Example | Description |
|---|---|---|
| *(none)* | `?filter[name]=Academic Programs` | Exact match |
| `(ne)` | `?filter[parent_group][(ne)]=Academic Programs` | Not equal |
The `filter[parent_group]` parameter accepts either a group name or a numeric group ID. Name lookups are case-sensitive.
*Note: URL-encode brackets in production requests (e.g. `%5B` for `[`).*
### Response
A JSON array of group objects. Returns an empty array (`[]`) if no groups exist or no results match the filter.
```json
[
{
"id": "7",
"name": "Graduate Studies",
"parent_group": "Academic Programs"
},
{
"id": "3",
"name": "Student Life",
"parent_group": ""
}
]
```
`parent_group` is always present. It is an empty string (`""`) for top-level groups.
---
## Get a Single Group
```
GET https://e1.envoke.com/v2/groups/{type}/{ID}
```
Returns a single group object. Replace `{type}` and `{ID}` with the item type and Envoke group ID.
If the ID is not found, the API returns an empty array (`[]`).
### Response
```json
{
"id": "7",
"name": "Graduate Studies",
"parent_group": "Academic Programs"
}
```
---
## Examples
### List all tag groups (default pagination)
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/groups/tags"
```
### List all subscription groups
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/groups/subscriptions"
```
### Filter by parent group name
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/groups/tags?filter%5Bparent_group%5D=Academic+Programs"
```
### Filter top-level groups only
Filtering for groups with no parent — pass an empty string or `0`:
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/groups/tags?filter%5Bparent_group%5D="
```
### Filter by name
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/groups/tags?filter%5Bname%5D=Student+Life"
```
### Sort by name descending
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/groups/tags?sort%5Bname%5D=desc"
```
### Return only specific fields
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/groups/tags?fields=id,name"
```
### Return as a key-value map (for dropdowns)
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/groups/tags?result_type=kvp"
```
Response:
```json
{ "3": "Student Life", "7": "Graduate Studies" }
```
### Get a single group by ID
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/groups/tags/7"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/groups/insert/
Markdown: https://envoke-developers.pages.dev/v2/groups/insert.md
# Group API - Insert
Use this endpoint to create a new group in your account.
---
## Endpoint
```
POST https://e1.envoke.com/v2/groups/{type}
```
Replace `{type}` with one of: `tags`, `subscriptions`, `messages`, or `templates`.
Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
---
## Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | The group name. |
| `parent_group` | string, integer, or null | No | Parent group name (string) or parent group ID (integer) to nest this group under. Omit, pass `null`, or pass `""` to create a top-level group. The parent must itself be top-level — only one level of nesting is supported. |
---
## Response
On success, the API returns the newly created group object:
```json
{
"success": true,
"result_text": "Created",
"result_data": {
"id": "8",
"name": "Graduate Studies",
"parent_group": "Academic Programs"
}
}
```
`result_data.id` is the Envoke group ID. Store this if you need to update or delete the group later.
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'name' is required"
]
}
}
```
Common validation errors:
| Error | Cause |
|---|---|
| `Request body may not be empty` | No JSON body was provided. |
| `The field 'name' is required` | `name` was missing or blank. |
| `The 'parent_group' '{value}' not found` | The parent group name or ID was not found in this account for this item type. |
| `The field 'parent_group' cannot be a nested group (maximum depth is 1)` | The specified parent is itself a subgroup — only top-level groups can be used as parents. |
---
## Examples
### Create a top-level tag group
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Academic Programs"}' \
"https://e1.envoke.com/v2/groups/tags"
```
### Create a subgroup (by parent name)
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Graduate Studies", "parent_group": "Academic Programs"}' \
"https://e1.envoke.com/v2/groups/tags"
```
### Create a subgroup (by parent ID)
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Graduate Studies", "parent_group": 7}' \
"https://e1.envoke.com/v2/groups/tags"
```
### Create a subscription group
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Stakeholder Communications"}' \
"https://e1.envoke.com/v2/groups/subscriptions"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/groups/update/
Markdown: https://envoke-developers.pages.dev/v2/groups/update.md
# Group API - Update
Use this endpoint to update an existing group. You can rename the group or change its parent.
---
## Endpoint
```
PATCH https://e1.envoke.com/v2/groups/{type}/{ID}
```
Replace `{type}` with one of: `tags`, `subscriptions`, `messages`, or `templates`. Replace `{ID}` with the Envoke group ID you want to update.
Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
---
## Request Body
At least one field must be provided.
| Field | Type | Description |
|---|---|---|
| `name` | string | New group name. Cannot be blank. |
| `parent_group` | string, integer, or null | Parent group name or ID to assign. Pass `null` or `""` (empty string) to promote the group to top-level (remove its parent). The parent must itself be top-level. |
If `id` is included in the request body, it must match the `{ID}` in the URL — if they differ, a validation error is returned: `"Provided 'id': {url_id} does not match 'id' in request body ({body_id})"`.
---
## Response
On success, the API returns the updated group object:
```json
{
"success": true,
"result_text": "Updated",
"result_data": {
"id": "7",
"name": "Graduate Studies (Updated)",
"parent_group": "Academic Programs"
}
}
```
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"Item with id '999999' not found"
]
}
}
```
Common errors:
| Error | Cause |
|---------------------------------------------------------------------------|---|
| `Request body may not be empty` | No JSON body was provided. |
| `Item with id '{id}' not found` | The `{ID}` in the URL does not match any group of this type in your account. |
| `The field 'name' cannot be empty` | `name` was provided but was blank or whitespace only. |
| `The 'parent_group' '{value}' not found` | The parent group name or ID was not found for this item type. |
| `The field 'parent_group' cannot be a nested group (maximum depth is 1)` | The specified parent is itself a subgroup. |
| `Provided 'id': {url_id} does not match 'id' in request body ({body_id})` | `id` in the request body differs from `{ID}` in the URL. |
---
## Examples
### Rename a group
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Academic Programs & Research"}' \
"https://e1.envoke.com/v2/groups/tags/7"
```
### Assign a parent group (make it a subgroup)
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"parent_group": "Academic Programs"}' \
"https://e1.envoke.com/v2/groups/tags/8"
```
### Promote to top-level (remove parent) using null
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"parent_group": null}' \
"https://e1.envoke.com/v2/groups/tags/8"
```
### Promote to top-level (remove parent) using empty string
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"parent_group": ""}' \
"https://e1.envoke.com/v2/groups/tags/8"
```
### Rename and reassign parent at the same time
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Graduate Studies", "parent_group": "Academic Programs"}' \
"https://e1.envoke.com/v2/groups/tags/8"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/groups/delete/
Markdown: https://envoke-developers.pages.dev/v2/groups/delete.md
# Group API - Delete
Use this endpoint to delete a group from your account.
---
## Endpoint
```
DELETE https://e1.envoke.com/v2/groups/{type}/{ID}
```
Replace `{type}` with one of: `tags`, `subscriptions`, `messages`, or `templates`. Replace `{ID}` with the Envoke group ID you want to delete.
Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## Important
**A group must be empty before it can be deleted.** The API returns the same validation error whether the group is blocked by subgroups or by assigned items:
- **Subgroups:** Delete or reassign all subgroups first.
- **Assigned items:** Tags, subscriptions, messages, or templates assigned to this group must be deleted or moved to a different group first.
**Deletion is permanent** and cannot be undone via the API.
---
## Response
### Success
```json
{
"success": true,
"result_text": "Deleted",
"result_data": []
}
```
### Group is not empty
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Only empty groups can be deleted"]
}
}
```
### Group not found
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '999999' not found"]
}
}
```
---
## Example
```bash
curl -X DELETE -gs \
-u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/groups/tags/7"
```
Replace `tags` with the appropriate item type and `7` with the ID of the group you want to delete.
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/templates/fields/
Markdown: https://envoke-developers.pages.dev/v2/templates/fields.md
# Template API - Fields Reference
This article describes all fields available through the Template API. Use this as a reference when reading, creating, or updating templates.
---
## Overview
The Template API lets you manage your Envoke email templates programmatically — create new templates, update existing ones, and retrieve template details including HTML content. Templates can be assigned to groups, tagged with a language, and toggled between published and unpublished states.
All endpoints require [HTTP Basic Auth](https://envoke-developers.pages.dev/guides/authentication/) using your API ID and API Key.
---
## Fields
| Field | Type | Read-only | Required on Create | Description |
|---|---|---|---|---|
| `id` | string | Yes | — | Unique identifier assigned on creation. Returned as a string. |
| `external_id` | string | Yes | — | Randomly assigned identifier used to build the preview URL. |
| `preview_url` | string | Yes | — | Public URL to preview the template in a browser. |
| `name` | string | No | No | Template name. Minimum 3 characters. Auto-generated if omitted. |
| `status` | string (enum) | No | No | `published` or `unpublished`. Defaults to `published` on create if not specified. |
| `language` | string | No | No | IETF language code (e.g. `en`, `fr`). Defaults to `en`. |
| `group` | string | No | No | Name of the group the template belongs to. Empty string (`""`) when unset. |
| `create_time` | string | Yes | — | Timestamp when the template was created. |
| `modified_time` | string | Yes | — | Timestamp when the template was last modified. |
| `html` | string | — | See below | Full HTML content. Write on create/update; read only via `?include_html=1`. |
---
## Field Details
### `id`
- **Type:** String
- **Read-only:** Yes — assigned automatically on creation.
- **Example:** `"42"`
Used to identify the template in update, delete, and single-record GET requests.
---
### `external_id`
- **Type:** String
- **Read-only:** Yes — assigned automatically on creation.
- **Example:** `"a1b2c3d4e5f6g7h8"`
A randomly assigned identifier. Not the same as `id`. Used to construct the `preview_url`.
---
### `preview_url`
- **Type:** String
- **Read-only:** Yes — computed from the account's external ID and the template's `external_id`.
- **Format:** `https://{domain}/m/{account_external_id}/m/{external_id}`
- **Example:** `"https://engage.envoke.com/m/acme-corp/m/a1b2c3d4e5f6g7h8"`
A public URL that shows the template in a browser without logging in. `{domain}` is your account's custom domain if it has one, and otherwise Envoke's message domain; it is not the API host. Use the URL as returned rather than building it yourself.
---
### `name`
- **Type:** String
- **Read-only:** No — updatable via PATCH.
- **Required on create:** No — if omitted, auto-generated as `API Template {timestamp}`.
- **Validation:** Minimum 3 characters (applies when provided).
- **Example:** `"Welcome Email"`
---
### `status`
- **Type:** String (enum)
- **Read-only:** No — updatable via PATCH.
- **Required on create:** No
- **Allowed values:** `published`, `unpublished`
Controls whether the template is available for use in campaigns.
On create, `status` defaults to `published` if not specified. Pass `"unpublished"` to create the template in a draft state. On PATCH, accepted values are `"published"` and `"unpublished"`.
---
### `language`
- **Type:** String
- **Read-only:** No — updatable via PATCH.
- **Required on create:** No — defaults to `en`.
- **Accepted values (write):** IETF language code (e.g. `en`) or numeric language ID.
- **Returned value (read):** Always the IETF language code.
- **Example:** `"en"`, `"fr"`
Common supported codes: `en`, `fr`, `ar`, `zh`, `de`, `it`, `ja`, `ko`, `pt`, `ru`, `es`.
---
### `group`
- **Type:** String
- **Read-only:** No — updatable via PATCH.
- **Required on create:** No
- **Accepted values (write):** Group name (string) or group ID (integer). The group must be of type `message_template`. Pass an empty string (`""`) to remove the group assignment.
- **Returned value (read):** The group name as a string. Returns `""` (empty string) when no group is assigned.
- **Example:** `"Marketing Templates"`, `""`
---
### `create_time`
- **Type:** String
- **Read-only:** Yes
- **Format:** `Y-m-d H:i:s` in the account's configured timezone.
- **Example:** `"2026-04-11 09:00:00"`
---
### `modified_time`
- **Type:** String
- **Read-only:** Yes
- **Format:** `Y-m-d H:i:s` in the account's configured timezone.
- **Example:** `"2026-04-11 14:22:10"`
Updated automatically whenever the template is saved.
---
### `html`
- **Type:** String
- **Maximum size:** 200 KB
- **Write:** Accepted in POST (create) and PATCH (update) request bodies.
- **Read:** Only returned on `GET /v2/templates/{id}?include_html=1`. Never included in list responses or in create/update response bodies.
The full HTML source of the template. On create, either `html` or `template_id` must be provided (see below).
---
## Write-Only Field: `template_id`
| Field | Type | Operations | Description |
|---|---|---|---|
| `template_id` | integer | POST only | ID of an existing template or message to copy HTML from. |
`template_id` is a write-only convenience field for creating a new template by copying the HTML from an existing one. It is never returned in any GET response. The source must be owned by your account or accessible as a shared template.
Either `html` or `template_id` must be provided when creating a template. `name` is optional — if omitted, the template is named `API Template {timestamp}`. If neither `html` nor `template_id` is included, the request will fail with a validation error.
---
## Error Responses
**Validation errors** (missing fields, invalid values, name too short, etc.):
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'name' must be at least 3 characters long"
]
}
}
```
Some errors also include an `allowed_values` object listing the valid options for the offending field:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Invalid 'status'"],
"allowed_values": {
"status": ["published", "unpublished"]
}
}
}
```
**Not-found behaviour varies by method:**
- **GET** `/templates/{id}` — when the ID is not found, returns an empty array (`[]`).
- **PATCH** `/templates/{id}` — when the ID is not found, returns a validation error:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '999999' not found"]
}
}
```
- **DELETE** `/templates/{id}` — when the ID is not found, returns a validation error:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '999999' not found"]
}
}
```
---
*Full reference, OpenAPI description, and Postman collection: [developers.envoke.com](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/v2/templates/read/
Markdown: https://envoke-developers.pages.dev/v2/templates/read.md
# Template API - Read
Use the read endpoints to retrieve email templates for your account. You can list all templates with filtering and sorting, retrieve a single template by ID, or fetch a template's full HTML content.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| GET | `https://e1.envoke.com/v2/templates` | Return a list of templates. |
| GET | `https://e1.envoke.com/v2/templates/{ID}` | Return a single template. |
Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## List Templates
```
GET https://e1.envoke.com/v2/templates
```
Returns a JSON array of templates for your account, sorted by most recently modified first. Supports filtering, sorting, pagination, and field selection.
### 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). |
| `skip` | integer | `0` | Number of records to skip. Use with `limit` to paginate. |
| `fields` | string | *(all)* | Comma-separated list of fields to include in each result. See limit note below. |
| `result_type` | string | *(array)* | Set to `kvp` to receive results as a key-value object (`{ "id": "name" }`). See limit note below. |
| `filter[name]` | string | — | Filter by template name. |
| `filter[status]` | string | — | Filter by status: `published` or `unpublished`. |
| `filter[language]` | string | — | Filter by language. Accepts a two-letter IETF code (`en`, `fr`). |
| `filter[group]` | string | — | Filter by group name. |
| `filter[id]` | integer | — | Filter by template ID. |
| `sort[name]` | string | — | Sort by template name. Value: `asc` or `desc`. |
| `sort[modified_time]` | string | — | Sort by last modified date. Value: `asc` or `desc`. |
| `sort[create_time]` | string | — | Sort by creation date. Value: `asc` or `desc`. |
| `sort[language]` | string | — | Sort by language. Value: `asc` or `desc`. |
| `sort[group]` | string | — | Sort by group name. Value: `asc` or `desc`. |
| `sort[status]` | string | — | Sort by status. Value: `asc` or `desc`. |
**Note — limit behaviour with `?fields=` and `?result_type=kvp`:** When either of these parameters is present, the default limit increases to 10,000 (returning all records if no explicit limit is specified) and a `?limit=` value above 100 is honoured as-is. This allows full exports and dropdown population without pagination.
### Filter Operators
All `filter[field]` parameters support the following operators:
| Operator | Example | Description |
|---|---|---|
| *(none)* | `?filter[status]=published` | Exact match |
| `(ne)` | `?filter[status][(ne)]=published` | Not equal |
*Note: The `(regex)` operator is not supported. Use exact match or `(ne)` instead.*
*Note: URL-encode brackets in production requests (e.g. `%5B` for `[`).*
### Response
A JSON array of template objects. Returns an empty array (`[]`) if no templates exist or no results match the filter.
```json
[
{
"id": "1",
"external_id": "a1b2c3d4e5f6",
"preview_url": "https://engage.envoke.com/m/acme-corp/m/a1b2c3d4e5f6",
"name": "Welcome Email",
"status": "published",
"language": "en",
"group": "Onboarding",
"create_time": "2026-04-01 09:00:00",
"modified_time": "2026-04-10 14:00:00"
}
]
```
`html` is never included in list responses.
---
## Get a Single Template
```
GET https://e1.envoke.com/v2/templates/{ID}
```
Returns a single template object. Replace `{ID}` with the Envoke template ID.
Add `?include_html=1` to include the full HTML content of the template in the response.
If the ID is not found, the API returns an empty array (`[]`).
### Response (default)
```json
{
"id": "42",
"external_id": "a1b2c3d4e5f6",
"preview_url": "https://engage.envoke.com/m/acme-corp/m/a1b2c3d4e5f6",
"name": "Welcome Email",
"status": "published",
"language": "en",
"group": "",
"create_time": "2026-04-11 09:00:00",
"modified_time": "2026-04-11 09:00:00"
}
```
### Response with `?include_html=1`
```json
{
"id": "42",
"external_id": "a1b2c3d4e5f6",
"preview_url": "https://engage.envoke.com/m/acme-corp/m/a1b2c3d4e5f6",
"name": "Welcome Email",
"status": "published",
"language": "en",
"group": "",
"create_time": "2026-04-11 09:00:00",
"modified_time": "2026-04-11 09:00:00",
"html": "Welcome!
"
}
```
---
## Examples
### Return all templates (default pagination)
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/templates"
```
### Paginate through results
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/templates?limit=10&skip=10"
```
### Filter by status
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/templates?filter%5Bstatus%5D=published"
```
### Filter by group
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/templates?filter%5Bgroup%5D=Onboarding"
```
### Filter by language
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/templates?filter%5Blanguage%5D=fr"
```
### Filter excluding a group
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/templates?filter%5Bgroup%5D%5B%28ne%29%5D=Archived"
```
### Sort by name ascending
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/templates?sort%5Bname%5D=asc"
```
### Sort by most recently created
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/templates?sort%5Bcreate_time%5D=desc"
```
### Return only specific fields
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/templates?fields=id,name,status"
```
### Return as a key-value map (for dropdowns)
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/templates?result_type=kvp"
```
Response:
```json
{ "1": "Welcome Email", "2": "Monthly Newsletter" }
```
### Get a single template by ID
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/templates/42"
```
### Get a single template with its HTML
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/templates/42?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).*
---
URL: https://envoke-developers.pages.dev/v2/templates/insert/
Markdown: https://envoke-developers.pages.dev/v2/templates/insert.md
# Template API - Insert
Use this endpoint to create a new email template in your account.
---
## Endpoint
```
POST https://e1.envoke.com/v2/templates
```
Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
---
## Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | No | Template name. Minimum 3 characters. If omitted, auto-generated as `API Template {timestamp}`. |
| `html` | string | Yes* | Full HTML content for the template. Maximum 200 KB. |
| `template_id` | integer | Yes* | ID of an existing template or message to copy HTML from. |
| `language` | string or integer | No | Language code (e.g. `en`, `fr`) or numeric language ID. Defaults to `en`. |
| `group` | string or integer | No | Group name or group ID to assign the template to. |
*Either `html` or `template_id` must be provided — but not necessarily both. If `template_id` is given, its HTML is copied into the new template.
**Note:** `status` defaults to `published` if not specified on create. Pass `"unpublished"` to create the template in a draft state.
---
## Response
On success, the API returns the newly created template object:
```json
{
"success": true,
"result_text": "Created",
"result_data": {
"id": "42",
"external_id": "a1b2c3d4e5f6",
"preview_url": "https://engage.envoke.com/m/acme-corp/m/a1b2c3d4e5f6",
"name": "Welcome Email",
"status": "published",
"language": "en",
"group": "Onboarding",
"create_time": "2026-04-11 09:00:00",
"modified_time": "2026-04-11 09:00:00"
}
}
```
`result_data.id` is the Envoke template ID. Store this if you need to update or delete the template later.
The `html` field is **not** included in the create response. To retrieve the HTML, use a separate `GET /v2/templates/{id}?include_html=1` request.
---
## Validation Errors
If the request body is invalid, the API returns a `400` response:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"One of the fields 'template_id' or 'html' must be provided in order to create a template"
]
}
}
```
Common validation errors:
| Error | Cause |
|---|---|
| `Request body may not be empty` | No JSON body was provided. |
| `One of the fields 'template_id' or 'html' must be provided in order to create a template` | Neither `html` nor `template_id` was included. |
| `The field 'name' must be at least 3 characters long` | `name` was fewer than 3 characters or missing. |
| `The field 'html' may not be longer than 200kB` | HTML content exceeded the 200 KB limit. |
| `Invalid 'language'` | The `language` value was not a recognised language code or ID. |
| `Invalid 'group'` | The `group` value did not match a valid group of type `message_template`. |
| `Nothing found for 'template_id': {id}` | The `template_id` does not exist or is not accessible to your account. |
| `'{field}' is not a valid field` | An unrecognised field was included in the request body. |
Some errors include an `allowed_values` object:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Invalid 'language'"],
"allowed_values": {
"language": { "1": "English", "2": "French" }
}
}
}
```
---
## Examples
### Create with inline HTML
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Welcome Email", "html": "Welcome!
"}' \
"https://e1.envoke.com/v2/templates"
```
### Create with a group and language
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Bienvenue", "html": "Bienvenue!
", "language": "fr", "group": "Onboarding"}' \
"https://e1.envoke.com/v2/templates"
```
### Create by copying an existing template
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Welcome Email (Copy)", "template_id": 10}' \
"https://e1.envoke.com/v2/templates"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/templates/update/
Markdown: https://envoke-developers.pages.dev/v2/templates/update.md
# Template API - Update
Use this endpoint to update an existing template. You can update the name, status, language, group assignment, and HTML content.
---
## Endpoint
```
PATCH https://e1.envoke.com/v2/templates/{ID}
```
Replace `{ID}` with the Envoke template ID you want to update. Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
---
## Request Body
At least one field must be provided.
| Field | Type | Description |
|---|---|---|
| `name` | string | New name. Minimum 3 characters. |
| `status` | string | `published` or `unpublished`. |
| `language` | string or integer | Language code (e.g. `en`, `fr`) or numeric language ID. |
| `group` | string or integer | Group name or group ID to assign. Pass an empty string (`""`) to remove the group assignment. |
| `html` | string | Replacement HTML content. Maximum 200 KB. |
The fields `external_id`, `preview_url`, `create_time`, and `modified_time` are silently ignored if included in the request body. If `id` is included, it must match the `{ID}` in the URL — if they differ, a validation error is returned: `"Provided 'id': {url_id} does not match 'id' in request body ({body_id})"`.
---
## Response
On success, the API returns the updated template object:
```json
{
"success": true,
"result_text": "Updated",
"result_data": {
"id": "42",
"external_id": "a1b2c3d4e5f6",
"preview_url": "https://engage.envoke.com/m/acme-corp/m/a1b2c3d4e5f6",
"name": "Updated Welcome Email",
"status": "published",
"language": "en",
"group": "",
"create_time": "2026-04-11 09:00:00",
"modified_time": "2026-04-11 15:00:00"
}
}
```
`html` is not included in the update response body. To retrieve the updated HTML, use `GET /v2/templates/{id}?include_html=1`.
---
## Validation Errors
All errors, including not-found, use the validation error envelope:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'name' must be at least 3 characters long"
]
}
}
```
Common errors:
| Error | Cause |
|---|---|
| `Request body may not be empty` | No JSON body was provided. |
| `The field 'name' must be at least 3 characters long` | New `name` was fewer than 3 characters. |
| `The field 'html' may not be longer than 200kB` | HTML content exceeded the 200 KB limit. |
| `Invalid 'status'` | `status` was not `published` or `unpublished`. |
| `Invalid 'language'` | The `language` value was not recognised. |
| `Invalid 'group'` | The group was not found or is not of type `message_template`. |
| `Item with id '{id}' not found` | The `{ID}` in the URL does not match any template in your account. |
---
## Examples
### Rename a template
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Updated Welcome Email"}' \
"https://e1.envoke.com/v2/templates/42"
```
### Unpublish a template
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "unpublished"}' \
"https://e1.envoke.com/v2/templates/42"
```
### Assign to a group
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"group": "Seasonal Campaigns"}' \
"https://e1.envoke.com/v2/templates/42"
```
### Remove group assignment
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"group": ""}' \
"https://e1.envoke.com/v2/templates/42"
```
### Replace HTML content
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "New content
"}' \
"https://e1.envoke.com/v2/templates/42"
```
### Update multiple fields at once
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Bienvenue", "language": "fr", "group": "Onboarding"}' \
"https://e1.envoke.com/v2/templates/42"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/templates/delete/
Markdown: https://envoke-developers.pages.dev/v2/templates/delete.md
# Template API - Delete
Use this endpoint to delete a template from your account.
---
## Endpoint
```
DELETE https://e1.envoke.com/v2/templates/{ID}
```
Replace `{ID}` with the Envoke template ID you want to delete. Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## Important
**Deletion is permanent from an API perspective** — a deleted template is immediately removed from all API responses and cannot be recovered via the API.
Internally, templates are soft-deleted: the record is archived rather than physically removed, so the underlying data may be retained temporarily during cleanup. However, you should treat deletion as irreversible.
---
## Response
On success:
```json
{
"success": true,
"result_text": "Deleted",
"result_data": []
}
```
If the template ID is not found:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": { "errors": ["Item with id '42' not found"] }
}
```
---
## Example
```bash
curl -X DELETE -gs \
-u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/templates/42"
```
Replace `42` with the ID of the template you want to delete, and substitute your actual API ID and API Key.
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/messages/fields/
Markdown: https://envoke-developers.pages.dev/v2/messages/fields.md
# Message API - Fields Reference
This article describes all fields available through the Message API. Use this as a reference when reading, creating, updating, or sending messages.
---
## Overview
The Message API lets you read, create, update, delete, send, and cancel the email messages in your Envoke account. A message is one email campaign: its settings (name, sender, subject, recipients, send time) and its HTML content. Messages are created as drafts; only a `Draft` message can be changed, and sending moves it through the statuses described under [`status`](#status).
The Message API answers on `/v2` only. Related resources have their own APIs: templates whose HTML a new message can copy through [`template_id`](#html-template_id-and-replace) are managed with the [Template API](https://envoke-developers.pages.dev/v2/templates/fields/), message groups (folders) with the [Group API](https://envoke-developers.pages.dev/v2/groups/fields/), the tags and subscriptions a message is sent to with the [Tag API](https://envoke-developers.pages.dev/v2/tags/fields/) and [Subscription API](https://envoke-developers.pages.dev/v2/subscriptions/fields/), and sender identities with the [Sender API](https://envoke-developers.pages.dev/v1/senders/fields/). Sending and cancelling are covered in [Sending](https://envoke-developers.pages.dev/v2/messages/sending/).
The API returns no delivery, open, or click statistics; the only counts it gives are the recipient estimate in a send response.
All endpoints require [HTTP Basic Auth](https://envoke-developers.pages.dev/guides/authentication/) using your API ID and API Key.
---
## Fields
| Field | Type | Read-only | Required on Create | Description |
|---|---|---|---|---|
| `id` | string | Yes | — | Envoke message ID, assigned on creation. |
| `external_id` | string | Yes | — | Externally visible identifier used in the preview URL. |
| `preview_url` | string | Yes | — | Public URL to view the message in a browser. |
| `create_time` | string | Yes | — | When the message was created, in the account time zone. |
| `modified_time` | string | Yes | — | When the message was last changed, in the account time zone. |
| `name` | string | No | No | Internal name. Generated if omitted. |
| `group` | string | No | No | Name of the message group (folder), or `""`. |
| `status` | string | Yes | — | Current status, e.g. `Draft` or `Sent`. Changed only by sending and cancelling. |
| `send_time` | string | No | No | Scheduled send time in the account time zone, or `""`. |
| `sender` | string | No | No | Sender identity as `Name `. Defaults to the account's default sender. |
| `reply_to` | string | No | No | Reply-to identity as `Name `. No default. |
| `subject` | string | No | No | Subject line. Required to send. |
| `language` | string | No | No | Language of the message, `en` or `fr`. Defaults to `en`. |
| `recipient_subscription` | string | No | No | Subscription the message is sent to, or `""`. |
| `recipient_tag` | string | No | No | Tag the message is sent to, or `""`. |
| `recipient_segment` | string | No | No | Segment the message is sent to, or `""`. |
| `recipient_language` | string | No | No | Send only to contacts with this language, `en` or `fr`, or `""` for all. |
| `publish_archive` | boolean | No | No | Whether the message is published to the account's web archive. |
| `utm_tag_links` | boolean | No | No | Whether UTM tracking parameters are added to links. |
| `html` | string | Write-only | One of `html` or `template_id` | The message HTML. Returned only on a single GET with `include_html`. |
| `template_id` | integer | Write-only | One of `html` or `template_id` | ID of a template or message to copy HTML from. Create only. Never returned. |
| `replace` | object | Write-only | No | Placeholder text to replace in the HTML when it is saved. Never returned. |
---
## Field Details
Every readable field is present in a read response unless `fields` is used to select some. An unset text value is returned as `""`, except `sender` and `reply_to`, which are returned as `null` when no identity is set (see [sender and reply_to](#sender-and-reply_to)). Read-only fields sent on create or update are silently ignored. A message object returned by a GET cannot always be sent back as is: a `null` `sender` or `reply_to` fails as `Invalid 'reply_to': ''`, a `send_time` of `""` fails as not in the future, and a `subject` of `""` fails as shorter than 6 characters, so remove those values first.
### `id`
- **Type:** String
- **Read-only:** Yes — assigned automatically on creation.
- **Returned value (read):** The message ID as a string.
- **Example:** `"2001"`
Identifies the message in the URL of single GET, update, delete, send, and cancel requests. The one exception to the string type is the response of `POST /v2/messages/send` (create and send), whose `result_data.id` is an integer (see [Sending](https://envoke-developers.pages.dev/v2/messages/sending/#create-and-send)).
---
### `external_id`
- **Type:** String (32 characters)
- **Read-only:** Yes — assigned automatically on creation.
- **Example:** `"3e8b1f6c9a2d4e7b0c5f8a1d3e6b9c2f"`
An externally visible identifier used to build `preview_url`. Not the same as `id`.
---
### `preview_url`
- **Type:** String
- **Read-only:** Yes — built from your account's external ID and the message's `external_id`.
- **Format:** `https://{domain}/m/{account_external_id}/m/{external_id}`
- **Example:** `"https://engage.envoke.com/m/a7c3e9f1b5d2/m/3e8b1f6c9a2d4e7b0c5f8a1d3e6b9c2f"`
A public URL that shows the message in a browser without logging in. `{domain}` is your account's custom domain if it has one, and otherwise Envoke's message domain; it is not the API host. Use the URL as returned rather than building it yourself.
---
### `create_time`
- **Type:** String
- **Read-only:** Yes — set automatically on creation.
- **Returned value (read):** `Y-m-d H:i:s` in the account time zone, with no offset.
- **Example:** `"2026-10-06 10:15:42"`
See [Dates and time zones](#dates-and-time-zones).
---
### `modified_time`
- **Type:** String
- **Read-only:** Yes — updated automatically.
- **Returned value (read):** `Y-m-d H:i:s` in the account time zone, with no offset.
- **Example:** `"2026-10-06 11:02:08"`
Changes on every successful update, even one that changes no value, and when a message is sent or cancelled. The list is sorted by this field, newest first, by default.
---
### `name`
- **Type:** String, at least 3 characters
- **Read-only:** No
- **Required on create:** No — if omitted, the message is named `API Message {Y-m-d H:i:s}` from the time of the request.
- **Example:** `"Spring Season Announcement"`
The internal name shown in the Envoke message list; recipients do not see it. Too short: `The field 'name' must be at least 3 characters long`. The API sets no maximum; a longer name is cut to its first 300 characters without an error.
The generated name uses Envoke's server clock, which can differ from the account time zone used by `create_time`.
---
### `group`
- **Type:** String
- **Read-only:** No
- **Required on create:** No
- **Accepted values (write):** The name or ID of an existing message group. Names are matched ignoring case, and a space and an underscore are treated as the same. `""` or `"0"` removes the message from its group.
- **Returned value (read):** The group name, or `""` when the message has no group.
- **Example:** `"Productions"`
Only message groups can be used; template groups and contact groups are not accepted. A group is not created automatically: an unknown value fails with `Invalid 'group': 'Playbills'` and the list of valid groups in `allowed_values` (see [Error Responses](#error-responses)). Create groups with the [Group API](https://envoke-developers.pages.dev/v2/groups/fields/). A message created through this API has no group unless `group` is set.
---
### `send_time`
- **Type:** String
- **Read-only:** No
- **Required on create:** No
- **Accepted values (write):** A future date and time, read as account-local time, e.g. `"2026-10-20 09:00:00"`.
- **Returned value (read):** `Y-m-d H:i:s` in the account time zone, or `""` when no time is set.
- **Example:** `"2026-10-20 09:00:00"`
Setting `send_time` on a draft does not schedule it; the message is scheduled only when it is [sent](https://envoke-developers.pages.dev/v2/messages/sending/). See [Dates and time zones](#dates-and-time-zones) for how the value is read and why it cannot be cleared.
---
### `subject`
- **Type:** String, at least 6 characters
- **Read-only:** No
- **Required on create:** No — but a message cannot be sent without one.
- **Example:** `"Our spring season is here"`
Defaults to `""`. Too short: `The field 'subject' must be at least 6 characters long`. The API sets no maximum; a longer subject is cut to its first 360 characters without an error.
---
### `language`
- **Type:** String
- **Read-only:** No
- **Required on create:** No — defaults to `en`.
- **Accepted values (write):** `en` or `fr` (case-insensitive), or the IDs `1` (English) or `3` (French). `""` or `"0"` sets `en`.
- **Returned value (read):** `"en"` or `"fr"`.
- **Example:** `"fr"`
The language the message is written in. Any other value fails with `Invalid 'language': 'de'` and `allowed_values` `{"1":"en","3":"fr"}`. To choose which contacts receive the message by language, use [`recipient_language`](#recipients).
---
### `publish_archive`
- **Type:** Boolean
- **Read-only:** No
- **Required on create:** No — defaults to `false`.
- **Accepted values (write):** `true`, `"true"`, `1`, `"1"`, `"yes"`, or `"on"` publishes. Anything else is treated as `false`.
- **Returned value (read):** `true` or `false`.
- **Example:** `true`
When `true`, the message is published to your account's public message archive.
---
### `utm_tag_links`
- **Type:** Boolean
- **Read-only:** No
- **Required on create:** No — defaults to your account's link analytics setting.
- **Accepted values (write):** As for `publish_archive`.
- **Returned value (read):** `true` or `false`.
- **Example:** `true`
When `true`, Envoke adds UTM tracking parameters to the links in the message. On create, the default is `true` if link analytics is turned on in your account preferences and `false` otherwise.
---
### `status`
- **Type:** String
- **Read-only:** Yes — changed only by [sending and cancelling](https://envoke-developers.pages.dev/v2/messages/sending/).
- **Returned value (read):** One of the status names below.
- **Example:** `"Draft"`
| Code | Name | How a message gets there | What the API allows |
|---|---|---|---|
| `0` | `Draft` | Created through the API or in Envoke; a cancelled send; a scheduled message that is deleted. | Update, send, delete. |
| `1` | `Scheduled` | [Sent](https://envoke-developers.pages.dev/v2/messages/sending/) through the API or launched in Envoke, before its send time. | Cancel (back to `Draft`), delete (also unschedules it). |
| `2` | `AB test in progress` | An A/B test launched in Envoke. A/B tests cannot be set up through the API. | Cancel, delete. |
| `4` | `Queuing` | The send has started: Envoke is selecting the recipients. | Cancel is accepted but does not stop delivery (see [Cancel](https://envoke-developers.pages.dev/v2/messages/sending/#cancel)); delete. |
| `5` | `Sending` | Recipients are queued and emails are going out. | Cancel is accepted but does not stop delivery. Cannot be deleted. |
| `7` | `Transactional` | A transactional message set up in Envoke. | Delete. |
| `8` | `Nurture` | A message in a nurture campaign (autoresponder). | Delete, unless it is part of a nurture series. |
| `9` | `Sent` | Every email has been handed off for delivery. | Delete. |
- A message in any status other than `Draft` cannot be updated or sent: `Only 'Draft' messages can be updated, this message has the status: 'Sent'`.
- `status` is read-only. Sending it in a request body has no effect.
- Status names are returned exactly as shown. In filters, a name or a code can be used (see [Read](https://envoke-developers.pages.dev/v2/messages/read/#filter-by-status)).
- The list includes messages in every status, including `Nurture` and `Transactional` messages created in Envoke.
---
### `html`, `template_id`, and `replace`
These three write-only fields set the message's content.
#### `html`
- **Type:** String, at most 200 kB (204,800 bytes)
- **Read-only:** Write-only — returned only by `GET /v2/messages/{id}?include_html=1`.
- **Required on create:** One of `html` or `template_id`.
- **Example:** `"Spring Season
Curtain up on {SHOW_TITLE}.
"`
The complete HTML of the message, stored as sent. Envoke does not check or clean it. If it contains the `This weekFri 7:30 p.m. - The Pirates of Penzance
Sat 2:00 p.m. - Matinee
" }
}
```
**Use case 3: Fill in your own HTML.** When your system already produces the HTML, send it with placeholders and the values in the same request:
```json
{
"html": "Dear patron, {SHOW_TITLE} opens on {OPENING_DATE}.
",
"replace": { "{SHOW_TITLE}": "The Pirates of Penzance", "{OPENING_DATE}": "Friday, October 23" }
}
```
Envoke merge tags such as `{@first_name}` are not protected keys. Avoid using them as `replace` keys, or the merge tag is replaced for every recipient.
---
### `sender` and `reply_to`
- **Type:** String
- **Read-only:** No
- **Required on create:** No. `sender` defaults to the account's default sender; `reply_to` has no default.
- **Accepted values (write):** An identity ID, the identity as `Name `, or the email address alone. `""` or `"0"` clears the field.
- **Returned value (read):** `"Name "`, or `null` when no identity is set.
- **Example:** `"Box Office "`
Both fields take one of the sender identities (From addresses) set up in your account; list them with the [Sender API](https://envoke-developers.pages.dev/v1/senders/fields/). A value is resolved in this order:
| Step | Value | Result |
|---|---|---|
| 1 | Digits that match an identity ID, e.g. `"412"` or `412` | That identity. |
| 2 | `Name ` matching an identity, ignoring case and treating a space and an underscore as the same, e.g. `"box office "` | That identity. |
| 3 | `""` or `"0"` | The field is cleared. |
| 4 | A valid email address, e.g. `"boxoffice@example.org"` | The first identity with that address. The address must match exactly, including case. If none matches: `No sender identity found for email: 'tickets@example.org'` (or `No reply_to identity found for email: ...`). |
| 5 | Anything else | `Invalid 'sender': 'Box Office'` (or `Invalid 'reply_to': ...`). |
Errors from steps 4 and 5 include the account's identities in `allowed_values`, keyed by ID, as `Name `.
When `sender` is omitted on create (or sent as `""`), the message uses the identity marked as the account default. On update, `""` clears the sender without falling back to the default, and a message with no sender cannot be sent: `The field 'sender' must be set`. `reply_to` is optional; when it is not set, replies go to the sender address.
---
### Recipients
Three fields choose who a message is sent to, and a fourth narrows the choice by language.
| Field | Accepted values (write) | Returned value (read) |
|---|---|---|
| `recipient_subscription` | The name or ID of a subscription, including your account's mandatory consent subscription. | The subscription name, or `""`. |
| `recipient_tag` | The name or ID of a tag. | The tag name, or `""`. |
| `recipient_segment` | The name or ID of a segment. | The segment name, or `""`. |
| `recipient_language` | `en`, `fr`, `1`, or `3`. `""` or `"0"` clears it. | `"en"`, `"fr"`, or `""` (all languages). |
Names are matched ignoring case, with a space and an underscore treated as the same. An unknown name or ID fails with `Invalid 'recipient_tag': 'Cast List'` (and similarly for the other fields) with the valid values in `allowed_values` when the account has 50 or fewer of them. `""` or `"0"` clears a field. Example values: `"Season News"`, `"Cast Call"`, `"Lapsed Subscribers"`.
Tags and subscriptions can be created with the [Tag API](https://envoke-developers.pages.dev/v2/tags/fields/) and [Subscription API](https://envoke-developers.pages.dev/v2/subscriptions/fields/). Segments cannot be created through any API; create them in Envoke first.
| Rule | Behaviour |
|---|---|
| Tag or segment, not both | `recipient_tag` and `recipient_segment` cannot both be set in one request: `Only one of the fields 'recipient_tag' or 'recipient_segment' may be provided`. |
| Subscription alone | The message goes to the contacts subscribed to it. |
| Tag or segment alone | The message goes to the contacts with the tag, or in the segment. |
| Subscription and a tag or segment | The subscription narrows the tag or segment: the message goes only to contacts who are in the tag or segment **and** subscribed. |
| Mandatory consent | When `recipient_subscription` is your account's mandatory consent subscription, the message is sent as a mandatory (consent-exempt) message, with the subscription's description as the stated reason. |
| `recipient_language` | Only contacts whose language matches are sent the message. When it matches your account's default language, contacts with no language set are included too. |
| Sending | At least one of `recipient_subscription`, `recipient_tag`, or `recipient_segment` must be set to send: `At least one of the fields 'recipient_subscription', 'recipient_tag', or 'recipient_segment' must be set`. |
**On update, the recipient fields are replaced as a set.** If a request includes any one of `recipient_subscription`, `recipient_tag`, or `recipient_segment`, the other two are cleared unless they are in the same request. For example, `{"recipient_tag": "Cast Call"}` on a message sent to the `Season News` subscription leaves it sent to the tag only. To keep a subscription and change the tag, send both. `{"recipient_subscription": ""}` clears all three. `recipient_language` is not affected.
On read, `recipient_tag` is `""` when the message is sent to a subscription alone.
Contacts are selected when the send starts, not when the fields are set, and only those whose consent allows sending are emailed (see [Recipient counts](https://envoke-developers.pages.dev/v2/messages/sending/#recipient-counts)).
---
### Dates and time zones
| Field | On write | On read |
|---|---|---|
| `create_time` | Ignored | `Y-m-d H:i:s` in the account time zone |
| `modified_time` | Ignored | `Y-m-d H:i:s` in the account time zone |
| `send_time` | A future date and time, read as account-local time | `Y-m-d H:i:s` in the account time zone, or `""` |
The account time zone is the one set in your Envoke account preferences, for example `America/Toronto`. Returned times carry no offset or `Z`; they are wall-clock times in that zone. Daylight saving time applies.
**Send `send_time` as account-local time without an offset**, for example `"2026-10-20 09:00:00"` for 9:00 a.m. in the account's zone. An offset or `Z` in the value is dropped, and the remaining wall-clock time is still read in the account time zone: `"2026-10-20T09:00:00Z"` and `"2026-10-20T09:00:00-07:00"` both mean 9:00 a.m. account time, not 9:00 UTC or 9:00 Pacific.
| `send_time` value | Result |
|---|---|
| A future time | Stored, and shown in later responses. |
| A time in the past | `The field 'send_time' must be in the future` |
| A value that is not a date | `Invalid 'send_time': 'next Friday evening'` |
| `""` or `null` | Read as the current time, which fails as not in the future. |
**`send_time` cannot be cleared.** Once a message has a send time, no value returns it to `""`. A send time also survives a cancel: a cancelled message goes back to `Draft` with its old `send_time`, and sending it again without a new `send_time` reuses the stored time (see [Send a draft](https://envoke-developers.pages.dev/v2/messages/sending/#send-a-draft)).
---
## Error Responses
**Validation errors** (missing or invalid fields, rule violations, bad filters):
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'name' must be at least 3 characters long",
"'headline' is not a valid field"
]
}
}
```
When a lookup field (`group`, `sender`, `reply_to`, `language`, `recipient_language`, `recipient_subscription`, `recipient_tag`, `recipient_segment`) has an unknown value, `result_data` also carries `allowed_values`, an object of the valid options keyed by ID. It is included only when the account has 50 or fewer options for that field:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Invalid 'group': 'Playbills'"],
"allowed_values": {
"group": { "31": "Productions", "32": "Newsletters" }
}
}
}
```
**Plain errors.** Some errors use `result_data: []` with the message in `result_text` instead of the validation envelope: `Invalid JSON`, `Invalid request`, the ID mismatch on update, send, and cancel, `Message content error`, the delete errors, and the zero-recipient send result (which carries data; see [Sending](https://envoke-developers.pages.dev/v2/messages/sending/#errors)).
```json
{
"success": false,
"result_data": [],
"result_text": "Item with id '2099' not found"
}
```
**Empty or unreadable body.** `POST /v2/messages`, `PATCH /v2/messages/{id}`, and `POST /v2/messages/send` with no body, an empty body, or `{}` return the validation envelope with `Request body may not be empty`. The send-by-ID and cancel endpoints accept an empty body. A body that is not valid JSON returns the plain envelope with `Invalid JSON`.
All error responses above use HTTP status `400`. An invalid API ID or key returns `401` with the body `{"success":false,"result_data":[],"result_text":"Invalid credentials"}`. 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 `"This account is not active."`) Exceeding your account's API limit returns `429` with `success: false` and a `result_text` naming the limit.
### Not found
**Not-found behaviour varies by method:**
- **GET** `/messages/{id}` — when the ID is not found, returns an empty array (`[]`). Deleted messages and templates are not found.
- **PATCH** `/messages/{id}` — returns a validation error:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["No message found for 'id': 2099"]
}
}
```
- **POST** `/messages/{id}/send` — the same validation error as PATCH.
- **POST** `/messages/{id}/cancel` — returns the validation error for a status that cannot be cancelled, with the status left blank: `Sending cannot be cancelled for a message with 'status': `.
- **DELETE** `/messages/{id}` — returns the plain envelope: `{"success":false,"result_data":[],"result_text":"Item with id '2099' not found"}`.
---
*Full reference, OpenAPI description, and Postman collection: [developers.envoke.com](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/v2/messages/read/
Markdown: https://envoke-developers.pages.dev/v2/messages/read.md
# 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 ",
"reply_to": "Front of House ",
"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 ",
"reply_to": null,
"subject": "",
"language": "en",
"recipient_subscription": "",
"recipient_tag": "",
"recipient_segment": "",
"recipient_language": "",
"publish_archive": false,
"utm_tag_links": false,
"html": "The Pirates of Penzance
Curtain at 7:30 p.m.
"
}
```
---
## 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).*
---
URL: https://envoke-developers.pages.dev/v2/messages/insert/
Markdown: https://envoke-developers.pages.dev/v2/messages/insert.md
# Message API - Insert
Use this endpoint to create a new draft message in your account. To create a message and send it in one request, see [Create and send](https://envoke-developers.pages.dev/v2/messages/sending/#create-and-send).
---
## Endpoint
```
POST https://e1.envoke.com/v2/messages
```
Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be a single JSON object.
---
## Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| `html` | string | One of `html` or `template_id` | The message HTML. Max 200 kB. |
| `template_id` | integer | One of `html` or `template_id` | ID of a template or message to copy the HTML from. Ignored when `html` is also sent. |
| `replace` | object | No | Placeholder text to replace in the HTML before it is saved. |
| `name` | string | No | Internal name. At least 3 characters. |
| `group` | string | No | Name or ID of an existing message group. |
| `subject` | string | No | Subject line. At least 6 characters. Required to send. |
| `sender` | string | No | Identity ID, `Name `, or email address. Defaults to the account's default sender. |
| `reply_to` | string | No | Identity ID, `Name `, or email address. |
| `language` | string | No | `en` or `fr`. Defaults to `en`. |
| `recipient_subscription` | string | No | Name or ID of a subscription. |
| `recipient_tag` | string | No | Name or ID of a tag. Not with `recipient_segment`. |
| `recipient_segment` | string | No | Name or ID of a segment. Not with `recipient_tag`. |
| `recipient_language` | string | No | `en` or `fr`: send only to contacts with that language. |
| `publish_archive` | boolean | No | Publish the message to the web archive. Defaults to `false`. |
| `utm_tag_links` | boolean | No | Add UTM parameters to links. Defaults to your account's link analytics setting. |
| `send_time` | string | No | A future account-local time, e.g. `2026-10-20 09:00:00`. Stored only; it does not schedule the message. |
Only `html` or `template_id` is required. A message can be created without a subject, sender, or recipients; they are checked when it is [sent](https://envoke-developers.pages.dev/v2/messages/sending/#send-a-draft).
`id`, `external_id`, `preview_url`, `create_time`, `modified_time`, and `status` are read-only and silently ignored. Any other key fails with `'headline' is not a valid field`. See the [Fields reference](https://envoke-developers.pages.dev/v2/messages/fields/) for accepted values and the full rules for each field.
### Defaults
| Field | Value when not sent |
|---|---|
| `name` | `API Message {Y-m-d H:i:s}`, from the time of the request in Envoke's server time |
| `status` | `Draft` |
| `group` | `""` (no group) |
| `sender` | The account's default sender identity |
| `reply_to` | Not set (`null`) |
| `subject` | `""` |
| `language` | `en` |
| `recipient_subscription`, `recipient_tag`, `recipient_segment`, `recipient_language` | `""` |
| `publish_archive` | `false` |
| `utm_tag_links` | `true` if link analytics is on in your account preferences, otherwise `false` |
| `send_time` | `""` |
A message created with `send_time` stays a `Draft`. The stored time is used when the message is later sent without a new `send_time`.
---
## Response
On success, the API returns the new message in full, in the same shape as a [read](https://envoke-developers.pages.dev/v2/messages/read/#response):
```json
{
"success": true,
"result_text": "Created",
"result_data": {
"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 10:15:42",
"name": "Spring Season Announcement",
"group": "Productions",
"status": "Draft",
"send_time": "",
"sender": "Box Office ",
"reply_to": "Front of House ",
"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
}
}
```
`result_data.id` is the Envoke message ID, as a string. Store it to update, send, cancel, or delete the message later. The HTML is not included; use `GET /v2/messages/{id}?include_html=1` to read it back.
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"One of the fields 'template_id' or 'html' must be provided in order to create a message"
]
}
}
```
The checks run in the order below. The first six each return on their own, with a single error. The field checks after them are collected and returned together.
| Order | Error | Cause |
|---|---|---|
| 1 | `Request body may not be empty` | No body, an empty body, or `{}`. |
| 2 | `The field 'html' may not be longer than 200kB` | `html` is over 204,800 bytes. |
| 3 | `Nothing found for 'template_id': 1999` | No template or message with that ID is available to your account. |
| 4 | `One of the fields 'template_id' or 'html' must be provided in order to create a message` | Neither field was sent, or `html` is `""`. |
| 5 | `Only one of the fields 'recipient_tag' or 'recipient_segment' may be provided` | Both were sent. |
| 6 | `Invalid 'replace' data` | `replace` is not an object of strings whose keys are wrapped in `{` `}`, or a key targets a preference or consent link. |
| 7 | `'headline' is not a valid field` | The body has a key that is not a message field. |
| 7 | `The field 'name' must be at least 3 characters long` | `name` is too short. |
| 7 | `The field 'subject' must be at least 6 characters long` | `subject` is too short. |
| 7 | `Invalid 'group': 'Playbills'` | No message group has that name or ID. Includes `allowed_values`. |
| 7 | `Invalid 'sender': 'Box Office'` | Not an identity ID, `Name `, or email address. `reply_to` gives `Invalid 'reply_to': ...`. Includes `allowed_values`. |
| 7 | `No sender identity found for email: 'tickets@example.org'` | No identity has that exact address. `reply_to` gives `No reply_to identity found for email: ...`. Includes `allowed_values`. |
| 7 | `Invalid 'language': 'de'` | Not `en`, `fr`, `1`, or `3`. Also for `recipient_language`. Includes `allowed_values`. |
| 7 | `Invalid 'recipient_tag': 'Cast List'` | No tag with that name or ID. Also for `recipient_subscription` and `recipient_segment`. Includes `allowed_values`. |
| 7 | `The field 'send_time' must be in the future` | `send_time` is in the past. |
| 7 | `Invalid 'send_time': 'next Friday evening'` | `send_time` could not be read as a date. |
`allowed_values` lists the valid options for the field, keyed by ID, when your account has 50 or fewer of them (see [Error Responses](https://envoke-developers.pages.dev/v2/messages/fields/#error-responses)).
Two server-side failures use the plain envelope (`result_data: []`) instead: `Message content error` when the content could not be saved, and `insert error: ` followed by the database error when the message could not be saved. A body that is not valid JSON returns `Invalid JSON` in the same shape.
---
## Examples
### Create a message from HTML
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "Auditions Open
Auditions for our spring production start Monday.
"}' \
"https://e1.envoke.com/v2/messages"
```
The message is created as a `Draft` named `API Message 2026-10-06 10:12:05`, with the account's default sender, no subject, and no recipients.
### Create a complete message
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Spring Season Announcement", "group": "Productions", "subject": "Our spring season is here", "sender": "boxoffice@example.org", "reply_to": 413, "language": "en", "recipient_subscription": "Season News", "publish_archive": true, "utm_tag_links": true, "html": "Spring Season
Four shows, one season pass.
"}' \
"https://e1.envoke.com/v2/messages"
```
`sender` is matched by email address and `reply_to` by identity ID; both are returned as `Name ` (see [Response](#response)). The message is ready to [send](https://envoke-developers.pages.dev/v2/messages/sending/#send-a-draft).
### Create a message from a template
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Auditions Reminder", "template_id": 1901, "subject": "Auditions start Monday", "recipient_tag": "Cast Call"}' \
"https://e1.envoke.com/v2/messages"
```
Only the template's HTML is copied. The subject and recipients come from this request.
### Fill in a template's placeholders
The template `Playbill Template` (ID `1901`) contains the placeholders `{SHOW_TITLE}` and `{CURTAIN_TIME}`:
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Pirates of Penzance - Opening Night", "template_id": 1901, "subject": "Opening night is almost here", "replace": {"{SHOW_TITLE}": "The Pirates of Penzance", "{CURTAIN_TIME}": "7:30 p.m."}}' \
"https://e1.envoke.com/v2/messages"
```
The message HTML is saved with the placeholders already replaced. The template itself is unchanged.
### Fill in your own HTML
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Opening Date Notice", "html": "Dear patron, {SHOW_TITLE} opens on {OPENING_DATE}.
", "replace": {"{SHOW_TITLE}": "The Pirates of Penzance", "{OPENING_DATE}": "Friday, October 23"}}' \
"https://e1.envoke.com/v2/messages"
```
### Send to a subscription, narrowed by a tag
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Cast Party Invitation", "subject": "You are invited to the cast party", "recipient_subscription": "Season News", "recipient_tag": "Cast Call", "html": "Join us after the final show.
"}' \
"https://e1.envoke.com/v2/messages"
```
When sent, the message goes only to contacts who have the `Cast Call` tag and are subscribed to `Season News`.
### Create a draft with a send time
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Subscriber Presale", "subject": "Presale opens Tuesday at 9", "recipient_subscription": "Season News", "send_time": "2026-10-20 09:00:00", "html": "Subscribers get first pick of seats.
"}' \
"https://e1.envoke.com/v2/messages"
```
The message is a `Draft` with `send_time` `"2026-10-20 09:00:00"` (9:00 a.m. in the account time zone). It is not scheduled until it is [sent](https://envoke-developers.pages.dev/v2/messages/sending/#send-a-draft).
### Missing content
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Spring Season Announcement"}' \
"https://e1.envoke.com/v2/messages"
```
Returns the validation error shown under [Validation Errors](#validation-errors).
### Tag and segment together
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "Hello
", "recipient_tag": "Cast Call", "recipient_segment": "Lapsed Subscribers"}' \
"https://e1.envoke.com/v2/messages"
```
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Only one of the fields 'recipient_tag' or 'recipient_segment' may be provided"]
}
}
```
### Unknown group
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "Hello
", "group": "Playbills"}' \
"https://e1.envoke.com/v2/messages"
```
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Invalid 'group': 'Playbills'"],
"allowed_values": {
"group": { "31": "Productions", "32": "Newsletters" }
}
}
}
```
No message is created. Create the group first with the [Group API](https://envoke-developers.pages.dev/v2/groups/fields/), or use one of the listed names or IDs.
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/messages/update/
Markdown: https://envoke-developers.pages.dev/v2/messages/update.md
# Message API - Update
Use this endpoint to change a draft message: its settings, its recipients, or its HTML.
---
## Endpoint
```
PATCH https://e1.envoke.com/v2/messages/{ID}
```
Replace `{ID}` with the Envoke message ID. Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be a single JSON object. There is no endpoint for updating several messages in one request.
---
## Request Body
Send only the fields you want to change. Fields you leave out keep their current values, with one exception: the recipient fields are replaced as a set (see [Recipients](#recipients)).
| Field | Type | Description |
|---|---|---|
| `name` | string | Internal name. At least 3 characters. |
| `group` | string | Name or ID of an existing message group. `""` removes the group. |
| `subject` | string | Subject line. At least 6 characters. |
| `sender` | string | Identity ID, `Name `, or email address. `""` clears it. |
| `reply_to` | string | Identity ID, `Name `, or email address. `""` clears it. |
| `language` | string | `en` or `fr`. `""` sets `en`. |
| `recipient_subscription` | string | Name or ID of a subscription. `""` clears it. |
| `recipient_tag` | string | Name or ID of a tag. Not with `recipient_segment`. `""` clears it. |
| `recipient_segment` | string | Name or ID of a segment. Not with `recipient_tag`. `""` clears it. |
| `recipient_language` | string | `en` or `fr`. `""` clears it (all languages). |
| `publish_archive` | boolean | Publish the message to the web archive. |
| `utm_tag_links` | boolean | Add UTM parameters to links. |
| `send_time` | string | A future account-local time. Stored only; it does not schedule the message. |
| `html` | string | New HTML for the message, replacing all of its content. Max 200 kB. |
| `replace` | object | Placeholder text to replace in the `html` sent in the same request. |
| `id` | string or integer | Optional. If sent, it must match `{ID}` (see [ID Mismatch](#id-mismatch)). |
`external_id`, `preview_url`, `create_time`, `modified_time`, and `status` are read-only and silently ignored. A message object returned by a GET can be edited and sent back only after removing a `null` `sender` or `reply_to`, an empty `send_time`, and an empty `subject`, which would otherwise fail validation (see the [Fields reference](https://envoke-developers.pages.dev/v2/messages/fields/#field-details)). `template_id` cannot be used on update. Any other key fails with `'headline' is not a valid field`. See the [Fields reference](https://envoke-developers.pages.dev/v2/messages/fields/) for accepted values and the full rules for each field.
Every successful update sets `modified_time`, even when no value changed.
---
## Only drafts can be updated
A message can be updated only while its status is `Draft`. Any other status fails with:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Only 'Draft' messages can be updated, this message has the status: 'Scheduled'"]
}
}
```
To change a scheduled message, [cancel](https://envoke-developers.pages.dev/v2/messages/sending/#cancel) it first, which returns it to `Draft`, then update it and send it again. Sent, nurture, and transactional messages cannot be changed through the API.
---
## Recipients
`recipient_subscription`, `recipient_tag`, and `recipient_segment` are treated as one setting. **When a request includes any one of them, the other two are cleared unless they are also in the request.**
| Current recipients | Request | Result |
|---|---|---|
| Subscription `Season News` | `{"recipient_tag": "Cast Call"}` | Tag `Cast Call` only; the subscription is removed. |
| Subscription `Season News` | `{"recipient_subscription": "Season News", "recipient_tag": "Cast Call"}` | Contacts with `Cast Call` who are subscribed to `Season News`. |
| Subscription and tag | `{"recipient_subscription": ""}` | No recipients. |
| Tag `Cast Call` | `{"recipient_segment": "Lapsed Subscribers"}` | Segment `Lapsed Subscribers` only. |
| Any | `{"subject": "New subject line"}` | Unchanged. |
`recipient_tag` and `recipient_segment` cannot both be set: `Only one of the fields 'recipient_tag' or 'recipient_segment' may be provided`. `recipient_language` is a separate setting and is never cleared by this rule. See [Recipients](https://envoke-developers.pages.dev/v2/messages/fields/#recipients) for how the fields combine when the message is sent.
---
## Replacing the HTML
Sending `html` replaces the whole content of the message. There is no way to change part of the HTML in place; read the current HTML with `GET /v2/messages/{ID}?include_html=1`, change it, and send it back.
`replace` works on update only together with `html` in the same request. The replacements are made in the HTML you send, and the result is saved. Without `html`, the request fails with `The field 'html' is required for content 'replace' data`; a replacement cannot be run over the HTML the message already has.
```json
{
"html": "{SHOW_TITLE}
Curtain at {CURTAIN_TIME}.
",
"replace": { "{SHOW_TITLE}": "The Pirates of Penzance", "{CURTAIN_TIME}": "8:00 p.m." }
}
```
`template_id` is not accepted: `The field 'template_id' is not valid for updating a message`. To switch a draft to a template's HTML, read the template with the [Template API](https://envoke-developers.pages.dev/v2/templates/read/) and send its HTML as `html`.
---
## Clearing Values
| Field | To clear | Notes |
|---|---|---|
| `group` | `""` or `"0"` | The message has no group. |
| `sender` | `""` or `"0"` | No fallback to the default sender. The message cannot be sent until a sender is set again. |
| `reply_to` | `""` or `"0"` | |
| `recipient_subscription`, `recipient_tag`, `recipient_segment` | `""` or `"0"` | Clearing one clears all three (see [Recipients](#recipients)). |
| `recipient_language` | `""` or `"0"` | Sends to all languages. |
| `language` | — | `""` or `"0"` sets `en`. |
| `name`, `subject` | — | Cannot be cleared; `""` fails the minimum-length check. |
| `html` | — | Cannot be cleared; `""` is ignored. |
| `send_time` | — | Cannot be cleared once set (see [Dates and time zones](https://envoke-developers.pages.dev/v2/messages/fields/#dates-and-time-zones)). |
Use the string `"0"`, not the number `0`: a JSON number `0` fails as an invalid value, e.g. `Invalid 'group': '0'`.
---
## ID Mismatch
If the body includes `id` and it differs from `{ID}`, the request fails with a plain envelope, not the validation envelope:
```json
{
"success": false,
"result_data": [],
"result_text": "Provided 'id': 2001 does not match 'id' in request body (2002)"
}
```
`"2001"` and `2001` count as the same. The same rule and message apply to `POST /v2/messages/{ID}/send` and `POST /v2/messages/{ID}/cancel`.
---
## Response
On success, the API returns the updated message in full, in the same shape as a [read](https://envoke-developers.pages.dev/v2/messages/read/#response):
```json
{
"success": true,
"result_text": "Updated",
"result_data": {
"id": "2002",
"name": "Auditions Open",
"status": "Draft",
"subject": "Auditions start Monday at 7",
"recipient_subscription": "",
"recipient_tag": "Cast Call",
"recipient_segment": "",
"modified_time": "2026-10-06 11:30:17"
}
}
```
*(`result_data` shortened; the response contains every field.)*
`result_data.id` is a string. The HTML is not included.
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"No message found for 'id': 2099"
]
}
}
```
The checks run in the order below. Each of the first six returns on its own; the field checks are then collected and returned together, before the `replace` checks.
| Error | Cause |
|---|---|
| `Request body may not be empty` | No body, an empty body, or `{}`. |
| `No message found for 'id': 2099` | No message with that ID in your account. Deleted messages and templates are not found. |
| `Only 'Draft' messages can be updated, this message has the status: 'Sent'` | The message is not a draft. |
| `The field 'template_id' is not valid for updating a message` | `template_id` was sent. |
| `The field 'html' may not be longer than 200kB` | `html` is over 204,800 bytes. |
| `Only one of the fields 'recipient_tag' or 'recipient_segment' may be provided` | Both were sent with values. |
| Field errors | `'headline' is not a valid field`, the `name` and `subject` length errors, unknown lookup values with `allowed_values`, and the `send_time` errors, as listed for [create](https://envoke-developers.pages.dev/v2/messages/insert/#validation-errors). |
| `The field 'html' is required for content 'replace' data` | `replace` was sent without `html`. |
| `Invalid 'replace' data` | `replace` is not an object of strings with `{...}` keys, or a key targets a preference or consent link. |
`Invalid JSON` and the [ID mismatch](#id-mismatch) use the plain envelope with `result_data: []`.
---
## Examples
### Change the subject and HTML
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject": "Auditions start Monday at 7", "html": "Auditions Open
Monday at 7 p.m. in the rehearsal hall.
"}' \
"https://e1.envoke.com/v2/messages/2002"
```
### Send to a tag instead of a subscription
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"recipient_tag": "Cast Call"}' \
"https://e1.envoke.com/v2/messages/2002"
```
If the message was going to the `Season News` subscription, it now goes to the `Cast Call` tag only.
### Clear the reply-to address
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"reply_to": ""}' \
"https://e1.envoke.com/v2/messages/2001"
```
`reply_to` is then returned as `null`.
### Set a send time
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"send_time": "2026-10-20 09:00:00"}' \
"https://e1.envoke.com/v2/messages/2002"
```
The message stays a `Draft`. The time is used when it is [sent](https://envoke-developers.pages.dev/v2/messages/sending/#send-a-draft).
### New HTML with placeholders
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "{SHOW_TITLE}
Curtain at {CURTAIN_TIME}.
", "replace": {"{SHOW_TITLE}": "The Pirates of Penzance", "{CURTAIN_TIME}": "8:00 p.m."}}' \
"https://e1.envoke.com/v2/messages/2003"
```
### Update a message that is not a draft
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject": "Last chance for presale seats"}' \
"https://e1.envoke.com/v2/messages/2001"
```
Message `2001` is `Scheduled`, so the request fails with `Only 'Draft' messages can be updated, this message has the status: 'Scheduled'` (see [Only drafts can be updated](#only-drafts-can-be-updated)).
### Use `template_id` on update
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"template_id": 1901}' \
"https://e1.envoke.com/v2/messages/2002"
```
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["The field 'template_id' is not valid for updating a message"]
}
}
```
### Message not found
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject": "Auditions start Monday at 7"}' \
"https://e1.envoke.com/v2/messages/2099"
```
Returns the validation error shown under [Validation Errors](#validation-errors).
---
*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).*
---
URL: https://envoke-developers.pages.dev/v2/messages/delete/
Markdown: https://envoke-developers.pages.dev/v2/messages/delete.md
# Message API - Delete
Use this endpoint to delete a message from your account.
---
## Endpoint
```
DELETE https://e1.envoke.com/v2/messages/{ID}
```
Replace `{ID}` with the Envoke message ID you want to delete. Authentication is via HTTP Basic Auth using your API ID and API Key. No request body is needed.
---
## Important
**Deletion moves the message to the Deleted folder.** The message disappears from the API at once: a GET for it returns `[]`, and it is no longer listed. In Envoke, it appears in the **Deleted** folder of the message list, for 60 days, where it can be restored; after that Envoke's scheduled cleanup removes it permanently. Restoring is possible only in Envoke; the API has no restore endpoint.
**A scheduled message is unscheduled.** Deleting a `Scheduled` message sets it back to `Draft` as well, so it is not sent. There is no need to [cancel](https://envoke-developers.pages.dev/v2/messages/sending/#cancel) it first.
**Messages in most statuses can be deleted**, including `Sent` messages. These cannot:
| Message | Why |
|---|---|
| Status `Sending` | Emails are going out. Wait until the message is `Sent`. |
| Part of a nurture series | Remove it from the nurture campaign in Envoke first. |
| Envoke system or offer messages | These are managed by Envoke. |
---
## Response
### Success
```json
{
"success": true,
"result_data": [],
"result_text": "Deleted"
}
```
### Message not found
Returned with HTTP status `400` when the ID does not match a message in your account. This uses the plain envelope, not the validation envelope:
```json
{
"success": false,
"result_data": [],
"result_text": "Item with id '2099' not found"
}
```
### Blocked messages
Returned with HTTP status `400`, in the same plain envelope, when the message cannot be deleted:
| `result_text` | Cause |
|---|---|
| `Message: 'Spring Season Announcement' is currently sending` | The message's status is `Sending`. |
| `This message cannot be deleted because it's part of the automated series: "Welcome Sequence"` | A `Nurture` message that belongs to a nurture campaign. |
| `System messages cannot be deleted` or `Offer messages cannot be deleted` | A message Envoke manages. |
### Database error
If the delete fails on the server, `result_text` is `Delete error: ` followed by the error detail, with HTTP status `400`. Retry the request; if it keeps failing, contact Envoke support.
---
## Example
```bash
curl -X DELETE -gs \
-u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/messages/2009"
```
Replace `2009` with the ID of the message you want to delete, and substitute your actual API ID and API Key.
A `GET https://e1.envoke.com/v2/messages/2009` afterwards returns `[]`.
To find the ID of a message from its name first:
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/messages?name=Auditions%20Open&fields=id&limit=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).*
---
URL: https://envoke-developers.pages.dev/v2/messages/sending/
Markdown: https://envoke-developers.pages.dev/v2/messages/sending.md
# Message API - Sending
Use these endpoints to send a draft message, to create and send a message in one request, and to cancel a scheduled send.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| POST | `https://e1.envoke.com/v2/messages/{ID}/send` | Send a draft message, now or at its send time. |
| POST | `https://e1.envoke.com/v2/messages/send` | Create a message and send it in one request. |
| POST | `https://e1.envoke.com/v2/messages/{ID}/cancel` | Cancel a scheduled message and return it to `Draft`. |
Authentication is via HTTP Basic Auth using your API ID and API Key. Request bodies are JSON objects.
---
## How sending works
Sending is asynchronous. A send request checks the message, estimates its recipients, and schedules it; it does not deliver any email itself. Envoke's sending process then picks up the message at its send time and moves it through these statuses:
| Status | What is happening |
|---|---|
| `Draft` | The message can be edited. Send it with one of the endpoints above. |
| `Scheduled` | The send request succeeded. The message waits for its send time. It can be [cancelled](#cancel). |
| `Queuing` | The send time has passed and Envoke is selecting the recipients. |
| `Sending` | The recipients are queued and the emails are going out. |
| `Sent` | Every email has been handed off for delivery. |
- The response to a send request is always `Message scheduled for send`, even when the message is sent straight away.
- The recipient counts in the response are an **estimate** taken at the time of the request. The actual recipient list is selected again when the send starts, with the same consent and address checks, so contacts added to or removed from the tag, subscription, or segment in the meantime change who is emailed.
- To follow a send, read the message with `GET /v2/messages/{ID}` and check `status`.
---
## Send a draft
```
POST https://e1.envoke.com/v2/messages/{ID}/send
```
**Request.** The body is optional. It can contain any field accepted by [update](https://envoke-developers.pages.dev/v2/messages/update/#request-body), most often `send_time`; the fields are saved to the message first, exactly as a `PATCH` would, and the message is then sent. Fields saved this way stay saved even if the send then fails. If the body includes `id`, it must match `{ID}` (see [ID Mismatch](https://envoke-developers.pages.dev/v2/messages/update/#id-mismatch)).
**Requirements.** The message must be a `Draft`, and after any update in the body it must have:
| Requirement | Error when missing |
|---|---|
| At least one recipient field | `At least one of the fields 'recipient_subscription', 'recipient_tag', or 'recipient_segment' must be set` |
| A subject | `The field 'subject' must be set` |
| A sender | `The field 'sender' must be set` |
The three errors are returned together. `name` is not required.
**Send time.**
| Situation | When the message is sent |
|---|---|
| The body has `send_time` | At that time. It must be in the future and is read as account-local time (see [Dates and time zones](https://envoke-developers.pages.dev/v2/messages/fields/#dates-and-time-zones)). |
| No `send_time` in the body, and the message has none | Immediately: on the next run of Envoke's sending process. |
| No `send_time` in the body, and the message already has one | At the stored time. It is not checked again: if it has already passed, the message is sent on the next run. |
A message keeps its `send_time` when it is cancelled. Sending it again with no `send_time` therefore reuses the old time, which may now be in the past and send the message at once. Send a new `send_time` when you re-send a cancelled message. A stored `send_time` cannot be cleared, so a message that has one cannot be switched back to "send now" except by sending a new, near-future time.
**Response.**
```json
{
"success": true,
"result_text": "Message scheduled for send",
"result_data": {
"id": "2001",
"send_time": "2026-10-20 09:00:00",
"recipients": {
"total_selected": 412,
"emails_to_be_sent": 377,
"excluded_unsubscribed_contacts": 29,
"excluded_invalid_emails": 2,
"excluded_bounced_emails": 4,
"excluded_unverified_emails": 0,
"excluded_by_language": 0
}
}
}
```
`result_data` is not the message object: it holds the message `id` (a string, as in the URL), the `send_time` in the account time zone, and the [recipient counts](#recipient-counts). For an immediate send, `send_time` is the time of the request.
**Credits.** For a prepaid account, the send is refused when the account's credit balance is lower than `emails_to_be_sent`: `Email can't be sent, the account is out of credits`. A monthly-billed account that is not yet on a paid plan, and has too few credits, gets `Email can't be sent, the account has not upgraded`. The message stays a `Draft`.
**No recipients.** When `emails_to_be_sent` is `0`, the message is not scheduled and stays a `Draft`. The response is HTTP `400` with the counts, so you can see why, and no `errors` key:
```json
{
"success": false,
"result_text": "Email can't be sent, no contacts selected",
"result_data": {
"id": "2004",
"recipients": {
"total_selected": 0,
"emails_to_be_sent": 0,
"excluded_unsubscribed_contacts": 0,
"excluded_invalid_emails": 0,
"excluded_bounced_emails": 0,
"excluded_unverified_emails": 0,
"excluded_by_language": 0
}
}
}
```
---
## Create and send
```
POST https://e1.envoke.com/v2/messages/send
```
**Request.** A full [create](https://envoke-developers.pages.dev/v2/messages/insert/#request-body) body (required). The message is created as a draft and then sent as described in [Send a draft](#send-a-draft), in one request. For a scheduled send, include `send_time` in the body; without it, the message is sent immediately. The body must therefore include content, a subject, and recipients; `sender` falls back to the account default.
**Response.** On success, the same shape as [Send a draft](#send-a-draft), except that `result_data.id` is an **integer**:
```json
{
"success": true,
"result_text": "Message scheduled for send",
"result_data": {
"id": 2005,
"send_time": "2026-10-06 14:05:31",
"recipients": { "total_selected": 2, "emails_to_be_sent": 2, "excluded_unsubscribed_contacts": 0, "excluded_invalid_emails": 0, "excluded_bounced_emails": 0, "excluded_unverified_emails": 0, "excluded_by_language": 0 }
}
}
```
**When it fails.** If the create step fails (for example, no `html` or `template_id`), nothing is created and the create error is returned. If the create step succeeds but the send step fails (for example, no subject, or out of credits), **the draft created by the first step is kept, and its ID is not returned**; the response is the send error only. The zero-recipient response is the one exception: its `result_data.id` is the new draft's ID. Give each message a unique `name` so you can find a leftover draft with `GET /v2/messages?name=...` and fix and send it, or delete it.
---
## Recipient counts
Both send endpoints return these seven counts in `result_data.recipients`. All are integers.
| Key | Meaning |
|---|---|
| `total_selected` | Every contact in the tag, subscription, or segment (after a subscription narrows a tag or segment). |
| `emails_to_be_sent` | Contacts who pass every check below. The estimated number of emails. |
| `excluded_unsubscribed_contacts` | Contacts whose consent does not allow sending, such as `Not Provided`, `Revoked`, or `Expired`. On a mandatory consent message, only contacts whose consent blocks all email (such as a spam report) are counted here. |
| `excluded_invalid_emails` | Contacts whose email address is not valid. |
| `excluded_bounced_emails` | Contacts whose address has been marked undeliverable. |
| `excluded_unverified_emails` | Contacts who have not confirmed their address. Only when your account requires email confirmation (double opt-in); otherwise `0`. |
| `excluded_by_language` | Contacts whose language does not match `recipient_language`. |
The counts overlap: one contact can be excluded for several reasons and is counted under each. The exclusions therefore need not add up to `total_selected` minus `emails_to_be_sent`.
---
## Cancel
```
POST https://e1.envoke.com/v2/messages/{ID}/cancel
```
**Request.** The body is optional. If it contains update fields, they are applied after the message is returned to `Draft`, as a `PATCH` would. If the body includes `id`, it must match `{ID}`.
**Effect.** A `Scheduled` message goes back to `Draft` and will not be sent. It can then be updated and sent again.
- **`send_time` is kept.** The cancelled draft still shows its old `send_time`. See [Send time](#send-a-draft) before sending it again.
- **Only cancel scheduled messages.** The API also accepts cancel for `Queuing`, `Sending`, and `AB test in progress` messages and sets them to `Draft`, but emails already queued are still delivered.
- If an update field in the body fails validation, the message has already been returned to `Draft`, but the response is the validation error.
**Response.** The message in full, in the same shape as a [read](https://envoke-developers.pages.dev/v2/messages/read/#response):
```json
{
"success": true,
"result_text": "Message reverted to draft status",
"result_data": {
"id": "2001",
"name": "Spring Season Announcement",
"status": "Draft",
"send_time": "2026-10-20 09:00:00"
}
}
```
*(`result_data` shortened; the response contains every field.)*
**Wrong status.** Cancelling a message that is not scheduled or sending fails with the status name (not quoted):
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Sending cannot be cancelled for a message with 'status': Draft"]
}
}
```
---
## Errors
All errors use HTTP status `400`.
| Error | Endpoints | Envelope | Cause |
|---|---|---|---|
| `Request body may not be empty` | `/send` | Validation | No body, an empty body, or `{}`. The other two endpoints accept an empty body. |
| `Invalid JSON` | All | Plain | The body is not valid JSON. |
| `Invalid request` | All | Plain | The request could not be processed. |
| `Provided 'id': 2001 does not match 'id' in request body (2002)` | `/{ID}/send`, `/{ID}/cancel` | Plain | The body `id` differs from the URL. |
| Create errors | `/send` | Validation | Any error listed for [create](https://envoke-developers.pages.dev/v2/messages/insert/#validation-errors). Nothing is created. |
| `No message found for 'id': 2099` | `/{ID}/send` | Validation | No message with that ID in your account. |
| `Only 'Draft' messages can be updated, this message has the status: 'Scheduled'` | `/{ID}/send` | Validation | The message is not a draft. |
| Update errors | `/{ID}/send`, `/{ID}/cancel` | Validation | Any error listed for [update](https://envoke-developers.pages.dev/v2/messages/update/#validation-errors), for fields sent in the body. |
| `At least one of the fields 'recipient_subscription', 'recipient_tag', or 'recipient_segment' must be set` | `/send`, `/{ID}/send` | Validation | No recipients. |
| `The field 'subject' must be set` | `/send`, `/{ID}/send` | Validation | No subject. |
| `The field 'sender' must be set` | `/send`, `/{ID}/send` | Validation | No sender. |
| `Email can't be sent, no contacts selected` | `/send`, `/{ID}/send` | Plain, with counts | `emails_to_be_sent` is `0`. See [No recipients](#send-a-draft). |
| `Email can't be sent, the account is out of credits` | `/send`, `/{ID}/send` | Validation | Not enough prepaid credits. |
| `Email can't be sent, the account has not upgraded` | `/send`, `/{ID}/send` | Validation | Monthly-billed account not yet on a paid plan. |
| `Sending cannot be cancelled for a message with 'status': Draft` | `/{ID}/cancel` | Validation | The message is not scheduled or sending. |
On `/send`, errors after the create step leave the new draft in place (see [Create and send](#create-and-send)).
---
## Examples
### Send a draft now
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/messages/2002/send"
```
Message `2002` has a subject, sender, and recipients and no `send_time`, so it is sent on the next run of Envoke's sending process. The response's `send_time` is the time of the request.
### Schedule a send
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"send_time": "2026-10-20 09:00:00"}' \
"https://e1.envoke.com/v2/messages/2001/send"
```
Sends at 9:00 a.m. on October 20 in the account time zone (for an account in `America/Toronto`, 9:00 a.m. Toronto time). Do not add an offset or `Z`; it would be ignored. The response is shown under [Send a draft](#send-a-draft).
### Create from a template and send
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Pirates of Penzance - Cast Notice", "template_id": 1901, "replace": {"{SHOW_TITLE}": "The Pirates of Penzance", "{CURTAIN_TIME}": "7:30 p.m."}, "subject": "Call times for opening night", "sender": "Box Office ", "recipient_tag": "Cast Call"}' \
"https://e1.envoke.com/v2/messages/send"
```
Creates the message from the `Playbill Template`, fills in its placeholders, and sends it immediately to contacts with the `Cast Call` tag. `result_data.id` in the response is an integer (see [Create and send](#create-and-send)).
### Cancel a scheduled send
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/messages/2001/cancel"
```
Message `2001` returns to `Draft` with its `send_time` of `2026-10-20 09:00:00` still set.
### Send to an audience with no recipients
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"recipient_tag": "Understudies"}' \
"https://e1.envoke.com/v2/messages/2004/send"
```
No contact has the `Understudies` tag, so the request fails with `Email can't be sent, no contacts selected` and the counts shown under [No recipients](#send-a-draft). The message stays a `Draft`, but its recipients have been changed to the `Understudies` tag by the body.
### Send a message that is not a draft
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/messages/2001/send"
```
Message `2001` is already `Scheduled`:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Only 'Draft' messages can be updated, this message has the status: 'Scheduled'"]
}
}
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v1/send/fields/
Markdown: https://envoke-developers.pages.dev/v1/send/fields.md
# Send API - Fields Reference
This article describes all fields accepted by the Send API. Use this as a reference when sending a message to a list of recipients.
---
## Overview
The Send API sends one email message to a list of people in a single request. Each request creates one message and sends it immediately to up to 1,000 recipients. There is no draft, schedule, or review step: when the request succeeds, the emails are already queued for delivery.
Recipients are given either by email address ([`recipients`](#recipients)) or by Envoke contact ID ([`recipient_contact_ids`](#recipients)). An email address that matches no contact in your account becomes a new contact and is sent the message (see [Consent](#consent)). To send to more than 1,000 people, send the first batch, then add further batches of up to 1,000 to the same message with [`id`](#id) (see [Send to more recipients](https://envoke-developers.pages.dev/v1/send/send/#send-to-more-recipients-with-an-existing-message)).
**The Transactional API preference must be set first.** In Envoke, go to **Account settings → Integrations → Transactional API** and choose a **Consent status** and enter a **Consent description**. They are the consent given to contacts the API creates. Until a consent status is set, every request fails with `A default consent status and description is required in order to use this API`. The section is shown once the API is enabled for your account, to users who can manage the API.
The Send API answers on `/v1` only and has a single endpoint, described in [Send](https://envoke-developers.pages.dev/v1/send/send/). It has no read, update, or delete endpoints of its own: a sent message can be read, and deleted once it has finished sending, through the [Message API](https://envoke-developers.pages.dev/v2/messages/fields/). Sender identities are managed with the [Sender API](https://envoke-developers.pages.dev/v1/senders/fields/), message groups with the [Group API](https://envoke-developers.pages.dev/v2/groups/fields/), contacts with the [Contact API](https://envoke-developers.pages.dev/v1/contacts/fields/), and templates whose HTML a message can copy with the [Template API](https://envoke-developers.pages.dev/v2/templates/fields/).
All requests require [HTTP Basic Auth](https://envoke-developers.pages.dev/guides/authentication/) using your API ID and API Key.
---
## Fields
| Field | Type | Read-only | Required on Create | Description |
|---|---|---|---|---|
| `id` | integer | No | No | ID of an earlier Send API message, to send it to more recipients. When set, only `recipients` may also be sent. |
| `name` | string | No | No | Internal name. Generated if omitted. |
| `subject` | string | No | Yes | Subject line, at least 6 characters. |
| `group` | string | No | No | Message group (folder) by name or ID. Defaults to `API Messages`. |
| `sender` | string | No | No | Sender identity. Defaults to the account's default sender. |
| `reply_to` | string | No | No | Reply-to identity. No default. |
| `language` | string | No | No | Language of the message, `en` or `fr`. Defaults to `en`. |
| `recipients` | array | No | One of `recipients` or `recipient_contact_ids` | Email addresses, or objects with `email` and optional `replace`. At most 1,000. |
| `recipient_contact_ids` | array | No | One of `recipients` or `recipient_contact_ids` | Contact IDs, or objects with `id` and optional `replace`. At most 1,000. |
| `html` | string | No | One of `html` or `template_id` | The message HTML, at most 200 kB. |
| `template_id` | integer | No | One of `html` or `template_id` | ID of a template or message to copy HTML from. |
| `is_mandatory` | boolean | No | No | Send as a mandatory consent message. Defaults to `false`. |
| `track_recipient_activity` | boolean | No | No | Track opens and clicks. Defaults to `false`. |
| `enable_merge_fields` | boolean | No | No | Replace Envoke merge fields. Defaults to `false`. |
| `publish_archive` | boolean | No | No | Publish the message to the account's web archive. Defaults to `false`. |
---
## Field Details
Every field is write-only: the response returns the message `id`, counts, and one entry per recipient (see [Response](https://envoke-developers.pages.dev/v1/send/send/#response)), not the fields sent. To see the stored message, read it with the [Message API](https://envoke-developers.pages.dev/v2/messages/read/). Any key not listed above fails with `'{field}' is not a valid field` (see [Not supported](#not-supported)).
### `id`
- **Type:** Integer
- **Read-only:** No
- **Required on create:** No — omit it to create a new message.
- **Accepted values (write):** The ID of a message created by the Send API. `0`, `""`, and `null` count as not sent.
- **Returned value:** `result_data.id` in the response. For a new message it is an integer. When you send `id`, the response echoes your value with its JSON type unchanged: `3006` comes back as `3006`, `"3006"` as `"3006"`.
- **Example:** `3006`
When `id` is set, the request adds recipients to that message instead of creating one, and only `recipients` may be sent with it. The message's content and settings are reused. See [Send to more recipients](https://envoke-developers.pages.dev/v1/send/send/#send-to-more-recipients-with-an-existing-message) for the rules and errors.
In the Message API, the same message has `id` `"3006"` as a string.
---
### `name`
- **Type:** String, at least 3 characters
- **Read-only:** No
- **Required on create:** No — if omitted, the message is named `API Message {Y-m-d H:i:s}` from the time of the request.
- **Example:** `"Head of the Valley - Crew Call-up"`
The internal name shown in the Envoke message list and in the Message API; recipients do not see it. Too short: `The field 'name' must be at least 3 characters long`. The API sets no maximum; a longer name is cut to its first 300 characters without an error.
The generated name uses Envoke's server clock, which can differ from your account time zone.
---
### `subject`
- **Type:** String, at least 6 characters
- **Read-only:** No
- **Required on create:** Yes
- **Example:** `"Crew call-up: Head of the Valley"`
Missing or empty: `The field 'subject' must be set`. Too short: `The field 'subject' must be at least 6 characters long`. An empty string `""` returns both errors. The API sets no maximum; a longer subject is cut to its first 360 characters without an error.
The subject is required even when `template_id` is used, because a template's subject is not copied. Per-recipient [`replace`](#replace) does not change the subject.
---
### `language`
- **Type:** String
- **Read-only:** No
- **Required on create:** No — defaults to `en`.
- **Accepted values (write):** `en` or `fr` (case-insensitive), or the IDs `1` (English) or `3` (French). `""` or `"0"` sets `en`.
- **Example:** `"fr"`
The language the message is written in. Envoke uses it for the text it adds, such as the unsubscribe footer and merge fields. It does not choose recipients: every recipient is sent the message whatever their language. Any other value fails with `Invalid 'language': 'de'` and `allowed_values` `{"1":"en","3":"fr"}`.
The default is English, not your account's default language.
---
### `publish_archive`
- **Type:** Boolean
- **Read-only:** No
- **Required on create:** No — defaults to `false`.
- **Accepted values (write):** `true`, `"true"`, `1`, `"1"`, `"yes"`, or `"on"` publishes. Anything else is treated as `false`.
- **Example:** `true`
When `true`, the message is published to your account's public message archive.
---
### `is_mandatory`
- **Type:** Boolean
- **Read-only:** No
- **Required on create:** No — defaults to `false`.
- **Accepted values (write):** As for `publish_archive`. Anything not read as true is `false`, with no error.
- **Example:** `true`
When `true`, the message is sent as a mandatory consent message and only reaches contacts who hold your account's mandatory consent subscription. See [Consent](#consent).
---
### Recipients
Two fields say who the message is sent to. Send one of them.
| Field | Simple form | Object form |
|---|---|---|
| `recipients` | `["stroke.seat@example.org", "bow.seat@example.org"]` | `[{ "email": "stroke.seat@example.org", "replace": { "{SEAT}": "8" } }]` |
| `recipient_contact_ids` | `[7001, 7002]` (IDs may also be numeric strings) | `[{ "id": 7001, "replace": { "{SEAT}": "8" } }]` |
Use the object form when each recipient needs their own [`replace`](#replace) values. An object has the key field (`email` or `id`) and, optionally, `replace`. Any other key, such as `first_name`, is ignored: contact details cannot be set through this API (use the [Contact API](https://envoke-developers.pages.dev/v1/contacts/fields/)).
| Rule | Behaviour |
|---|---|
| One form per list | Only the first item decides the form. If it is a string or number, every item is read as an address or ID. If it is an object, every item must be an object. Use the same form for the whole list. |
| First item | An object first item without the key field fails with `Invalid 'recipients': items must have the field 'email'` (or `Invalid 'recipient_contact_ids': items must have the field 'id'`). |
| Later items | A later object without the key field is not sent and is returned as `{"missing_key_field": true}`. It is counted in `total_selected` only. |
| Empty | Missing or `[]`: `The field 'recipients' must be set`. `"recipient_contact_ids": []`: `The field 'recipient_contact_ids' must not be empty`. |
| Not a list | `The field 'recipients' must be an array` (or `The field 'recipient_contact_ids' must be an array`). |
| Limit | At most 1,000 items: `The field 'recipients' may not contain more than 1000` (or `The field 'recipient_contact_ids' may not contain more than 1000`). Every item counts toward the limit, including invalid ones and repeats. Exactly 1,000 is allowed. |
| Both sent | `recipient_contact_ids` wins and `recipients` is ignored, with no error. |
| By email | Each address is matched against your contacts. An address that matches no contact becomes a new contact and is sent the message. An invalid address is not sent and no contact is created. |
| By ID | Each ID must be a contact in your account. An ID that matches no contact is not sent and is returned as `{"excluded_invalid_ids": true}`. No contacts are created. |
Which recipients are actually sent the message depends on their consent and address status (see [Consent](#consent)). The response reports the outcome for each item (see [Response](https://envoke-developers.pages.dev/v1/send/send/#response)).
**Send each address once, spelled exactly as it is stored in Envoke.** Recipients are not de-duplicated: a repeated item and an address in a different letter case (`Stroke.Seat@example.org` for a contact stored as `stroke.seat@example.org`) are treated as separate recipients.
---
### `html`, `template_id`, and `replace`
`html` or `template_id` sets the content of the message; `replace` personalises it per recipient.
#### `html`
- **Type:** String, at most 200 kB (204,800 bytes)
- **Read-only:** No
- **Required on create:** One of `html` or `template_id`.
- **Example:** `"You are in {BOAT_NAME}, seat {SEAT}.
"`
The complete HTML of the message, stored as sent. Envoke does not check or clean it. If it contains the ``, or the email address alone.
- **Example:** `"Boathouse "`
Both fields take one of the sender identities (From addresses) set up in your account; list them with the [Sender API](https://envoke-developers.pages.dev/v1/senders/fields/). A value is resolved in this order:
| Step | Value | Result |
|---|---|---|
| 1 | Digits that match an identity ID, e.g. `"512"` or `512` | That identity. |
| 2 | `Name ` matching an identity, ignoring case and treating a space and an underscore as the same, e.g. `"boathouse "` | That identity. |
| 3 | `""` or `"0"` | The field is not set. |
| 4 | A valid email address, e.g. `"boathouse@example.org"` | The first identity with that address. The address must match exactly, including case. If none matches: `No sender identity found for email: 'regatta@example.org'` (or `No reply_to identity found for email: ...`). |
| 5 | Anything else | `Invalid 'sender': 'Boathouse'` (or `Invalid 'reply_to': ...`). |
Errors from steps 4 and 5 include the account's identities in `allowed_values`, keyed by ID, as `Name `.
When `sender` is omitted, `""`, or `"0"`, the message uses the identity marked as the account default. `reply_to` is optional; when it is not set, replies go to the sender address. The identity is used as configured.
---
### `group`
- **Type:** String (a group ID may also be sent as an integer)
- **Read-only:** No
- **Required on create:** No — defaults to the `API Messages` group.
- **Accepted values (write):** The name or ID of an existing message group. Names are matched ignoring case, and a space and an underscore are treated as the same. `""` or `"0"` uses the default.
- **Example:** `"Crew Notices"`
The message group (folder) the message is filed in. Only message groups can be used; template groups and contact groups are not accepted. A group is not created automatically: an unknown value fails with `Invalid 'group': 'Regattas'` and the list of valid groups in `allowed_values` (see [Error Responses](#error-responses)). Create groups with the [Group API](https://envoke-developers.pages.dev/v2/groups/fields/).
**The `API Messages` group.** Without `group`, the message is filed in a message group named `API Messages`, which Envoke creates the first time it is needed. In Envoke, messages in this group are shown in the `API Messages` folder and are left out of the **All** folder and the status folders. A message sent with a custom `group` appears in that group's folder and also in **All**. Through the Message API, the message's `group` reads `API Messages` (or the custom group's name).
---
### Consent
Who is sent the message is decided when the request is made, from each recipient's consent and address status.
**Default (without `is_mandatory`).**
| Recipient | Sent when | Otherwise counted as |
|---|---|---|
| Existing contact | Their consent allows sending (for example Express or Implied, but not `Not Provided`, `Revoked`, or `Expired`). | `excluded_unsubscribed_contacts` |
| Existing contact | Their address has not been marked undeliverable. | `excluded_bounced_emails` |
| Existing contact | Your account does not require email confirmation (double opt-in), or they have confirmed their address. | `excluded_unverified_emails` |
| Existing contact | Their stored address is valid. | `excluded_invalid_emails` |
| New address | It is a valid address. The contact is created with the consent status and description from your Transactional API preference, and sent the message. | `excluded_invalid_emails` |
**Mandatory (`is_mandatory: true`).**
| Rule | Behaviour |
|---|---|
| The message | Sent as a mandatory (consent-exempt) message, with the mandatory subscription's description, in the message's language, as the stated reason. |
| Existing contacts | Sent only when they hold your account's mandatory consent subscription and have not reported the account's email as spam. Their other consent does not matter. |
| Without the subscription | A contact who does not hold it is counted as `excluded_mandatory_consent`. A contact who holds it but has reported spam is counted as `excluded_unsubscribed_contacts`. |
| No mandatory subscription | When your account has no mandatory consent subscription, every existing contact is excluded as `excluded_mandatory_consent`, and the message is not marked consent-exempt. |
| New addresses | Created with the consent status and description from your Transactional API preference, given the mandatory consent subscription, and sent the message. |
| Bounced, unverified | Excluded as in the default rules. |
| Counter | The `excluded_mandatory_consent` count in the summary, and the per-recipient `excluded_mandatory_consent` key, appear only when `is_mandatory` is `true`, or when `id` refers to a message first sent with `is_mandatory`. |
New contacts are completed in the background: the contact is created at once with its email address only, and its consent, mandatory subscription, and the account's new-contact automation (such as lists added to every new contact) follow shortly after (see [How sending works](https://envoke-developers.pages.dev/v1/send/send/#how-sending-works)).
---
### Tracking and merge fields
| Field | Default | When `false` | When `true` |
|---|---|---|---|
| `track_recipient_activity` | `false` | Links are left as written and no open-tracking image is added. | Links are rewritten to record clicks, and an open-tracking image is added. |
| `enable_merge_fields` | `false` | Only the basic tags in the table below are replaced. | All Envoke merge fields (such as `{@first_name}`) and the older `{fname}`-style tags are replaced. |
Both accept the same values as `publish_archive`.
With `enable_merge_fields` off, these tags are still replaced:
| Tag | Replaced with |
|---|---|
| `{@pref-0}`, `{@pref-1}`, `{@consent-url}`, `{@consent-prefpage-url}` | The link to the recipient's email preferences page. |
| `{fname}`, `{lname}` | The contact's first and last name. |
| `{eid}` | The contact's `external_id` (as returned by the Contact API). |
**Footer.** When the message HTML contains none of `{@pref-`, `{@consent-url`, or `{@consent-prefpage-url`, Envoke adds its standard unsubscribe footer to the end of every email. To place the unsubscribe link yourself, include one of these tags in your HTML; the footer is then not added. This works whether merge fields are on or off. Tags inserted through `replace` values do not count.
UTM tracking parameters are never added to links in Send API messages.
---
### Not supported
The following keys are not part of this API and fail with `'{field}' is not a valid field`, for example `'send_time' is not a valid field`:
| Key | Notes |
|---|---|
| `send_time` | Every send is immediate. To schedule a message, use the [Message API](https://envoke-developers.pages.dev/v2/messages/sending/). |
| `utm_tag_links` | UTM tracking parameters are always off. |
| `preheader_text` | Not available. |
| `replace` at the top level | `'replace' is not a valid field`. Put `replace` on each recipient object instead (see [`replace`](#replace)). |
| `recipient_subscription`, `recipient_tag`, `recipient_segment`, `recipient_language` | Recipients are listed one by one with `recipients` or `recipient_contact_ids`. To send to a tag, subscription, or segment, use the [Message API](https://envoke-developers.pages.dev/v2/messages/fields/#recipients). |
---
## Error Responses
**Validation errors** (missing or invalid fields, rule violations):
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'subject' must be set",
"'send_time' is not a valid field"
]
}
}
```
Four checks return a single error at once, in this order: the Transactional API preference, the size of `html`, a `template_id` that is not found, and a request with neither `html` nor `template_id`. All other errors are collected and returned together.
When `group`, `sender`, `reply_to`, or `language` has an unknown value, `result_data` also carries `allowed_values`, an object of the valid options keyed by ID. It is included only when the account has 50 or fewer options for that field:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Invalid 'group': 'Regattas'"],
"allowed_values": {
"group": { "41": "Crew Notices", "42": "API Messages" }
}
}
}
```
`allowed_values` is not returned when [`id`](#id) is sent.
**No valid recipients.** When none of the recipients can be sent the message, nothing is created and the response carries the counts and per-recipient entries instead of an `errors` list (see [Partial and failed sends](https://envoke-developers.pages.dev/v1/send/send/#partial-and-failed-sends)):
```json
{
"success": false,
"result_text": "Message not sent, there are no valid recipients",
"result_data": {
"summary": { "total_selected": 1, "emails_to_be_sent": 0, "excluded_unsubscribed_contacts": 1, "excluded_invalid_emails": 0, "excluded_bounced_emails": 0, "excluded_unverified_emails": 0 },
"recipients": [ { "id": 7003, "email": "cox@example.org", "excluded_unsubscribed_contacts": true } ]
}
}
```
**Plain errors.** `Invalid JSON`, `Invalid request`, and `Message content error` use `result_data: []` with the message in `result_text`:
```json
{
"success": false,
"result_data": [],
"result_text": "Message content error"
}
```
**Internal errors.** When the message or its recipients cannot be saved, the response has no `result_data` key at all. Contact Envoke support if it persists:
```json
{
"success": false,
"result_text": "Internal error; please contact Envoke support"
}
```
**Empty or unreadable body.** No body, an empty body, `{}`, `[]`, `null`, `0`, or `false` returns the validation envelope with `Request body may not be empty`. A body that is not valid JSON returns the plain envelope with `Invalid JSON`.
All error responses above use HTTP status `400`. An invalid API ID or key returns `401` with the body `{"success":false,"result_data":[],"result_text":"Invalid credentials"}`. 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 `"This account is not active."`) Exceeding your account's API limit returns `429` with `success: false` and a `result_text` naming the limit.
### Errors by scenario
| Scenario | `result_text` | `result_data` |
|---|---|---|
| Empty body | `Validation failed, see 'result_data' -> 'errors'` | `{"errors":["Request body may not be empty"]}` |
| Body is not valid JSON | `Invalid JSON` | `[]` |
| Request could not be processed | `Invalid request` | `[]` |
| Transactional API preference not set | `Validation failed, ...` | `{"errors":["A default consent status and description is required in order to use this API"]}` |
| `html` over 200 kB | `Validation failed, ...` | `{"errors":["The field 'html' may not be longer than 200kB"]}` |
| `template_id` not found | `Validation failed, ...` | `{"errors":["Nothing found for 'template_id': 1999"]}` |
| Neither `html` nor `template_id` | `Validation failed, ...` | `{"errors":["One of the fields 'template_id' or 'html' must be provided in order to create a message"]}` |
| Field errors | `Validation failed, ...` | `{"errors":[...]}`, with `allowed_values` for lookup fields. The full list is in [Validation Errors](https://envoke-developers.pages.dev/v1/send/send/#validation-errors). |
| `id` not found | `Validation failed, ...` | `{"errors":["No message found for 'id': 3099"]}` |
| `id` is not a Send API message | `Validation failed, ...` | `{"errors":["Only messages created by the API are valid when providing an 'id'"]}` |
| `id` sent with other fields | `Validation failed, ...` | `{"errors":["The field 'subject' is not valid when providing an 'id'"]}` |
| Not enough credits | `Validation failed, ...` | `{"errors":["Message not sent, the account is out of credits"]}` or `{"errors":["Message not sent, the account has not upgraded"]}` |
| No recipient can be sent the message | `Message not sent, there are no valid recipients` | `{"summary":{...},"recipients":[...]}` |
| Content could not be saved | `Message content error` | `[]` |
| Message or recipients could not be saved | `Internal error; please contact Envoke support` | *(no key)* |
### Not found
The Send API has one endpoint, so the only not-found case is an [`id`](#id) that does not match a Send API message in your account. It returns the validation error `No message found for 'id': 3099`, with the value as sent. A message deleted through the Message API, a template, and another account's message are all not found.
---
*Full reference, OpenAPI description, and Postman collection: [developers.envoke.com](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/v1/send/send/
Markdown: https://envoke-developers.pages.dev/v1/send/send.md
# Send API - Send
Use this endpoint to create a message and send it immediately to a list of recipients, or to send an earlier Send API message to more recipients.
---
## Endpoint
| Method | URL | Description |
|---|---|---|
| POST | `https://e1.envoke.com/v1/send` | Create a message and send it now, or with `id`, send an existing Send API message to more recipients. |
Authentication is via HTTP Basic Auth using your API ID and API Key. The request body is a JSON object.
---
## How sending works
A successful request creates the message, decides who is sent it, and queues the emails for delivery straight away. There is no draft, schedule, or cancel step in this API.
| Step | What happens |
|---|---|
| Request | The fields are checked, each recipient is looked up, and the [consent rules](https://envoke-developers.pages.dev/v1/send/fields/#consent) decide who is sent the message. Addresses that match no contact are created as contacts. |
| Response | `Message is sending`, with the new message's `id`, a count per outcome, and one entry per recipient. The emails are already queued. |
| `Sending` | Envoke's sending process delivers the queued emails. |
| `Sent` | Every email has been handed off for delivery. Envoke checks for this every minute. |
- **Who is sent the message is decided when the request is made.** The counts in the response describe the send itself, not an estimate.
- **New contacts are completed in the background.** A new address is created as a contact at once, with its email address only, and is sent the message in the same request. Its consent (the consent status and description from your Transactional API preference), the `api_insert` contact event, and your account's automation for new contacts are added by a background process, usually within a minute. Until then, the Contact API does not return the contact at all: a lookup by email address or by ID returns `[]`.
- **The message can be read through the Message API.** `GET https://e1.envoke.com/v2/messages/{id}` returns it with `status` `Sending`, then `Sent`; `group` `API Messages` (or the group you set); `send_time` the time of the request; `utm_tag_links` `false`; and the recipient fields `recipient_subscription`, `recipient_tag`, `recipient_segment`, and `recipient_language` empty, because Send API recipients are listed one by one. With `include_html=1`, the HTML still shows any `replace` placeholders. In the Message API the `id` is a string.
- **Mandatory messages** sent with `is_mandatory` show your mandatory consent subscription in `recipient_subscription` in the Message API.
- **A sent message cannot be changed.** The Message API refuses to update or send it, because it is not a `Draft`. It can be deleted with `DELETE https://e1.envoke.com/v2/messages/{id}` once its status is `Sent`; while it is `Sending`, delete fails with `Message: 'Head of the Valley - Crew Call-up' is currently sending`. A deleted message can no longer be used with `id`.
---
## Request Body
The fields are described in full in the [Fields Reference](https://envoke-developers.pages.dev/v1/send/fields/).
| Field | Type | Required | Description |
|---|---|---|---|
| `subject` | string | Yes | Subject line, at least 6 characters. |
| `html` | string | One of `html` or `template_id` | The message HTML, at most 200 kB. |
| `template_id` | integer | One of `html` or `template_id` | ID of a template or message whose HTML is copied. `html` wins if both are sent. |
| `recipients` | array | One of `recipients` or `recipient_contact_ids` | Up to 1,000 email addresses, or objects `{ "email": ..., "replace": {...} }`. |
| `recipient_contact_ids` | array | One of `recipients` or `recipient_contact_ids` | Up to 1,000 contact IDs, or objects `{ "id": ..., "replace": {...} }`. Wins if both are sent. |
| `name` | string | No | Internal name, at least 3 characters. Default `API Message {Y-m-d H:i:s}`. |
| `group` | string | No | Message group name or ID. Default `API Messages`. |
| `sender` | string | No | Identity ID, `Name `, or email. Default: the account's default sender. |
| `reply_to` | string | No | Identity ID, `Name `, or email. No default. |
| `language` | string | No | `en` or `fr`. Default `en`. |
| `is_mandatory` | boolean | No | Send as a mandatory consent message. Default `false`. |
| `track_recipient_activity` | boolean | No | Track opens and clicks. Default `false`. |
| `enable_merge_fields` | boolean | No | Replace Envoke merge fields. Default `false`. |
| `publish_archive` | boolean | No | Publish to the account's web archive. Default `false`. |
| `id` | integer | No | Send an existing Send API message to more recipients. Only `recipients` may be sent with it (see [below](#send-to-more-recipients-with-an-existing-message)). |
---
## Response
```json
{
"success": true,
"result_text": "Message is sending",
"result_data": {
"id": 3001,
"new_contacts_created": 0,
"summary": {
"total_selected": 1,
"emails_to_be_sent": 1,
"excluded_unsubscribed_contacts": 0,
"excluded_invalid_emails": 0,
"excluded_bounced_emails": 0,
"excluded_unverified_emails": 0
},
"recipients": [
{ "id": 7001, "email": "stroke.seat@example.org", "emails_to_be_sent": true }
]
}
}
```
| Key | Type | Meaning |
|---|---|---|
| `id` | integer | The new message's ID. When the request sends `id`, this echoes the value as sent, with its JSON type unchanged. |
| `new_contacts_created` | integer | Number of addresses created as new contacts. Always `0` with `recipient_contact_ids`. |
| `summary` | object | One count per outcome, below. |
| `recipients` | array | One entry per recipient, below. |
HTTP status `200`. No dates are returned.
### Summary counts
All counts are integers. Some keys are present only in certain requests.
| Key | Present when | Meaning |
|---|---|---|
| `total_selected` | Always | Every item in the recipient list. |
| `emails_to_be_sent` | Always | Recipients who are sent the message, existing and new. The number of emails queued. |
| `excluded_unsubscribed_contacts` | Always | Existing contacts whose consent does not allow sending, such as `Not Provided`, `Revoked`, or `Expired`. |
| `excluded_invalid_emails` | The request uses `recipients` | Addresses that are not valid, whether new or stored on an existing contact. |
| `excluded_invalid_ids` | The request uses `recipient_contact_ids` | IDs that match no contact in your account. |
| `excluded_bounced_emails` | Always | Existing contacts whose address has been marked undeliverable. |
| `excluded_unverified_emails` | Always | Existing contacts who have not confirmed their address, when your account requires confirmation (double opt-in); otherwise `0`. |
| `excluded_invalid_replace_data` | At least one recipient had an invalid `replace` | Recipients excluded because their `replace` was not valid (see [`replace`](https://envoke-developers.pages.dev/v1/send/fields/#replace)). |
| `excluded_mandatory_consent` | `is_mandatory` is `true`, or `id` refers to a message first sent with `is_mandatory` | Existing contacts who do not hold your mandatory consent subscription (see [Consent](https://envoke-developers.pages.dev/v1/send/fields/#consent)). |
The counts are exclusive: each recipient is counted once, under the first reason that applies, in the order bounced, consent, unverified, invalid address, invalid `replace`. So `emails_to_be_sent` plus the `excluded_` counts equals `total_selected`, except that items missing their key field are counted in `total_selected` only. The keys appear in the order shown.
### Recipient entries
Each entry describes one recipient. Entries follow the order of the request.
| Situation | Entry |
|---|---|
| Existing contact, sent | `{ "id": 7001, "email": "stroke.seat@example.org", "emails_to_be_sent": true }` |
| Existing contact, excluded | `{ "id": 7003, "email": "cox@example.org", "excluded_unsubscribed_contacts": true }`, with one of `excluded_bounced_emails`, `excluded_unsubscribed_contacts`, `excluded_mandatory_consent`, `excluded_unverified_emails`, `excluded_invalid_emails`, or `excluded_invalid_replace_data` |
| New address, created and sent | `{ "id": 7005, "email": "novice.rower@example.org", "new_contacts_created": true, "emails_to_be_sent": true }` |
| New address, not valid | `{ "email": "kvrc-not-an-email", "excluded_invalid_emails": true }` (no contact is created) |
| New address, invalid `replace` | `{ "email": "spare.rower@example.org", "excluded_invalid_replace_data": true }` (no contact is created) |
| ID not found | `{ "excluded_invalid_ids": true }` (the ID is not repeated; match it by position) |
| Object without its key field | `{ "missing_key_field": true }` |
- `id` is the contact ID, an integer, for existing and new contacts.
- `email` is the address as stored on the contact for an existing contact, and as sent for a new address.
- Entries are keyed by the stored address, so a recipient listed more than once has a single entry.
---
## Partial and failed sends
A request succeeds as long as at least one recipient can be sent the message. Excluded recipients are reported in the response but do not fail the request.
| Outcome | `success` | HTTP | `result_text` | `result_data` |
|---|---|---|---|---|
| At least one recipient sent | `true` | `200` | `Message is sending` | `id`, `new_contacts_created`, `summary`, `recipients` |
| No recipient can be sent | `false` | `400` | `Message not sent, there are no valid recipients` | `summary` and `recipients` only: no `id`, no `new_contacts_created`, and no `errors` key |
When no recipient can be sent, no message is created. The counts and entries show why each recipient was excluded:
```json
{
"success": false,
"result_text": "Message not sent, there are no valid recipients",
"result_data": {
"summary": { "total_selected": 2, "emails_to_be_sent": 0, "excluded_unsubscribed_contacts": 1, "excluded_invalid_emails": 1, "excluded_bounced_emails": 0, "excluded_unverified_emails": 0 },
"recipients": [
{ "id": 7003, "email": "cox@example.org", "excluded_unsubscribed_contacts": true },
{ "email": "kvrc-not-an-email", "excluded_invalid_emails": true }
]
}
}
```
A valid new address without an invalid `replace` is always sent, so a request with one never ends this way.
**Credits.** For a prepaid account, the request is refused when the account's credit balance is lower than `emails_to_be_sent`: `Message not sent, the account is out of credits`. A monthly-billed account that is not yet on a paid plan, and has too few credits, gets `Message not sent, the account has not upgraded`. No message is created.
---
## Send to more recipients with an existing message
A request can list at most 1,000 recipients. To send the same message to more people, send the first 1,000 as a normal request, then send each further batch of up to 1,000 with the `id` from the first response:
```json
{
"id": 3006,
"recipients": ["bow.seat@example.org", "novice.rower@example.org"]
}
```
| Rule | Behaviour |
|---|---|
| Which messages | Only messages created by the Send API. Any other message fails with `Only messages created by the API are valid when providing an 'id'`. |
| Not found | An ID that is not in your account, or a message that has been deleted, fails with `No message found for 'id': 3099` (the value as sent). |
| Allowed fields | Only `id` and `recipients`. Any other field fails with `The field 'subject' is not valid when providing an 'id'`, or for several, `The fields 'subject', 'html' are not valid when providing an 'id'`. This includes `recipient_contact_ids`, so recipients are added by email address. |
| Recipients | The same forms, limit, and checks as a new message, including per-recipient `replace`, new contacts for unknown addresses, and the [consent rules](https://envoke-developers.pages.dev/v1/send/fields/#consent). A message first sent with `is_mandatory` is sent as a mandatory message again. |
| Reused | The message's content, subject, sender, reply-to, language, group, tracking, and merge field settings. They cannot be changed. |
| Status | Unchanged. A message that is already `Sent` stays `Sent` while the new batch is delivered. |
| Response | The same shape as a new message, with `result_text` `Message is sending`. `id` echoes the value you sent: `3006` returns `3006`, and `"3006"` returns `"3006"`. |
| Errors | The field errors are returned together, without `allowed_values`. The credit and no-valid-recipients responses are the same as for a new message. |
---
## Validation Errors
Validation errors use HTTP status `400` and this envelope:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["One of the fields 'template_id' or 'html' must be provided in order to create a message"]
}
}
```
### Common validation errors
| Error | Cause |
|---|---|
| `Request body may not be empty` | No body, an empty body, `{}`, `[]`, `null`, `0`, or `false`. |
| `A default consent status and description is required in order to use this API` | The Transactional API preference is not set (see [Overview](https://envoke-developers.pages.dev/v1/send/fields/#overview)). Returned alone. |
| `The field 'html' may not be longer than 200kB` | `html` is over 204,800 bytes. Returned alone. |
| `Nothing found for 'template_id': 1999` | No template or message with that ID. Returned alone. |
| `One of the fields 'template_id' or 'html' must be provided in order to create a message` | Neither field is sent, or both are empty. Returned alone. |
| `The field 'subject' must be set` | No subject, or `""`. |
| `The field 'subject' must be at least 6 characters long` | Subject shorter than 6 characters (also returned for `""`). |
| `The field 'name' must be at least 3 characters long` | Name shorter than 3 characters. |
| `The field 'recipients' must be set` | No `recipients` (and no `recipient_contact_ids`), or `[]`. |
| `The field 'recipients' must be an array` | `recipients` is not a list. |
| `The field 'recipients' may not contain more than 1000` | More than 1,000 items. |
| `Invalid 'recipients': items must have the field 'email'` | The first item is an object without `email`. |
| `The field 'recipient_contact_ids' must not be empty` | `recipient_contact_ids` is `[]`. |
| `The field 'recipient_contact_ids' must be an array` | `recipient_contact_ids` is not a list. |
| `The field 'recipient_contact_ids' may not contain more than 1000` | More than 1,000 items. |
| `Invalid 'recipient_contact_ids': items must have the field 'id'` | The first item is an object without `id`. |
| `Invalid 'group': 'Regattas'` | No message group with that name or ID. With `allowed_values`. |
| `Invalid 'sender': 'Boathouse'` | Not an identity ID, `Name `, or email. With `allowed_values`. Also for `reply_to`. |
| `No sender identity found for email: 'regatta@example.org'` | No identity has that address (exact case). With `allowed_values`. Also `No reply_to identity found for email: ...`. |
| `Invalid 'language': 'de'` | Not `en`, `fr`, `1`, or `3`. With `allowed_values`. |
| `'send_time' is not a valid field` | A key that is not a Send API field, including `utm_tag_links`, `preheader_text`, and a top-level `replace` (see [Not supported](https://envoke-developers.pages.dev/v1/send/fields/#not-supported)). |
| `No message found for 'id': 3099` | `id` does not match a message in your account. Returned alone. |
| `Only messages created by the API are valid when providing an 'id'` | `id` is a message that was not created by the Send API. Returned alone. |
| `The field 'subject' is not valid when providing an 'id'` | A field other than `recipients` was sent with `id`. |
| `Message not sent, the account is out of credits` | Not enough prepaid credits for `emails_to_be_sent`. |
| `Message not sent, the account has not upgraded` | Monthly-billed account not yet on a paid plan. |
"Returned alone" errors stop the request at once; the others are collected and returned together. Other failures use different envelopes: `Message not sent, there are no valid recipients` (see [Partial and failed sends](#partial-and-failed-sends)), and `Invalid JSON`, `Invalid request`, `Message content error`, and `Internal error; please contact Envoke support` (see [Error Responses](https://envoke-developers.pages.dev/v1/send/fields/#error-responses)).
---
## Examples
### Send HTML to one address
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject": "Practice moved to 6:00 a.m.", "html": "Tomorrow'\''s practice starts at 6:00 a.m. at the boathouse.
", "recipients": ["stroke.seat@example.org"]}' \
"https://e1.envoke.com/v1/send"
```
The message is named `API Message {date and time}`, filed in the `API Messages` group, and sent from the account's default sender, without open or click tracking. The response is shown under [Response](#response).
### Personalise each email with replace
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Head of the Valley - Crew Call-up",
"subject": "Crew call-up: Head of the Valley",
"sender": "Boathouse ",
"html": "You are racing in {BOAT_NAME}, seat {SEAT}.
Be at the boathouse by {CALL_TIME}.
",
"recipients": [
{ "email": "stroke.seat@example.org", "replace": { "{BOAT_NAME}": "Heron", "{SEAT}": "8 (stroke)", "{CALL_TIME}": "6:15 a.m." } },
{ "email": "bow.seat@example.org", "replace": { "{BOAT_NAME}": "Heron", "{SEAT}": "1 (bow)", "{CALL_TIME}": "6:15 a.m." } }
]
}' \
"https://e1.envoke.com/v1/send"
```
Each rower receives the HTML with their own boat, seat, and call time. The values are filled in when each email is delivered; the stored message keeps the `{BOAT_NAME}`, `{SEAT}`, and `{CALL_TIME}` placeholders.
### Fill in a template, with merge fields
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Spring Regatta Confirmation", "subject": "Your Spring Regatta entry is confirmed", "template_id": 1951, "enable_merge_fields": true, "recipients": [{"email": "stroke.seat@example.org", "replace": {"{RACE_NAME}": "Men'\''s Eight", "{START_TIME}": "9:40 a.m."}}, {"email": "bow.seat@example.org", "replace": {"{RACE_NAME}": "Mixed Four", "{START_TIME}": "11:05 a.m."}}]}' \
"https://e1.envoke.com/v1/send"
```
Copies the HTML of the `Regatta Template` (ID `1951`), which contains `Hi {@first_name}`, `{RACE_NAME}`, and `{START_TIME}`. Each recipient's `replace` values are applied first, then merge fields fill in `{@first_name}` from the contact. `"template_id": "1951"` works the same.
### Send to contacts by ID
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject": "Erg test results are posted", "html": "Your erg test results are on the club board.
", "recipient_contact_ids": [7001, 7002, 7009]}' \
"https://e1.envoke.com/v1/send"
```
Contact `7009` does not exist:
```json
{
"success": true,
"result_text": "Message is sending",
"result_data": {
"id": 3004,
"new_contacts_created": 0,
"summary": { "total_selected": 3, "emails_to_be_sent": 2, "excluded_unsubscribed_contacts": 0, "excluded_invalid_ids": 1, "excluded_bounced_emails": 0, "excluded_unverified_emails": 0 },
"recipients": [
{ "id": 7001, "email": "stroke.seat@example.org", "emails_to_be_sent": true },
{ "id": 7002, "email": "bow.seat@example.org", "emails_to_be_sent": true },
{ "excluded_invalid_ids": true }
]
}
}
```
### Set every option
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Régate de printemps - Résultats", "subject": "Résultats de la régate de printemps", "group": "Crew Notices", "sender": "boathouse@example.org", "reply_to": 513, "language": "fr", "track_recipient_activity": true, "publish_archive": true, "html": "Les résultats sont en ligne : voir les résultats.
", "recipients": ["stroke.seat@example.org", "bow.seat@example.org"]}' \
"https://e1.envoke.com/v1/send"
```
Files the message in the `Crew Notices` group (ID `41`), so it also appears in the **All** folder in Envoke. The sender is found by its address (identity `512`, `Boathouse `) and replies go to identity `513`. The footer Envoke adds is in French, links are rewritten to track clicks, an open-tracking image is added, and the message is published to the web archive.
### Send a mandatory message
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Updated Water Safety Policy", "subject": "Updated water safety policy for all members", "is_mandatory": true, "html": "The club'\''s water safety policy has changed. Please read it before your next outing.
", "recipients": ["stroke.seat@example.org", "bow.seat@example.org"]}' \
"https://e1.envoke.com/v1/send"
```
Contact `7001` holds the account's mandatory consent subscription; contact `7002` does not:
```json
{
"success": true,
"result_text": "Message is sending",
"result_data": {
"id": 3007,
"new_contacts_created": 0,
"summary": { "total_selected": 2, "emails_to_be_sent": 1, "excluded_unsubscribed_contacts": 0, "excluded_invalid_emails": 0, "excluded_bounced_emails": 0, "excluded_unverified_emails": 0, "excluded_mandatory_consent": 1 },
"recipients": [
{ "id": 7001, "email": "stroke.seat@example.org", "emails_to_be_sent": true },
{ "id": 7002, "email": "bow.seat@example.org", "excluded_mandatory_consent": true }
]
}
}
```
### A mixed result
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Learn to Row - Week 1", "subject": "Welcome to Learn to Row", "html": "Week 1 starts Saturday at 8:00 a.m.
", "recipients": ["stroke.seat@example.org", "novice.rower@example.org", "cox@example.org", "kvrc-not-an-email"]}' \
"https://e1.envoke.com/v1/send"
```
`stroke.seat@example.org` is an existing contact with Express consent, `novice.rower@example.org` matches no contact, `cox@example.org` has unsubscribed (`Revoked`), and the last item is not an email address:
```json
{
"success": true,
"result_text": "Message is sending",
"result_data": {
"id": 3006,
"new_contacts_created": 1,
"summary": { "total_selected": 4, "emails_to_be_sent": 2, "excluded_unsubscribed_contacts": 1, "excluded_invalid_emails": 1, "excluded_bounced_emails": 0, "excluded_unverified_emails": 0 },
"recipients": [
{ "id": 7001, "email": "stroke.seat@example.org", "emails_to_be_sent": true },
{ "id": 7005, "email": "novice.rower@example.org", "new_contacts_created": true, "emails_to_be_sent": true },
{ "id": 7003, "email": "cox@example.org", "excluded_unsubscribed_contacts": true },
{ "email": "kvrc-not-an-email", "excluded_invalid_emails": true }
]
}
}
```
Two emails are sent. Contact `7005` is created for `novice.rower@example.org` and receives the account's Transactional API consent shortly after.
### Add recipients to an existing message
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"id": 3006, "recipients": ["bow.seat@example.org"]}' \
"https://e1.envoke.com/v1/send"
```
Sends message `3006`, with its content and settings unchanged, to one more contact:
```json
{
"success": true,
"result_text": "Message is sending",
"result_data": {
"id": 3006,
"new_contacts_created": 0,
"summary": { "total_selected": 1, "emails_to_be_sent": 1, "excluded_unsubscribed_contacts": 0, "excluded_invalid_emails": 0, "excluded_bounced_emails": 0, "excluded_unverified_emails": 0 },
"recipients": [
{ "id": 7002, "email": "bow.seat@example.org", "emails_to_be_sent": true }
]
}
}
```
### Errors
**No content:**
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject": "Crew call-up: Head of the Valley", "recipients": ["stroke.seat@example.org"]}' \
"https://e1.envoke.com/v1/send"
```
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["One of the fields 'template_id' or 'html' must be provided in order to create a message"]
}
}
```
**No valid recipients:**
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject": "Practice moved to 6:00 a.m.", "html": "Practice starts at 6:00 a.m.
", "recipients": ["cox@example.org", "kvrc-not-an-email"]}' \
"https://e1.envoke.com/v1/send"
```
`cox@example.org` has unsubscribed and the other item is not an address, so no message is created. The response is shown under [Partial and failed sends](#partial-and-failed-sends).
**Another field with `id`:**
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"id": 3006, "subject": "Week 1 reminder", "recipients": ["bow.seat@example.org"]}' \
"https://e1.envoke.com/v1/send"
```
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["The field 'subject' is not valid when providing an 'id'"]
}
}
```
Nothing is sent. Send only `id` and `recipients`.
---
*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).*
---
URL: https://envoke-developers.pages.dev/v1/senders/fields/
Markdown: https://envoke-developers.pages.dev/v1/senders/fields.md
# Sender Identity API - Fields Reference
This article describes all fields available through the Sender Identity API. Use this as a reference when reading, creating, or updating sender identities.
---
## Overview
The Sender Identity API lets you manage the sender identities in your Envoke account programmatically. Sender identities are the From name and email address combinations that appear in your contacts' inboxes. Each account must have at least one sender identity, and exactly one identity is always designated as the default.
All endpoints require [HTTP Basic Auth](https://envoke-developers.pages.dev/guides/authentication/) using your API ID and API Key.
---
## Fields
| Field | Type | Read-only | Required on Create | Description |
|---|---|---|---|---|
| `id` | string | Yes | — | Unique identifier assigned on creation. |
| `name` | string | No | Yes | The sender display name shown in the From field. |
| `email` | string | No | Yes | The sender email address. |
| `is_default` | boolean | Yes | — | Whether this is the account's default sender. |
| `dkim_status` | object | — | — | DKIM verification status. Returned on create and update only. |
---
## Field Details
### `id`
- **Type:** String
- **Read-only:** Yes — assigned automatically on creation.
- **Example:** `"42"`
Used to identify the sender in update, delete, and set-default requests.
---
### `name`
- **Type:** String
- **Read-only:** No — updatable via PATCH.
- **Required on create:** Yes
- **Constraints:** Double quotes (`"`) are not allowed.
- **Example:** `"City of Harborview"`
The display name shown in the From field of outbound emails. Choose a name your contacts will immediately recognise.
---
### `email`
- **Type:** String
- **Read-only:** No — updatable via PATCH.
- **Required on create:** Yes
- **Constraints:** Must be a valid email address using a domain your organisation controls. Free and personal email providers are not accepted.
- **Example:** `"communications@harborview.gov"`
The email address shown in the From field. Use a domain your organisation owns for best deliverability results. Addresses from free or personal email providers will be rejected.
---
### `is_default`
- **Type:** Boolean
- **Read-only:** Yes — managed via `POST /v1/senders/{id}/default`.
- **Values:** `true` or `false`
- **Example:** `true`
Indicates whether this sender identity is the account's current default. Exactly one identity is always the default. Use the [set default endpoint](https://envoke-developers.pages.dev/v1/senders/update/#set-the-default-sender) to change which identity is the default.
The very first identity you create is automatically set as the default.
---
### `dkim_status`
- **Type:** Object
- **Read-only:** N/A — informational only.
- **Returned by:** Create (`POST /v1/senders`) and update (`PATCH /v1/senders/{id}`) responses. **Not included** in list or single-GET responses.
- **Shape:** `{ "verified": boolean, "domain": string }`
| Key | Type | Description |
|---|---|---|
| `exists` | boolean | `true` if a DKIM private key has been generated or uploaded for this domain. |
| `verified` | boolean | `true` if DKIM is configured and the DNS records have been verified. `false` if setup is incomplete. |
| `domain` | string | The domain extracted from the sender's email address. |
**Example:**
```json
{
"exists": true,
"verified": true,
"domain": "harborview.gov"
}
```
When `verified` is `false`, DKIM has not been configured for this domain. Configuring DKIM DNS records for your domain improves email deliverability and reduces the likelihood of messages being flagged as spam. Contact Envoke support if you need assistance setting up DKIM.
---
## Error Responses
**Validation errors** (missing required fields, invalid values, etc.):
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'name' is required"
]
}
}
```
**Not-found behaviour varies by method:**
- **GET** `/senders/{id}` — when the ID is not found, returns an empty array (`[]`).
- **PATCH** `/senders/{id}` and **DELETE** `/senders/{id}` — when the ID is not found, returns a validation error:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '42' not found"]
}
}
```
---
*Full reference, OpenAPI description, and Postman collection: [developers.envoke.com](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/v1/senders/read/
Markdown: https://envoke-developers.pages.dev/v1/senders/read.md
# Sender Identity API - Read
Use the read endpoints to retrieve sender identities for your account. You can list all identities with filtering and pagination, or retrieve a single identity by ID.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| GET | `https://e1.envoke.com/v1/senders` | Return a list of sender identities. |
| GET | `https://e1.envoke.com/v1/senders/{ID}` | Return a single sender identity. |
Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## List Sender Identities
```
GET https://e1.envoke.com/v1/senders
```
Returns a JSON array of sender identities for your account, sorted alphabetically by name by default. Supports filtering, sorting, pagination, and field selection.
### 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). |
| `skip` | integer | `0` | Number of records to skip. Use with `limit` to paginate. |
| `fields` | string | *(all)* | Comma-separated list of fields to include in each result. See limit note below. |
| `result_type` | string | *(array)* | Set to `kvp` to receive results as a key-value object (`{ "id": "name" }`). See limit note below. |
| `filter[id]` | string | — | Filter by sender ID. |
| `filter[name]` | string | — | Filter by sender display name. |
| `filter[email]` | string | — | Filter by sender email address. |
| `filter[is_default]` | string | — | Filter by default status. Use `1` for the default sender, `0` for non-default senders. |
| `sort[name]` | string | — | Sort by sender display name. Value: `asc` or `desc`. |
| `sort[email]` | string | — | Sort by sender email address. Value: `asc` or `desc`. |
**Note — limit behaviour with `?fields=` and `?result_type=kvp`:** When either of these parameters is present, the default limit increases to 10,000 (returning all records if no explicit limit is specified) and a `?limit=` value above 100 is honoured as-is. This allows full exports and dropdown population without pagination.
### Filter Operators
All `filter[field]` parameters support the following operators:
| Operator | Example | Description |
|---|---|---|
| *(none)* | `?filter[name]=City+of+Harborview` | Exact match |
| `(ne)` | `?filter[name][(ne)]=City+of+Harborview` | Not equal |
*Note: URL-encode brackets in production requests (e.g. `%5B` for `[`).*
### Response
A JSON array of sender identity objects. Returns an empty array (`[]`) if no identities match.
```json
[
{
"id": "1",
"name": "City of Harborview",
"email": "communications@harborview.gov",
"is_default": true
},
{
"id": "2",
"name": "Parks & Recreation",
"email": "parks@harborview.gov",
"is_default": false
}
]
```
Note: `dkim_status` is not included in list responses. It is only returned on create and update.
---
## Get a Single Sender Identity
```
GET https://e1.envoke.com/v1/senders/{ID}
```
Returns a single sender identity object. Replace `{ID}` with the Envoke sender ID.
If the ID is not found, the API returns an empty array (`[]`).
### Response
```json
{
"id": "1",
"name": "City of Harborview",
"email": "communications@harborview.gov",
"is_default": true
}
```
---
## Examples
### Return all sender identities (default pagination)
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" "$BASE/v1/senders"
```
### Get a single sender by ID
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" "$BASE/v1/senders/1"
```
### Filter to find the default sender
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"$BASE/v1/senders?filter%5Bis_default%5D=1"
```
### Filter by email address
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"$BASE/v1/senders?filter%5Bemail%5D=communications%40harborview.gov"
```
### Filter by name (exact match)
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"$BASE/v1/senders?filter%5Bname%5D=Parks+%26+Recreation"
```
### Sort by name descending
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"$BASE/v1/senders?sort%5Bname%5D=desc"
```
### Sort by email ascending
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"$BASE/v1/senders?sort%5Bemail%5D=asc"
```
### Return only specific fields
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"$BASE/v1/senders?fields=id,name,email"
```
### Return as a key-value map (for dropdowns)
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"$BASE/v1/senders?result_type=kvp"
```
Response:
```json
{ "1": "City of Harborview", "2": "Parks & Recreation" }
```
### Paginate through results
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"$BASE/v1/senders?limit=5&skip=5"
```
---
*Note: The curl examples above are formatted for readability. URL-encode brackets in production requests (e.g. `%5B` for `[`, `%5D` for `]`).*
*Full reference, OpenAPI description, and Postman collection: [developers.envoke.com](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/v1/senders/insert/
Markdown: https://envoke-developers.pages.dev/v1/senders/insert.md
# Sender Identity API - Insert
Use this endpoint to create a new sender identity in your account.
---
## Endpoint
```
POST https://e1.envoke.com/v1/senders
```
Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
---
## Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Sender display name shown in the From field. Double quotes (`"`) are not allowed. |
| `email` | string | Yes | Sender email address. Must use a domain your organisation controls. |
Free and personal email providers are not accepted as sender email addresses. Use a domain your organisation owns.
---
## Response
On success, the API returns the newly created sender identity:
```json
{
"success": true,
"result_text": "Created",
"result_data": {
"id": "1",
"name": "City of Harborview",
"email": "communications@harborview.gov",
"is_default": true,
"dkim_status": {
"exists": true,
"verified": true,
"domain": "harborview.gov"
}
}
}
```
`result_data.id` is the Envoke sender ID. Store this to use in update, delete, and set-default requests.
`result_data.is_default` will be `true` if this is the first sender identity in your account — the first identity created is automatically set as the default.
`result_data.dkim_status.verified` indicates whether DKIM is configured for this domain. If `false`, configure DKIM DNS records for your domain to improve email deliverability.
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"The field 'name' is required"
]
}
}
```
Common validation errors:
| Error | Cause |
|---|---|
| `Request body may not be empty` | No JSON body was provided. |
| `The field 'name' is required` | `name` was missing or blank. |
| `Email is required` | `email` was missing or blank. |
| `Email address is not valid` | The value in `email` is not a valid email address format. |
| `Double quotes are not allowed in the from name field` | The `name` value contains a `"` character. |
| `Double quotes are not allowed in the email field` | The `email` value contains a `"` character. |
| `Third party email providers such as gmail.com, yahoo.com, etc. cannot be used as senders` | The email address uses a free or personal email provider. |
| `A sender with this name and email already exists` | A sender with the same name and email combination already exists in your account. |
---
## Examples
### Create the primary sender identity
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "City of Harborview", "email": "communications@harborview.gov"}' \
"$BASE/v1/senders"
```
### Create a department sender
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Parks & Recreation", "email": "parks@harborview.gov"}' \
"$BASE/v1/senders"
```
### Create a second department sender
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Planning Department", "email": "planning@harborview.gov"}' \
"$BASE/v1/senders"
```
---
*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).*
---
URL: https://envoke-developers.pages.dev/v1/senders/update/
Markdown: https://envoke-developers.pages.dev/v1/senders/update.md
# Sender Identity API - Update
Use these endpoints to update an existing sender identity or change the account's default sender.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| PATCH | `https://e1.envoke.com/v1/senders/{ID}` | Update the name and/or email of a sender identity. |
| POST | `https://e1.envoke.com/v1/senders/{ID}/default` | Set a sender identity as the account's default. |
Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## Update a Sender Identity
```
PATCH https://e1.envoke.com/v1/senders/{ID}
```
Replace `{ID}` with the Envoke sender ID you want to update. The request body must be JSON.
### Request Body
At least one field must be provided.
| Field | Type | Description |
|---|---|---|
| `name` | string | Updated sender display name. Double quotes (`"`) are not allowed. |
| `email` | string | Updated sender email address. Must use a domain your organisation controls. |
`id` and `is_default` are read-only and cannot be updated via PATCH. To change the default sender, use `POST /v1/senders/{ID}/default` (see below).
If `id` is included in the request body, it must match the `{ID}` in the URL. If they differ, a validation error is returned: `"Provided 'id': {url_id} does not match 'id' in request body ({body_id})"`.
### Response
On success, the API returns the updated sender identity including `dkim_status`:
```json
{
"success": true,
"result_text": "Updated",
"result_data": {
"id": "2",
"name": "Parks & Recreation",
"email": "parks@harborview.gov",
"is_default": false,
"dkim_status": {
"verified": true,
"domain": "harborview.gov"
}
}
}
```
`dkim_status.verified` reflects the DKIM state for the sender's domain after the update. If you changed the `email` field to use a different domain, this indicates whether the new domain has DKIM configured.
### Validation Errors
Common errors:
| Error | Cause |
|---|---|
| `Request body may not be empty` | No JSON body was provided. |
| `Item with id '42' not found` | The `{ID}` in the URL does not match any sender in your account. |
| `Provided 'id': {url_id} does not match 'id' in request body ({body_id})` | `id` in the request body differs from `{ID}` in the URL. |
| `The field 'name' is required` | `name` was provided but is blank or whitespace only. |
| `Email is required` | `email` was provided but is blank. |
| `Email address is not valid` | The value in `email` is not a valid email format. |
| `Double quotes are not allowed in the from name field` | The `name` value contains a `"` character. |
| `Double quotes are not allowed in the email field` | The `email` value contains a `"` character. |
| `Third party email providers such as gmail.com, yahoo.com, etc. cannot be used as senders` | The email address uses a free or personal provider. |
| `A sender with this name and email already exists` | Another sender in your account already has this name and email combination. |
### Examples
**Update the name only:**
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Office of Communications"}' \
"$BASE/v1/senders/1"
```
**Update the email only:**
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "info@harborview.gov"}' \
"$BASE/v1/senders/1"
```
**Update both name and email:**
```bash
curl -X PATCH -gs \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Office of Communications", "email": "info@harborview.gov"}' \
"$BASE/v1/senders/1"
```
---
## Set the Default Sender
```
POST https://e1.envoke.com/v1/senders/{ID}/default
```
Promotes the specified sender identity to be the account's default. Replace `{ID}` with the sender ID to promote.
The default sender is used automatically when no sender is explicitly specified in a campaign. Exactly one identity is always the default — when you promote a new identity, the previous default is automatically demoted.
This endpoint requires no request body.
### Response
On success:
```json
{
"success": true,
"result_text": "Updated",
"result_data": []
}
```
If the sender ID is not found:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '42' not found"]
}
}
```
### Example
**Set a department sender as the default:**
```bash
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
"$BASE/v1/senders/2/default"
```
**To confirm the change, read back the updated sender:**
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" "$BASE/v1/senders/2"
```
Expected: `is_default` is `true`.
---
*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).*
---
URL: https://envoke-developers.pages.dev/v1/senders/delete/
Markdown: https://envoke-developers.pages.dev/v1/senders/delete.md
# Sender Identity API - Delete
Use this endpoint to delete a sender identity from your account.
---
## Endpoint
```
DELETE https://e1.envoke.com/v1/senders/{ID}
```
Replace `{ID}` with the Envoke sender ID you want to delete. Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## Important
**Deletion is permanent.** Deleted sender identities cannot be recovered via the API.
**The default sender cannot be deleted.** If you need to remove the current default, first use `POST /v1/senders/{ID}/default` to promote a different identity as the default, then delete the former default.
---
## Response
On success:
```json
{
"success": true,
"result_text": "Deleted",
"result_data": []
}
```
If the sender ID is not found:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["Item with id '42' not found"]
}
}
```
If you attempt to delete the default sender:
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": ["The default sender identity cannot be deleted"]
}
}
```
---
## Examples
### Delete a sender identity
```bash
curl -X DELETE -gs \
-u "$API_ID:$API_KEY" \
"$BASE/v1/senders/4"
```
### Promote a new default, then delete the old one
If you need to delete the current default, promote another sender first:
```bash
# Step 1 — promote sender 2 as the new default
curl -X POST -gs \
-u "$API_ID:$API_KEY" \
"$BASE/v1/senders/2/default"
# Step 2 — delete the old default (sender 1)
curl -X DELETE -gs \
-u "$API_ID:$API_KEY" \
"$BASE/v1/senders/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).*
---
URL: https://envoke-developers.pages.dev/v2/reports/fields/
Markdown: https://envoke-developers.pages.dev/v2/reports/fields.md
# 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.
---
## Overview
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](https://envoke-developers.pages.dev/v2/reports/messages/) |
| Message stats | `GET https://e1.envoke.com/v2/reports/messages/{id}` and `GET https://e1.envoke.com/v2/reports/messages/compare` | [Message Stats](https://envoke-developers.pages.dev/v2/reports/message-stats/) |
| Message contacts | `GET https://e1.envoke.com/v2/reports/messages/{id}/contacts` | [Message Contacts](https://envoke-developers.pages.dev/v2/reports/message-contacts/) |
| Contact activity | `GET https://e1.envoke.com/v2/reports/contacts/{id}` and `GET https://e1.envoke.com/v2/reports/contacts` | [Contact Activity](https://envoke-developers.pages.dev/v2/reports/contacts/) |
Every message report uses the same ten [metrics](#fields), 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](https://envoke-developers.pages.dev/v2/messages/fields/), message folders (`group_id`) are managed with the [Group API](https://envoke-developers.pages.dev/v2/groups/fields/), and the tags and subscriptions used as filters with the [Tag API](https://envoke-developers.pages.dev/v2/tags/fields/) and [Subscription API](https://envoke-developers.pages.dev/v2/subscriptions/fields/). Contact `id` values are the same as the `id` of the [Contact API](https://envoke-developers.pages.dev/v1/contacts/fields/).
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](https://envoke-developers.pages.dev/guides/authentication/) using your API ID and API Key.
---
## Fields
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`. |
---
## Field Details
### How the counts work
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](#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`](#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.
---
### `sends`
- **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`](#skips), not here.
---
### `delivered`
- **Type:** Integer
- **Codes:** Computed. Lifetime (no activity window): [`sends`](#sends) − [`bounces`](#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.
---
### `bounces`
- **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.
---
### `hard_bounces`
- **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`
---
### `soft_bounces`
- **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.
---
### `opens`
- **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](https://envoke-developers.pages.dev/v2/reports/message-contacts/#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`](https://envoke-developers.pages.dev/v2/reports/message-stats/#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.
---
### `clicks`
- **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.
---
### `unsubscribes`
- **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.
---
### `spam_complaints`
- **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`
---
### `skips`
- **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
Rates appear in the `rates` object of the [single-message and compare reports](https://envoke-developers.pages.dev/v2/reports/message-stats/), 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`](#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`](#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`](#clicks)).
---
### Messages
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](https://envoke-developers.pages.dev/v2/reports/message-stats/#ab-tests). (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](https://envoke-developers.pages.dev/v2/reports/message-contacts/) (`event=opened` for `opens`, and so on).
---
### Times and time zone
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](https://envoke-developers.pages.dev/v2/messages/fields/#dates-and-time-zones) 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.
---
## Shared Query Parameters
### Date parameters
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](#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.
### `limit` and `skip`
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](https://envoke-developers.pages.dev/v2/reports/message-contacts/#fields-and-the-10000-limit)).
### Unknown parameters
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`.
---
## Error Responses
**Validation errors** return HTTP **400** with the validation envelope. Every problem in the request is listed:
```json
{
"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:
```json
{
"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:
```json
{
"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](https://envoke-developers.pages.dev/v2/reports/contacts/#errors). The message contacts report returns the same `403` for every event; see [Message Contacts errors](https://envoke-developers.pages.dev/v2/reports/message-contacts/#errors).
### Not found
**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](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/v2/reports/messages/
Markdown: https://envoke-developers.pages.dev/v2/reports/messages.md
# Reports API - Message Activity
Use the message activity list to retrieve the ten activity metrics for your messages, one row per message, filtered by message, folder, recipient list, type, and date.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| GET | `https://e1.envoke.com/v2/reports/messages` | Return activity metrics per message. |
Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## List Message Activity
```
GET https://e1.envoke.com/v2/reports/messages
```
Returns a JSON array of rows, one per message, most recently sent first. Each row describes the message and gives its ten [metrics](https://envoke-developers.pages.dev/v2/reports/fields/#fields) as integers. When nothing matches, the response is `[]`.
The list has no rates and no breakdowns; for those, request the message's [single report](https://envoke-developers.pages.dev/v2/reports/message-stats/).
### Which messages are included
By default the list includes:
- sent email campaigns (Message API status `Sent`), and
- automated messages (status `Nurture`).
`type=campaign` or `type=automated` narrows the list to one of the two. `include_in_progress=1` also includes campaigns that have started sending but are not finished (status `AB test in progress`, `Queuing`, or `Sending`). Their numbers change from one request to the next until the send completes, and an A/B test still in its test phase shows only the contacts sent so far.
Drafts, scheduled messages that have not started, messages of the `Transactional` status, and deleted or hidden messages (a message hidden or deleted in the app, stored with `visible` = 0) are never included. An automated message whose sequence is transactional has the `Nurture` status and is included like any automated message.
A message is listed when it matches every filter, whether or not it has any activity: a message sent to nobody, or one with nothing recorded inside the activity window, appears with every count `0`. When an activity window is given, messages launched after `activity_to` are left out, since nothing can have happened to them yet.
**A/B tests.** An A/B test is one message with one row. `has_variants` is `true`, `subject` is `null` for a subject-line test (each version's subject is in the single report), and the counts are those of all its versions together, including the contacts sent the winning version after the test. The versions are never listed as rows; their own counts are in the `variants` of the [single-message report](https://envoke-developers.pages.dev/v2/reports/message-stats/#ab-tests).
### Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| `message_id` | integer or array | — | One message ID (`message_id=2001`) or several (`message_id[]=2001&message_id[]=2004`). |
| `group_id` | integer or array | — | One message folder ID or several. Messages filed in the folder or in any of its subfolders, at any depth, are included, as in the folder report in Envoke. |
| `tag_id` | integer | — | Only messages sent to this tag. One value. |
| `segment_id` | integer | — | Only messages sent to this segment. One value. |
| `subscription_id` | integer | — | Only messages sent to this subscription. One value. The account's mandatory subscription is not returned by the Subscription API, so messages sent to it are selected by `message_id`, `group_id` or date unless you know its ID from the app. |
| `type` | string | *(both)* | `campaign` or `automated`. |
| `include_in_progress` | integer | `0` | `1` to include campaigns still sending. |
| `sent_from` | string | — | Only messages whose `sent_time` is on or after this date or time. See [Date parameters](https://envoke-developers.pages.dev/v2/reports/fields/#date-parameters). |
| `sent_to` | string | — | Only messages whose `sent_time` is on or before this date or time. |
| `activity_from` | string | — | Count only activity on or after this date or time. |
| `activity_to` | string | — | Count only activity on or before this date or time. |
| `limit` | integer | `10` | Maximum number of rows; values above `100` return 100. |
| `skip` | integer | `0` | Number of rows to skip. |
The filters combine with AND. Any other parameter returns HTTP 400 (`Parameter 'segment' is not valid`); see [Unknown parameters](https://envoke-developers.pages.dev/v2/reports/fields/#unknown-parameters).
### Recipient list filters
`tag_id`, `segment_id`, and `subscription_id` select messages by the list recorded on the message when it was sent, not by the contacts who are on that list today. They match the same values the Message API returns in `recipient_tag`, `recipient_segment`, and `recipient_subscription` for the message:
| Filter | Matches a message whose |
|---|---|
| `subscription_id=514` | `recipient_subscription` is that subscription, with or without a tag or segment narrowing it. |
| `tag_id=512` | `recipient_tag` is that tag, alone or narrowed by a subscription. |
| `segment_id=77` | `recipient_segment` is that segment. |
A message sent to several tags at once is recorded against an internal segment that no API returns, so it matches neither `tag_id` for one of those tags nor any `segment_id`. Automated messages are usually not sent to a list and match none of the three.
**Names in the rows, IDs in the filters.** `recipient_subscription`, `recipient_tag`, and `recipient_segment` in the rows are the **names** of the lists recorded on the message, while the filters `tag_id`, `subscription_id`, and `segment_id` take **IDs**. The IDs and current names of tags and subscriptions come from `GET https://e1.envoke.com/v2/tags` ([Tag API](https://envoke-developers.pages.dev/v2/tags/read/)) and `GET https://e1.envoke.com/v2/subscriptions` ([Subscription API](https://envoke-developers.pages.dev/v2/subscriptions/read/)); segment IDs come from the segments in the Envoke app. A message whose list was deleted since it was sent can still show the list's name, but the deleted list is no longer returned by those APIs, so there is no ID to filter it by.
### Date window
`sent_from` and `sent_to` choose **which messages** are listed, by `sent_time`. `activity_from` and `activity_to` choose **which activity is counted**. They can be combined:
| Request | Result |
|---|---|
| no date parameter | Every message, lifetime counts. |
| `sent_from=2026-04-01&sent_to=2026-04-30` | Messages launched in April, lifetime counts. |
| `activity_from=2026-05-01&activity_to=2026-05-31` | Every matching message launched by the end of May, counting only May's activity. A message launched in April appears with `sends` `0` and its May opens and clicks. |
| `sent_from=2026-04-01&sent_to=2026-04-30&activity_from=2026-05-01&activity_to=2026-05-31` | Messages launched in April, counting only their May activity. |
For an automated message `sent_time` is the date it was set up or last activated, so `sent_from` and `sent_to` select automated messages by that date, not by when contacts received them. To report on what an automated message sent in a period, use `message_id` with `activity_from` and `activity_to`.
Inside an activity window `delivered` counts the contacts sent the message inside the window who have no bounce for it, so it is never negative and is not the window's `sends − bounces`; see [`delivered`](https://envoke-developers.pages.dev/v2/reports/fields/#delivered).
### Ordering and pagination
Rows are ordered by `sent_time`, newest first, and then by `id`, highest first, so messages launched in the same second always come back in the same order and pages never overlap. Messages with no `sent_time` come last. The order cannot be changed, and the order of `message_id[]` values in the request is not kept.
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.
### Response fields
| Field | Type | Description |
|---|---|---|
| `id` | string | Message ID; the Message API `id`. |
| `name` | string | Internal name. |
| `subject` | string or null | Stored subject line; `null` for a subject-line A/B test, and for a message with no stored subject (then `has_variants` is `false`). Can be the placeholder `{@ab_test}` (see [Messages](https://envoke-developers.pages.dev/v2/reports/fields/#messages)). |
| `type` | string | `campaign` or `automated`. |
| `sent_time` | string or null | Launch time, `YYYY-MM-DD HH:MM:SS`, account time zone. |
| `recipient_subscription` | string | The name of the subscription the message was sent to, or `""`. The same value the Message API returns. Filter by the subscription's ID with `subscription_id`. |
| `recipient_tag` | string | The name of the tag the message was sent to, or `""`. `""` when the message was sent to a subscription alone. Filter by the tag's ID with `tag_id`. |
| `recipient_segment` | string | The name of the segment the message was sent to, or `""`. Filter by the segment's ID with `segment_id`. |
| `has_variants` | boolean | `true` for an A/B test. |
| `sends` … `skips` | integer | The ten [metrics](https://envoke-developers.pages.dev/v2/reports/fields/#fields), in glossary order. |
See [Messages](https://envoke-developers.pages.dev/v2/reports/fields/#messages) in the fields reference for `subject`, `type`, and `sent_time` in detail.
### Response
```json
[
{
"id": "2004",
"name": "Pirates Opening Night",
"subject": null,
"type": "campaign",
"sent_time": "2026-05-14 09:30:00",
"recipient_subscription": "Season News",
"recipient_tag": "",
"recipient_segment": "",
"has_variants": true,
"sends": 386,
"delivered": 379,
"bounces": 7,
"hard_bounces": 4,
"soft_bounces": 3,
"opens": 201,
"clicks": 64,
"unsubscribes": 2,
"spam_complaints": 0,
"skips": 3
},
{
"id": "2001",
"name": "Spring Season Announcement",
"subject": "Our spring season is here, {fname}",
"type": "campaign",
"sent_time": "2026-04-15 10:00:00",
"recipient_subscription": "Season News",
"recipient_tag": "",
"recipient_segment": "",
"has_variants": false,
"sends": 412,
"delivered": 403,
"bounces": 9,
"hard_bounces": 5,
"soft_bounces": 4,
"opens": 188,
"clicks": 57,
"unsubscribes": 4,
"spam_complaints": 1,
"skips": 6
},
{
"id": "2002",
"name": "Auditions Open",
"subject": "Auditions for The Pirates of Penzance",
"type": "campaign",
"sent_time": "2026-03-10 18:00:00",
"recipient_subscription": "",
"recipient_tag": "Cast Call",
"recipient_segment": "",
"has_variants": false,
"sends": 64,
"delivered": 62,
"bounces": 2,
"hard_bounces": 1,
"soft_bounces": 1,
"opens": 47,
"clicks": 21,
"unsubscribes": 0,
"spam_complaints": 0,
"skips": 1
}
]
```
---
## Errors
Invalid parameters return HTTP 400 with the validation envelope (see [Error Responses](https://envoke-developers.pages.dev/v2/reports/fields/#error-responses)). Besides the shared date, `limit`, `skip`, and unknown-parameter errors, the list returns:
| Error | Cause |
|---|---|
| `Invalid 'message_id': 'abc'` | A `message_id` value that is not a whole number. The same form for `group_id`, `tag_id`, `segment_id`, and `subscription_id`. |
| `Parameter 'tag_id' accepts one value` | A single-value parameter sent as an array (`tag_id[]=512`, `limit[]=5`, `type[]=campaign`): every parameter except `message_id` and `group_id`. |
| `Invalid 'type': 'nurture' (possible values: 'campaign', 'automated')` | `type` is not `campaign` or `automated`. |
| `Invalid 'include_in_progress': 'yes' (possible values: '0', '1')` | `include_in_progress` is not `0` or `1`. |
A filter value that is a whole number but matches nothing (an unknown folder, a tag in another account) is not an error: the response is `[]`.
---
## Examples
### The ten most recent messages
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages"
```
### Second page of 25
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages?limit=25&skip=25"
```
### Lifetime counts for two messages
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages?message_id%5B%5D=2001&message_id%5B%5D=2004"
```
The rows come back newest first, not in the order the IDs were given.
### Messages launched in April
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages?sent_from=2026-04-01&sent_to=2026-04-30&limit=100"
```
### May activity for one message
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages?message_id=2001&activity_from=2026-05-01&activity_to=2026-05-31"
```
Response (2001 was sent in April; only its May activity is counted, so `sends` is `0` and so is `delivered`, which counts only contacts sent the message in May):
```json
[
{
"id": "2001",
"name": "Spring Season Announcement",
"subject": "Our spring season is here, {fname}",
"type": "campaign",
"sent_time": "2026-04-15 10:00:00",
"recipient_subscription": "Season News",
"recipient_tag": "",
"recipient_segment": "",
"has_variants": false,
"sends": 0,
"delivered": 0,
"bounces": 0,
"hard_bounces": 0,
"soft_bounces": 0,
"opens": 12,
"clicks": 3,
"unsubscribes": 1,
"spam_complaints": 0,
"skips": 0
}
]
```
### Automated messages only
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages?type=automated"
```
Response:
```json
[
{
"id": "2006",
"name": "Welcome to Maple Ridge",
"subject": "Welcome to Maple Ridge Community Theatre, {fname}",
"type": "automated",
"sent_time": "2025-09-02 08:00:00",
"recipient_subscription": "",
"recipient_tag": "",
"recipient_segment": "",
"has_variants": false,
"sends": 1284,
"delivered": 1261,
"bounces": 23,
"hard_bounces": 15,
"soft_bounces": 8,
"opens": 702,
"clicks": 233,
"unsubscribes": 11,
"spam_complaints": 2,
"skips": 40
}
]
```
### Messages in one folder
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages?group_id=31"
```
The rows include the messages filed in the folder's subfolders.
### Messages sent to one tag
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages?tag_id=512"
```
### Messages launched in December 2021
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages?sent_from=2021-12-01&sent_to=2021-12-31"
```
Response (every metric is available whatever the message's age; only the single report's `email_clients` is `null` for messages sent before June 2022):
```json
[
{
"id": "2003",
"name": "Holiday Gala Invitation",
"subject": "You're invited to the Holiday Gala",
"type": "campaign",
"sent_time": "2021-12-01 10:00:00",
"recipient_subscription": "Season News",
"recipient_tag": "",
"recipient_segment": "",
"has_variants": false,
"sends": 355,
"delivered": 349,
"bounces": 6,
"hard_bounces": 4,
"soft_bounces": 2,
"opens": 171,
"clicks": 38,
"unsubscribes": 3,
"spam_complaints": 0,
"skips": 12
}
]
```
### Including campaigns still sending
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages?type=campaign&include_in_progress=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).*
---
URL: https://envoke-developers.pages.dev/v2/reports/message-stats/
Markdown: https://envoke-developers.pages.dev/v2/reports/message-stats.md
# Reports API - Message Stats
Use the message stats reports to retrieve the full report for one message (totals, rates, bounce and preference breakdowns, links, email clients, unsubscribe reasons, polls, and A/B variants), or to compare several messages or a whole folder side by side with a combined total.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| GET | `https://e1.envoke.com/v2/reports/messages/{ID}` | Return the full report for one message. |
| GET | `https://e1.envoke.com/v2/reports/messages/compare` | Return totals and rates for several messages, or for every message in a folder, plus a combined block. |
Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## Get a Single Message Report
```
GET https://e1.envoke.com/v2/reports/messages/{ID}
```
Returns one JSON object: the message, its ten [metrics](https://envoke-developers.pages.dev/v2/reports/fields/#fields) in `totals`, its six [rates](https://envoke-developers.pages.dev/v2/reports/fields/#rates) in `rates`, and the breakdowns below. Replace `{ID}` with the Envoke message ID. The `totals` are the same numbers the [message activity list](https://envoke-developers.pages.dev/v2/reports/messages/) returns for the message with the same activity window. To list the contacts behind a total, a bounce reason, or a link, use the [message contacts report](https://envoke-developers.pages.dev/v2/reports/message-contacts/).
The report is available for 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 (a draft or a scheduled message) returns `[]` with HTTP 200.
### Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| `activity_from` | string | — | Count only activity on or after this date or time. See [Date parameters](https://envoke-developers.pages.dev/v2/reports/fields/#date-parameters). |
| `activity_to` | string | — | Count only activity on or before this date or time. |
Every count in the report, including the breakdowns, `links`, and `email_clients`, uses the window. `unsubscribe_reasons` and `polls` describe contacts as they are now and cannot be limited to a window, so when `activity_from` or `activity_to` is given both are `null` (see below). Any other parameter, including `sent_from` and `sent_to`, returns HTTP 400 (`Parameter 'sent_from' is not valid`).
### Response fields
| Field | Type | Description |
|---|---|---|
| `id`, `name`, `subject`, `type`, `sent_time`, `has_variants` | | As in the [message activity list](https://envoke-developers.pages.dev/v2/reports/messages/#response-fields). |
| `totals` | object | The ten metrics. |
| `rates` | object | The six rates. |
| `bounces` | array | Contacts per bounce reason. |
| `preference_changes` | array | Contacts per preference change. |
| `links` | array | Contacts per tracked link. |
| `email_clients` | object or null | Open records (not contacts) per email client. `null` for messages sent before June 2022. |
| `unsubscribe_reasons` | array or null | Contacts per reason given when unsubscribing. `null` when an activity window is given. |
| `polls` | array or null | Answers to each poll in the message; `[]` when the message has none. `null` when an activity window is given. |
| `variants` | array | One object per A/B version, always present when `has_variants` is `true`; `[]` when the message is not an A/B test, and when its A/B block no longer exists. |
### `bounces`
One entry per bounce reason, always all six, in this order. `contacts` is the number of distinct contacts with that reason:
| `type` | `label` | Codes |
|---|---|---|
| `hard_bounce` | `Hard bounce` | `2/12` |
| `soft_bounce` | `Soft bounce` | `2/11` |
| `deferred` | `Temporarily deferred` | `2/14` |
| `rejected` | `Rejected` | `2/15` |
| `invalid_address` | `Invalid email address` | `2/16` |
| `other` | `Other` | `2/10` (SMTP error) and the retired `2/13` |
A contact with two bounce reasons is in two entries, so the entries can add up to more than `totals.bounces`. On a few messages sent before 2015, bounce records with a retired sub-type count in `totals.bounces` but appear in no entry, and no `bounce_type` of the [message contacts report](https://envoke-developers.pages.dev/v2/reports/message-contacts/) selects them. `hard_bounces` covers `hard_bounce` and `invalid_address`; `soft_bounces` covers `soft_bounce`, `deferred`, `rejected`, and the SMTP-error part of `other`. The `type` values are stable keys; `label` is English display text and may change.
The contacts behind an entry: [message contacts](https://envoke-developers.pages.dev/v2/reports/message-contacts/) with `event=bounced&bounce_type={type}`.
### `preference_changes`
One entry per kind of preference change made from the message, always all six, in this order:
| `type` | `label` | Codes |
|---|---|---|
| `revoked_consent` | `Revoked consent` | `5/11` |
| `one_click_unsubscribe` | `One-click unsubscribe` | `5/30` |
| `spam_reported` | `Spam reported` | `5/12` and `5/13` |
| `subscription_changed` | `Subscription changed` | `5/17` |
| `language_changed` | `Language changed` | `5/16` |
| `express_consent_granted` | `Express consent granted` | `5/21` |
`totals.unsubscribes` is the distinct contacts across `revoked_consent` and `one_click_unsubscribe`; `totals.spam_complaints` equals `spam_reported`. Each entry equals the Sent message report row with the same label, except `spam_reported`, which combines two report rows. A one-click unsubscribe on a message sent with a primary subscription removes the contact from that subscription but records only `one_click_unsubscribe`, not `subscription_changed`. Nurture campaign changes (`5/18`) are not reported.
The contacts behind `revoked_consent` and `one_click_unsubscribe` together: [message contacts](https://envoke-developers.pages.dev/v2/reports/message-contacts/) with `event=unsubscribed`; behind `spam_reported`: `event=spam_reported`. The other three entries have no contact list.
### `links`
One entry per tracked link in the message with at least one click (inside the window, when one is given), most clicked first; a link nobody clicked has no entry:
| Field | Type | Description |
|---|---|---|
| `id` | string | The tracked link's ID. |
| `url` | string | The link as written in the message, with Envoke's tracking parameters (`utm_*`, `eid`) removed; `""` for the view-online and preferences links. Merge tags are not replaced, so `url` can be or contain a merge tag (`{@customfield-url_test}`) rather than a valid URL. |
| `title` | string | The link's anchor text, or a shortened URL when it has none, cut at 60 bytes (fewer characters when the text has accented or other non-ASCII characters); the view-online and preferences links have a fixed title. |
| `type` | string | `link` for a link in the content, including links to pages with Envoke forms; `view` for the view-online link; `pref` for the preferences and unsubscribe links. |
| `clicks` | integer | Distinct contacts who clicked this link. |
A contact who clicked two links is in both entries, so the entries can add up to more than `totals.clicks`. Poll answers are tracked as clicks and counted in `totals.clicks`, but are reported under `polls`, not here. When the same URL appears twice in a message, each copy has its own entry; the Sent message report merges them into one row whose count is the sum of the entries.
The contacts behind an entry: [message contacts](https://envoke-developers.pages.dev/v2/reports/message-contacts/) with `event=clicked&link_id={id}`.
### `email_clients`
The email client panel of the Sent message report, from the open records' user agents. Every figure in this block counts **open records**, one per contact per email client, not distinct contacts, so `known_opens` can be larger than `totals.opens`. It is the only part of the report that counts records rather than contacts.
| Field | Type | Description |
|---|---|---|
| `known_opens` | integer | Open records whose email client was recorded. |
| `machine_open_records` | integer | Open records made by Apple Mail Privacy Protection (user agent `Mozilla/5.0`), which Envoke classifies as machine opens. A contact has at most one such record per message, so this also equals the number of contacts with at least one machine open, including contacts who also opened in another email client. There is no machine-opens figure in `totals`. |
| `by_app` | array | `{ "name", "opens" }` per email client, always five entries: `Apple Mail` (including the machine-open records), `Gmail`, `Outlook`, and `Yahoo`, most opens first (ties in that order), then `Other` for every client Envoke does not recognise, always last and `0` when every client was recognised. |
`email_clients` is `null` for a message sent before 1 June 2022, when Envoke began recording user agents. Opens recorded with no user agent are in neither `known_opens` nor `by_app`, so `known_opens` can be smaller than the report's Opens row.
### `unsubscribe_reasons`
One entry per reason, in the order the preferences page lists them, for reasons at least one contact chose:
| Field | Type | Description |
|---|---|---|
| `reason` | string | `no_longer_interested`, `never_subscribed`, `too_many_emails`, `irrelevant`, or `other`. |
| `contacts` | integer | Contacts whose current consent was revoked from this message with this reason. |
This counts contacts **as they are now**: a contact who unsubscribed from this message and later gave consent again is no longer counted. Because it describes the present rather than activity at a time, `unsubscribe_reasons` is `null` when `activity_from` or `activity_to` is given. Giving a reason is optional, so the entries usually add up to less than `totals.unsubscribes`.
### `polls`
One entry per poll in the message; `[]` when it has none, and `null` when an activity window is given.
| Field | Type | Description |
|---|---|---|
| `id` | string | The poll's ID. |
| `name` | string | The poll question. |
| `anonymous` | boolean | `true` for an anonymous poll. The counts are still given; the contacts are not identifiable in Envoke. |
| `answers` | array | `{ "id", "text", "contacts" }` per answer, in the order the Sent message report lists them: by the answer's sort position, then alphabetically by text. Envoke stores poll answers without a sort position, so this is normally alphabetical, not the order of the answers in the message. `contacts` is the number of contacts who currently hold that answer. |
Poll answers are stored on the contact, so the counts describe contacts **as they are now**, not activity: every contact in the account who holds the answer today is counted, however they came to hold it. For that reason `polls` is `null` when `activity_from` or `activity_to` is given.
### A/B tests
For an A/B test, `has_variants` is `true`, the message's `subject` is `null` for a subject-line test, `totals` and `rates` cover every version together (including the contacts sent the winning version after the test), and `variants` has one object per version, each with its own subject or sender. `variants` is always included when `has_variants` is `true`; there is no parameter to request it or leave it out. It is `[]` when the message's A/B block no longer exists, although `has_variants` is still `true`. For a test that does not vary the subject (preheader, sender, or content), `subject` is the stored subject, which can be the literal placeholder `{@ab_test}` (see [Messages](https://envoke-developers.pages.dev/v2/reports/fields/#messages)).
| Field | Type | Description |
|---|---|---|
| `id` | string | The version's ID. |
| `name` | string | `A`, `B`, `C`, ... in the order of the versions, as in the Sent message report's tabs. |
| `test_type` | string | What the test varies: `subject`, `preheader`, `sender`, or `content`. |
| `subject` | string or null | This version's subject line, for a subject test; otherwise `null`. |
| `sender` | string or null | This version's sender as `Name `, for a sender test; otherwise `null`. |
| `winner` | boolean | `true` for the version Envoke chose as the winner; `false` for every version while the test is running, and when the test ended without a winner being recorded. |
| `totals` | object | The ten metrics for the contacts who were sent this version during the test. |
| `rates` | object | The six rates for those contacts. |
Each version counts the contacts sent that version, as the Sent message report's version tabs do: the contacts assigned to it during the test phase and, for the winning version, the contacts sent the winner after the test. So the winner's `sends` is usually the larger figure, and the versions' `sends` usually add up to `totals.sends`. They can add up to less, because contacts sent the message outside the versions' assignment (overrides and resends, for example) are in `totals` but in no version, and to more on old tests where a contact was recorded under two versions. A version's metrics relate to the Version A or Version B tab of the Sent message report (`.variant_stats[n].stats` in `stats.json`) exactly as the message's `totals` relate to the Combined tab: `sends` and `clicks` are equal, `opens` and `bounces` are less than or equal, and so on per the [glossary](https://envoke-developers.pages.dev/v2/reports/fields/#field-details).
### Response
```json
{
"id": "2004",
"name": "Pirates Opening Night",
"subject": null,
"type": "campaign",
"sent_time": "2026-05-14 09:30:00",
"has_variants": true,
"totals": {
"sends": 386,
"delivered": 379,
"bounces": 7,
"hard_bounces": 4,
"soft_bounces": 3,
"opens": 201,
"clicks": 64,
"unsubscribes": 2,
"spam_complaints": 0,
"skips": 3
},
"rates": {
"delivered": 0.9819,
"open": 0.5303,
"click": 0.1689,
"click_to_open": 0.3184,
"bounce": 0.0181,
"unsubscribe": 0.0052
},
"bounces": [
{ "type": "hard_bounce", "label": "Hard bounce", "contacts": 3 },
{ "type": "soft_bounce", "label": "Soft bounce", "contacts": 1 },
{ "type": "deferred", "label": "Temporarily deferred", "contacts": 1 },
{ "type": "rejected", "label": "Rejected", "contacts": 0 },
{ "type": "invalid_address", "label": "Invalid email address", "contacts": 1 },
{ "type": "other", "label": "Other", "contacts": 1 }
],
"preference_changes": [
{ "type": "revoked_consent", "label": "Revoked consent", "contacts": 1 },
{ "type": "one_click_unsubscribe", "label": "One-click unsubscribe", "contacts": 1 },
{ "type": "spam_reported", "label": "Spam reported", "contacts": 0 },
{ "type": "subscription_changed", "label": "Subscription changed", "contacts": 1 },
{ "type": "language_changed", "label": "Language changed", "contacts": 0 },
{ "type": "express_consent_granted", "label": "Express consent granted", "contacts": 3 }
],
"links": [
{ "id": "88125", "url": "https://example.org/tickets/pirates", "title": "Get your tickets", "type": "link", "clicks": 39 },
{ "id": "88126", "url": "https://example.org/shows/pirates-of-penzance", "title": "About the show", "type": "link", "clicks": 18 },
{ "id": "88129", "url": "", "title": "Envoke Preferences / Unsubscribe", "type": "pref", "clicks": 4 },
{ "id": "88128", "url": "", "title": "Envoke View Message in Browser", "type": "view", "clicks": 3 }
],
"email_clients": {
"known_opens": 214,
"machine_open_records": 47,
"by_app": [
{ "name": "Apple Mail", "opens": 88 },
{ "name": "Gmail", "opens": 64 },
{ "name": "Outlook", "opens": 41 },
{ "name": "Yahoo", "opens": 7 },
{ "name": "Other", "opens": 14 }
]
},
"unsubscribe_reasons": [
{ "reason": "too_many_emails", "contacts": 1 }
],
"polls": [
{
"id": "61",
"name": "Which performance will you attend?",
"anonymous": false,
"answers": [
{ "id": "7301", "text": "Friday 7:30 p.m.", "contacts": 48 },
{ "id": "7302", "text": "Saturday matinee", "contacts": 31 },
{ "id": "7303", "text": "Saturday 7:30 p.m.", "contacts": 22 }
]
}
],
"variants": [
{
"id": "5121",
"name": "A",
"test_type": "subject",
"subject": "Opening night: The Pirates of Penzance",
"sender": null,
"winner": false,
"totals": {
"sends": 96,
"delivered": 94,
"bounces": 2,
"hard_bounces": 1,
"soft_bounces": 1,
"opens": 49,
"clicks": 14,
"unsubscribes": 1,
"spam_complaints": 0,
"skips": 0
},
"rates": {
"delivered": 0.9792,
"open": 0.5213,
"click": 0.1489,
"click_to_open": 0.2857,
"bounce": 0.0208,
"unsubscribe": 0.0104
}
},
{
"id": "5122",
"name": "B",
"test_type": "subject",
"subject": "Curtain up on Pirates, {fname}!",
"sender": null,
"winner": true,
"totals": {
"sends": 97,
"delivered": 95,
"bounces": 2,
"hard_bounces": 1,
"soft_bounces": 1,
"opens": 58,
"clicks": 19,
"unsubscribes": 0,
"spam_complaints": 0,
"skips": 0
},
"rates": {
"delivered": 0.9794,
"open": 0.6105,
"click": 0.2,
"click_to_open": 0.3276,
"bounce": 0.0206,
"unsubscribe": 0
}
}
]
}
```
### Errors
| Response | Cause |
|---|---|
| `[]` with HTTP 200 | The message does not exist, belongs to another account, is deleted, or has not started sending. |
| HTTP 400, `Parameter 'sent_from' is not valid` | A parameter other than `activity_from` and `activity_to`. |
| HTTP 400, `Invalid 'activity_from': '...' (expected YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)` | A bad date (see [Date parameters](https://envoke-developers.pages.dev/v2/reports/fields/#date-parameters)). |
| HTTP 404, `404 Not Found` | `{ID}` is not a number. |
---
## Compare Messages
```
GET https://e1.envoke.com/v2/reports/messages/compare
```
Returns the totals and rates of several messages side by side, and a `combined` block for the set. Give the set either as message IDs or as a folder:
- `message_id[]=2001&message_id[]=2004` — the listed messages. Each must be a sent campaign or an automated message in your account. A repeated ID counts once: the message appears once and `message_count` counts it once.
- `group_id=31` — every sent campaign and automated message filed in the folder or in any of its subfolders, at any depth, as in the folder report in Envoke.
The per-message `totals` and `rates` are the same numbers the [single report](#get-a-single-message-report) and the [message activity list](https://envoke-developers.pages.dev/v2/reports/messages/) return for each message with the same activity window.
With an activity window, messages launched after `activity_to` are left out of the set, exactly as the list leaves them out: a folder's messages launched later are in neither `messages` nor `combined`, and a `message_id` launched later is dropped without an error.
### Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| `message_id` | integer or array | — | One or more message IDs, at most 100. Not with `group_id`. |
| `group_id` | integer | — | One message folder ID; its subfolders are included. Not with `message_id`. |
| `activity_from` | string | — | Count only activity on or after this date or time. |
| `activity_to` | string | — | Count only activity on or before this date or time. |
Exactly one of `message_id` and `group_id` is required. Any other parameter returns HTTP 400.
### The combined block
`combined.totals` counts **distinct contacts across the whole set**: a contact sent two of the messages counts once in `sends`, a contact who opened both counts once in `opens`. So `combined.totals.sends` is the number of different people the set reached, and it is less than or equal to the sum of the messages' `sends`. The same holds for every metric:
| Metric | Combined value |
|---|---|
| `sends`, `bounces`, `hard_bounces`, `soft_bounces`, `opens`, `clicks`, `unsubscribes`, `spam_complaints`, `skips` | Contacts with a matching record for any message in the set. |
| `delivered` | Lifetime: combined `sends` − combined `bounces`. With an activity window: contacts who, for at least one message in the set, were sent it inside the window and have no bounce for it (see [`delivered`](https://envoke-developers.pages.dev/v2/reports/fields/#delivered)); never negative. |
`combined.rates` are computed from `combined.totals` with the formulas in [Rates](https://envoke-developers.pages.dev/v2/reports/fields/#rates), so they are rates of the set's reach, not averages of the messages' rates. `combined.message_count` is the number of messages in the set, after the launch cut-off when an activity window is given.
A/B tests count as one message each; their versions are not shown.
### Response
`messages` is in the same order as the [message activity list](https://envoke-developers.pages.dev/v2/reports/messages/#ordering-and-pagination): newest `sent_time` first, then highest `id`.
```json
{
"messages": [
{
"id": "2004",
"name": "Pirates Opening Night",
"sent_time": "2026-05-14 09:30:00",
"totals": {
"sends": 386,
"delivered": 379,
"bounces": 7,
"hard_bounces": 4,
"soft_bounces": 3,
"opens": 201,
"clicks": 64,
"unsubscribes": 2,
"spam_complaints": 0,
"skips": 3
},
"rates": {
"delivered": 0.9819,
"open": 0.5303,
"click": 0.1689,
"click_to_open": 0.3184,
"bounce": 0.0181,
"unsubscribe": 0.0052
}
},
{
"id": "2001",
"name": "Spring Season Announcement",
"sent_time": "2026-04-15 10:00:00",
"totals": {
"sends": 412,
"delivered": 403,
"bounces": 9,
"hard_bounces": 5,
"soft_bounces": 4,
"opens": 188,
"clicks": 57,
"unsubscribes": 4,
"spam_complaints": 1,
"skips": 6
},
"rates": {
"delivered": 0.9782,
"open": 0.4665,
"click": 0.1414,
"click_to_open": 0.3032,
"bounce": 0.0218,
"unsubscribe": 0.0097
}
}
],
"combined": {
"message_count": 2,
"totals": {
"sends": 702,
"delivered": 686,
"bounces": 16,
"hard_bounces": 9,
"soft_bounces": 7,
"opens": 331,
"clicks": 108,
"unsubscribes": 6,
"spam_complaints": 1,
"skips": 9
},
"rates": {
"delivered": 0.9772,
"open": 0.4825,
"click": 0.1574,
"click_to_open": 0.3263,
"bounce": 0.0228,
"unsubscribe": 0.0085
}
}
}
```
Here 96 contacts were sent both messages, so combined `sends` is 702, not 798, and the combined `open` rate (0.4825) is not the average of the two messages' rates.
A folder that exists but holds no sent or automated messages returns `messages` `[]` and a `combined` block with `message_count` `0`, every total `0`, and every rate `null`.
### Errors
Invalid requests return HTTP 400 with the validation envelope. Unknown message IDs are all listed:
```json
{
"success": false,
"result_data": {
"errors": [
"No message found for 'message_id': 2098",
"No message found for 'message_id': 2099"
]
},
"result_text": "Validation failed, see 'result_data' -> 'errors'"
}
```
| Error | Cause |
|---|---|
| `One of the parameters 'message_id' or 'group_id' must be provided` | Neither was given. |
| `Only one of the parameters 'message_id' or 'group_id' may be provided` | Both were given. |
| `No message found for 'message_id': 2099` | The message does not exist, belongs to another account, is deleted, or is not a sent campaign or automated message. One error per ID. |
| `No message group found for 'group_id': 39` | The folder does not exist or belongs to another account. |
| `Too many messages to compare: 140 (maximum 100)` | More than 100 message IDs, or a folder holding, with its subfolders, more than 100 sent and automated messages (counted before the activity window's launch cut-off). |
| `Invalid 'message_id': 'abc'` | A `message_id` or `group_id` value that is not a whole number. |
| `Parameter 'group_id' accepts one value` | `group_id` sent as an array. |
| `Parameter 'sent_from' is not valid` | Any parameter other than those above. |
The shared date errors also apply.
---
## Examples
### Full report for one message
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages/2004"
```
### One message's activity in its first week
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages/2004?activity_from=2026-05-14&activity_to=2026-05-20"
```
With a window, `unsubscribe_reasons` and `polls` are `null`; every other part of the report counts only that week's activity.
### Compare two messages
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages/compare?message_id%5B%5D=2001&message_id%5B%5D=2004"
```
### Every message in a folder, May activity only
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages/compare?group_id=31&activity_from=2026-05-01&activity_to=2026-05-31"
```
### A message that is not found
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/messages/2099"
```
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).*
---
URL: https://envoke-developers.pages.dev/v2/reports/message-contacts/
Markdown: https://envoke-developers.pages.dev/v2/reports/message-contacts.md
# 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.
### 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).*
---
URL: https://envoke-developers.pages.dev/v2/reports/contacts/
Markdown: https://envoke-developers.pages.dev/v2/reports/contacts.md
# Reports API - Contact Activity
Use the contact activity report to retrieve one contact's email history: which messages they were sent and what they did with each one (delivered, bounced, opened, clicked which links, unsubscribed), with a summary across all of them.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| GET | `https://e1.envoke.com/v2/reports/contacts/{ID}` | Return the activity of the contact with this Envoke contact ID. |
| GET | `https://e1.envoke.com/v2/reports/contacts?email={email}` | Return the activity of the contact with this email address. |
| GET | `https://e1.envoke.com/v2/reports/contacts?remote_id={remote_id}` | Return the activity of the contact with this remote ID. |
Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## Get a Contact's Activity
```
GET https://e1.envoke.com/v2/reports/contacts/{ID}
```
Returns one JSON object with three parts: `contact` (who), `summary` (counts across every message), and `messages` (one entry per message, newest first, paged with `limit` and `skip`). Replace `{ID}` with the Envoke contact ID, the `id` of the [Contact API](https://envoke-developers.pages.dev/v1/contacts/fields/).
A contact that does not exist, belongs to another account, or has been deleted returns `[]` with HTTP 200. A contact who has never been sent a message returns the object with every count `0`, every time `null`, and `messages` `[]`.
This report covers email messages only: form submissions and other contact events are not included.
**Accounts restricted from contact-level email activity.** On an account restricted from seeing contacts associated with email activity (the setting that hides a contact's Email sending history in Envoke), this endpoint and [Find a Contact's Activity](#find-a-contacts-activity-by-email-or-remote-id) return HTTP 403; see [Errors](#errors). The message reports are not affected.
### Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| `activity_from` | string | — | Count only activity on or after this date or time. See [Date parameters](https://envoke-developers.pages.dev/v2/reports/fields/#date-parameters). |
| `activity_to` | string | — | Count only activity on or before this date or time. |
| `limit` | integer | `10` | Maximum number of entries in `messages`; values above `100` return 100. |
| `skip` | integer | `0` | Number of `messages` entries to skip. |
`limit` and `skip` apply only to the `messages` array; `contact` and `summary` are always complete. Any other parameter, including `sent_from` and `sent_to`, returns HTTP 400 (`Parameter 'sent_from' is not valid`).
---
## Find a Contact's Activity by Email or Remote ID
```
GET https://e1.envoke.com/v2/reports/contacts?email={email}
GET https://e1.envoke.com/v2/reports/contacts?remote_id={remote_id}
```
Returns the same object as [Get a Contact's Activity](#get-a-contacts-activity) for the contact found by `email` or by `remote_id`. Give exactly one of the two, plus any of `activity_from`, `activity_to`, `limit`, and `skip`. URL-encode the value (`@` is `%40`).
The value is compared as a whole, exactly as the Contact API's `filter[email]` and `filter[remote_id]` compare it, so a value the Contact API finds is found here. Letter case does not matter: `MAYA.CHEN@EXAMPLE.ORG` finds the contact `maya.chen@example.org`, and a remote ID is matched the same way. An empty value is an error rather than a match for every contact without an email address or remote ID. When no contact matches, the response is `[]` with HTTP 200. When more than one contact matches, the request fails with HTTP 400 rather than choosing one; use the contact's `id` instead. More than one match is possible for `remote_id`, and for `email` on accounts that allow several contacts to share an address.
---
## Response
### `contact`
| Field | Type | Description |
|---|---|---|
| `id` | string | Envoke contact ID. |
| `email` | string | The contact's email address. |
| `remote_id` | string | The contact's remote ID, or `""` when it has none. |
### `summary`
Counts of **messages**, across every message in the report (not only the current page of `messages`), inside the activity window when one is given:
| Field | Type | Description |
|---|---|---|
| `sends` | integer | Messages the contact was sent. |
| `delivered` | integer | Messages with `delivered` `true`. |
| `bounces` | integer | Messages with `bounced` `true`. |
| `opens` | integer | Messages with `opened` `true`. |
| `clicks` | integer | Messages with `clicked` `true`. |
| `unsubscribes` | integer | Messages with `unsubscribed` `true`. |
| `last_send_time` | string or null | The most recent time the contact was sent a message. |
| `last_open_time` | string or null | The most recent open record. |
| `last_click_time` | string or null | The most recent click record. |
Envoke records a contact's first open of a message in each email client and their first click on each link, never a repeat (see [Times and time zone](https://envoke-developers.pages.dev/v2/reports/fields/#times-and-time-zone)). So `last_open_time` is the latest of those first opens: a contact who reopens an old message in the same email client does not move it, but opening a new message, or the same message in another client, does. `last_click_time` works the same way for links.
### `messages`
One entry per message the contact was sent, ordered by `sent_time`, newest first, then by message `id`, highest first. A message is included when the contact has a send record for it and, when an activity window is given, at least one activity record for it inside the window. Messages the contact was selected for but skipped (no consent, invalid address) are not included. A message is listed whatever its status today, as on the contact page: a transactional message, or a message sent and later returned to draft, appears here even though the message reports do not answer for it.
| Field | Type | Description |
|---|---|---|
| `id` | string | Message ID; the Message API `id`. |
| `name` | string | The message's internal name. |
| `sent_time` | string | When this contact was sent the message. For an automated message, the time of this contact's send, not the message's set-up date. |
| `delivered` | boolean | `true` when the contact was sent the message and has no bounce record for it. With an activity window, the send must be inside the window; a bounce counts whenever it arrived. |
| `bounced` | boolean | `true` when the contact has any bounce record for the message, including a temporary deferral. |
| `opened` | boolean | `true` when the contact opened the message. |
| `first_open_time` | string or null | The contact's first open of the message, or `null`. |
| `opens` | integer | The number of email clients the contact opened the message in; not the number of times they opened it. |
| `clicked` | boolean | `true` when the contact clicked any tracked link in the message. |
| `clicks` | integer | The number of different links the contact clicked; not the number of clicks. |
| `links` | array | `{ "id", "url", "first_click_time" }` for each link the contact clicked, most recent first. |
| `unsubscribed` | boolean | `true` when the contact revoked consent or used one-click unsubscribe from the message (the events counted in [`unsubscribes`](https://envoke-developers.pages.dev/v2/reports/fields/#unsubscribes)). |
In `links`, `id` is the tracked link's ID (the same as in the single-message report's [`links`](https://envoke-developers.pages.dev/v2/reports/message-stats/#links)) and `url` is the link as written in the message with Envoke's tracking parameters removed, or `""` for Envoke's own links (view online, preferences, poll answers). Merge tags in a link are not replaced, so `url` can be or contain a merge tag (`{@customfield-url_test}`, `https://example.org/show?seat={@seat|url}`) rather than a valid URL. `first_click_time` is the contact's first click on that link. For a message with an anonymous poll, poll-answer clicks are left out entirely, as in the Sent message report's drill-downs: they are not listed in `links`, not counted in `clicks`, `clicked`, or the `summary` (`clicks` and `last_click_time`), and do not by themselves place the message inside an activity window.
**Activity window.** With `activity_from` or `activity_to`, every flag and count reflects only the records inside the window, except `sent_time`, which is always the contact's send. A contact sent the message before the window who opened it inside the window shows `opened` `true` and `delivered` `false`, because the send is outside the window. `delivered` follows the message-level [`delivered`](https://envoke-developers.pages.dev/v2/reports/fields/#delivered): with a window it needs a send inside the window and no bounce for the message at any time.
**Relation to the contact page.** Each entry matches the row for the same message in the contact's Email sending history in Envoke (`https://e1.envoke.com/contacts/{id}/delivery-history.json`, `.campaigns[] | {campaign_id, flags}`): `bounced`, `opened`, and `clicked` equal its `bounce`, `open`, and `click` flags, except that on a message with an anonymous poll a contact whose only clicks were poll answers has `clicked` `false` where the page shows a click. A message the page lists from opens or clicks alone, with no send record for the contact, is not in this report. The app and this report both show times in your account's time zone.
### Response
```json
{
"contact": {
"id": "1004",
"email": "maya.chen@example.org",
"remote_id": "MRCT-0412"
},
"summary": {
"sends": 3,
"delivered": 3,
"bounces": 0,
"opens": 2,
"clicks": 1,
"unsubscribes": 0,
"last_send_time": "2026-05-14 09:31:07",
"last_open_time": "2026-05-14 12:02:44",
"last_click_time": "2026-05-14 12:03:10"
},
"messages": [
{
"id": "2004",
"name": "Pirates Opening Night",
"sent_time": "2026-05-14 09:31:07",
"delivered": true,
"bounced": false,
"opened": true,
"first_open_time": "2026-05-14 12:02:44",
"opens": 1,
"clicked": true,
"clicks": 1,
"links": [
{ "id": "88125", "url": "https://example.org/tickets/pirates", "first_click_time": "2026-05-14 12:03:10" }
],
"unsubscribed": false
},
{
"id": "2001",
"name": "Spring Season Announcement",
"sent_time": "2026-04-15 10:00:52",
"delivered": true,
"bounced": false,
"opened": true,
"first_open_time": "2026-04-15 10:04:19",
"opens": 2,
"clicked": false,
"clicks": 0,
"links": [],
"unsubscribed": false
},
{
"id": "2006",
"name": "Welcome to Maple Ridge",
"sent_time": "2025-11-03 08:00:12",
"delivered": true,
"bounced": false,
"opened": false,
"first_open_time": null,
"opens": 0,
"clicked": false,
"clicks": 0,
"links": [],
"unsubscribed": false
}
]
}
```
---
## Errors
Invalid requests return HTTP 400 with the validation envelope (see [Error Responses](https://envoke-developers.pages.dev/v2/reports/fields/#error-responses)). Besides the shared date, `limit`, `skip`, and unknown-parameter errors:
| Error | Cause |
|---|---|
| `One of the parameters 'email' or 'remote_id' must be provided` | `GET /v2/reports/contacts` without either. |
| `Only one of the parameters 'email' or 'remote_id' may be provided` | Both were given. |
| `Invalid 'email': ''` | `email` (or `remote_id`: `Invalid 'remote_id': ''`) was given with an empty value. |
| `More than one contact found for 'remote_id': 'MRCT-0412'` | Several contacts have this remote ID (or, with `email`, this address: `More than one contact found for 'email': 'maya.chen@example.org'`). |
| `Parameter 'email' is not valid` | `email` or `remote_id` given on `GET /v2/reports/contacts/{ID}`, where the path already names the contact. |
| Response | Cause |
|---|---|
| `[]` with HTTP 200 | The contact does not exist, belongs to another account, or is deleted; or no contact has this `email` or `remote_id`. |
| 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 by both contact activity endpoints. |
The 403 body:
```json
{
"success": false,
"result_text": "Contact-level email activity is not available on this account",
"result_data": []
}
```
---
## Examples
### One contact's activity, ten most recent messages
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/contacts/1004"
```
### By email address
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/contacts?email=maya.chen%40example.org"
```
### By remote ID, activity in May
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/contacts?remote_id=MRCT-0412&activity_from=2026-05-01&activity_to=2026-05-31"
```
Response (only 2004 had activity for this contact in May):
```json
{
"contact": {
"id": "1004",
"email": "maya.chen@example.org",
"remote_id": "MRCT-0412"
},
"summary": {
"sends": 1,
"delivered": 1,
"bounces": 0,
"opens": 1,
"clicks": 1,
"unsubscribes": 0,
"last_send_time": "2026-05-14 09:31:07",
"last_open_time": "2026-05-14 12:02:44",
"last_click_time": "2026-05-14 12:03:10"
},
"messages": [
{
"id": "2004",
"name": "Pirates Opening Night",
"sent_time": "2026-05-14 09:31:07",
"delivered": true,
"bounced": false,
"opened": true,
"first_open_time": "2026-05-14 12:02:44",
"opens": 1,
"clicked": true,
"clicks": 1,
"links": [
{ "id": "88125", "url": "https://example.org/tickets/pirates", "first_click_time": "2026-05-14 12:03:10" }
],
"unsubscribed": false
}
]
}
```
### A contact with a bounce
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"https://e1.envoke.com/v2/reports/contacts/1007?limit=1"
```
Response:
```json
{
"contact": {
"id": "1007",
"email": "r.osei@example.org",
"remote_id": ""
},
"summary": {
"sends": 2,
"delivered": 1,
"bounces": 1,
"opens": 1,
"clicks": 0,
"unsubscribes": 0,
"last_send_time": "2026-03-10 18:00:41",
"last_open_time": "2025-09-02 08:47:15",
"last_click_time": null
},
"messages": [
{
"id": "2002",
"name": "Auditions Open",
"sent_time": "2026-03-10 18:00:41",
"delivered": false,
"bounced": true,
"opened": false,
"first_open_time": null,
"opens": 0,
"clicked": false,
"clicks": 0,
"links": [],
"unsubscribed": false
}
]
}
```
`summary` covers both messages; `limit=1` returns only the newest in `messages`.
---
*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).*
---
URL: https://envoke-developers.pages.dev/v1/hooks/fields/
Markdown: https://envoke-developers.pages.dev/v1/hooks/fields.md
# Hooks API — Fields Reference
This article describes all fields available through the Hooks API. Use it as a reference when creating, reading, or deleting webhook subscriptions.
---
## Overview
The Hooks API lets you manage webhook subscriptions programmatically. A webhook subscription tells Envoke to send an HTTP POST to your endpoint whenever a specific event — a new contact, a form submission, an email open, or a lead activity — occurs in your account.
Each subscription has four fields: a unique ID, an event type, a destination URL, and an optional filter.
All endpoints require [HTTP Basic Auth](https://envoke-developers.pages.dev/guides/authentication/) using your API ID and API Key.
---
## Fields
| Field | Type | Read-only | Required on Create | Description |
|---|---|---|---|---|
| `id` | string | Yes | — | Unique 32-character hex identifier assigned on creation. |
| `type` | string (enum) | Yes | Yes | The event type that triggers this subscription. |
| `url` | string (URI) | Yes | Yes | The destination URL that receives HTTP POST payloads. |
| `filter` | object | Yes | No | Optional key-value conditions that must match before delivery. |
> **Note:** All fields are immutable after creation. To change the `type`, `url`, or `filter` of an existing subscription, delete it and create a new one.
---
## Field Details
### `id`
- **Type:** String
- **Read-only:** Yes — assigned automatically on creation.
- **Format:** 32-character lowercase hexadecimal string.
- **Example:** `"9f1c2a42baedb56c7e65b3eeaf1a8814"`
Use this ID in `GET /v1/hooks/{id}.json` and `DELETE /v1/hooks/{id}.json` requests.
---
### `type`
- **Type:** String (enum)
- **Read-only:** Yes — set at creation, cannot be updated.
- **Required on create:** Yes
The event type that triggers delivery. Must be one of the following values:
**Contact events**
| Value | Triggers when… |
|---|---|
| `new_contact` | A new contact is added to your account by any means. |
| `consent_change` | A contact's consent status changes. |
**Submission events**
| Value | Triggers when… |
|---|---|
| `submission` | Any form submission occurs (generic, covers all submission types below). |
| `form_submission` | A contact submits a website form. |
| `preference_page_submission` | A contact submits a preference page form. |
| `api_submission` | A contact is added or updated via the API. |
| `import_submission` | A contact is added via a list import. |
| `ui_submission` | A contact is added manually in the Envoke UI. |
**Email activity events**
| Value | Triggers when… |
|---|---|
| `email_send` | An email is sent to a contact. |
| `email_open` | A contact opens an email. |
| `email_click` | A contact clicks a link in an email. |
| `email_autoresponder_send` | An autoresponder email is sent to a contact. |
| `email_bounce` | An email to a contact bounces or is refused (deferrals are not included). |
| `email_spam_report` | A contact reports an email as spam, through their mailbox provider or the Envoke preference page. |
| `email_delivery` | The recipient's mail server accepts an email. Reserved for an upcoming release: you can subscribe now, but no events are delivered yet. |
**Lead events**
| Value | Triggers when… |
|---|---|
| `new_lead` | A new lead is created. |
| `passed_to_sales` | A lead is passed to the sales team. |
| `lead_update` | A lead record is updated. |
| `new_opportunity` | An opportunity is created on a lead. |
| `new_sale` | A sale is recorded on a lead. |
---
### `url`
- **Type:** String (URI)
- **Read-only:** Yes — set at creation, cannot be updated.
- **Required on create:** Yes
- **Validation:** Must begin with `http://` or `https://`.
- **Example:** `"https://hooks.zapier.com/hooks/catch/123456/abcdef/"`
Envoke sends an HTTP POST to this URL when a matching event fires. Your endpoint should return an HTTP 2xx status to acknowledge delivery. On non-2xx responses, Envoke will retry once after 10 minutes. If your endpoint returns HTTP 410 (Gone), the subscription is automatically removed.
---
### `filter`
- **Type:** Object
- **Read-only:** Yes — set at creation, cannot be updated.
- **Required on create:** No — omit or pass `{}` to receive all events of the subscribed type.
- **Key type:** String
- **Value type:** Scalar (string, number, or boolean)
An optional set of key-value conditions. Before delivering an event, Envoke evaluates every key in the filter against the event payload. If all conditions match, the event is delivered. If any condition does not match, the event is silently skipped.
An empty object (`{}`) disables filtering — every event of the subscribed type is delivered.
**Common filter keys by event type:**
| Event type(s) | Filter key | Description |
|---|---|---|
| `form_submission`, `submission`, `preference_page_submission`, `api_submission`, `import_submission`, `ui_submission` | `form_id` | Deliver only events from a specific form. |
| All email activity events | `message_id` | Deliver only events from a specific email message, by its integer ID. |
| All email activity events | `message_external_id` | Deliver only events from a specific email message, by its 32-character external ID. An alternative to `message_id`. |
| All email activity events | `contact_remote_id` | Deliver only events for a contact with a specific remote ID. |
| `email_bounce` | `bounce_type` | Deliver only one kind of bounce, for example `hard`. |
| `email_spam_report` | `source` | Deliver only spam reports from one source: `feedback_loop` or `envoke`. |
The email activity events are `email_send`, `email_open`, `email_click`, `email_autoresponder_send`, `email_bounce`, `email_spam_report`, and `email_delivery`. Filter keys are matched against the delivered payload, so a key that is not in the payload (for example `campaign_id`) never matches and every event is skipped.
**Example filter values:**
```json
{ "form_id": "abc123def456" }
```
```json
{ "message_id": "789" }
```
```json
{}
```
---
## Error Responses
**Validation errors** (invalid request body):
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"'type' is required"
]
}
}
```
**Duplicate subscription** (same `type` and `url` already exists):
```json
{
"success": false,
"result_text": "A webhook subscription with this type and URL already exists",
"result_data": {
"id": "9f1c2a42baedb56c7e65b3eeaf1a8814"
}
}
```
`result_data.id` is the ID of the existing subscription.
**Not-found behaviour by method:**
- `GET /hooks/{id}.json` — returns `null` when the ID is not found.
- `DELETE /hooks/{id}.json` — returns success even when the ID does not exist (idempotent).
---
*Full reference, OpenAPI description, and Postman collection: [developers.envoke.com](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/v1/hooks/read/
Markdown: https://envoke-developers.pages.dev/v1/hooks/read.md
# Hooks API — Read
Use the read endpoints to retrieve webhook subscriptions for your account. You can list all subscriptions or retrieve a single one by ID.
---
## Endpoints
| Method | URL | Description |
|---|---|---|
| GET | `https://e1.envoke.com/v1/hooks.json` | Return all webhook subscriptions. |
| GET | `https://e1.envoke.com/v1/hooks/{ID}.json` | Return a single subscription. |
Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## List Webhook Subscriptions
```
GET https://e1.envoke.com/v1/hooks.json
```
Returns a JSON array of all webhook subscriptions for your account. Unlike most Envoke list endpoints, **all subscriptions are returned by default** — there is no default limit. Most accounts have a small number of subscriptions, so pagination is rarely necessary.
### Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| `limit` | integer | *(none)* | Maximum number of results to return. When omitted, all subscriptions are returned. |
| `skip` | integer | `0` | Number of records to skip. Use with `limit` to paginate. |
| `fields` | string | *(all)* | Comma-separated list of fields to include in each result (e.g. `id,type,url`). |
| `type` | string | — | Filter results to subscriptions of the given event type (e.g. `?type=form_submission`). |
### Response
A JSON array of hook objects. Returns an empty array (`[]`) if no subscriptions exist.
```json
[
{
"id": "9f1c2a42baedb56c7e65b3eeaf1a8814",
"type": "form_submission",
"url": "https://crm.ridgeline.edu/webhooks/envoke/admissions",
"filter": { "form_id": "abc123def456" }
},
{
"id": "a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7",
"type": "new_contact",
"url": "https://sis.ridgeline.edu/webhooks/contacts",
"filter": {}
}
]
```
---
## Get a Single Subscription
```
GET https://e1.envoke.com/v1/hooks/{ID}.json
```
Returns a single webhook subscription object. Replace `{ID}` with the 32-character hex subscription ID.
If the ID is not found, the API returns `null`.
### Response
```json
{
"id": "9f1c2a42baedb56c7e65b3eeaf1a8814",
"type": "form_submission",
"url": "https://crm.ridgeline.edu/webhooks/envoke/admissions",
"filter": { "form_id": "abc123def456" }
}
```
---
## Examples
### List all subscriptions
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" "$BASE/v1/hooks.json"
```
### Filter by event type
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" "$BASE/v1/hooks.json?type=form_submission"
```
### Paginate (first 5, skip first 5)
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" "$BASE/v1/hooks.json?limit=5&skip=5"
```
### Return only `id` and `type` fields
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" "$BASE/v1/hooks.json?fields=id,type"
```
### Get a single subscription by ID
```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
"$BASE/v1/hooks/9f1c2a42baedb56c7e65b3eeaf1a8814.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).*
---
URL: https://envoke-developers.pages.dev/v1/hooks/insert/
Markdown: https://envoke-developers.pages.dev/v1/hooks/insert.md
# Hooks API — Create
Use this endpoint to create a new webhook subscription in your account.
---
## Endpoint
```
POST https://e1.envoke.com/v1/hooks.json
```
Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be JSON.
Returns **HTTP 201** on success.
---
## Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| `type` | string (enum) | Yes | The event type to subscribe to. See the full list of types below. |
| `url` | string (URI) | Yes | The destination URL that will receive HTTP POST payloads. Must start with `http://` or `https://`. |
| `filter` | object | No | Key-value conditions that must match before delivery. Omit or pass `{}` to receive all events of the subscribed type. |
### Supported Event Types
| Category | Type |
|---|---|
| Contact | `new_contact`, `consent_change` |
| Submission | `submission`, `form_submission`, `preference_page_submission`, `api_submission`, `import_submission`, `ui_submission` |
| Email Activity | `email_send`, `email_open`, `email_click`, `email_autoresponder_send`, `email_bounce`, `email_spam_report`, `email_delivery` |
| Lead | `new_lead`, `passed_to_sales`, `lead_update`, `new_opportunity`, `new_sale` |
`email_delivery` is reserved for an upcoming release: you can subscribe to it now, but no events are delivered yet.
### Filter Keys
The `filter` object accepts any key-value pairs that correspond to fields in the event payload. Common filter keys:
| Event type(s) | Key | Description |
|---|---|---|
| Form events | `form_id` | Deliver only when the event originates from a specific form. |
| Email events | `message_id` | Deliver only when the event is associated with a specific email message, by its integer ID. |
| Email events | `message_external_id` | The same, by the message's 32-character external ID. Use either key. |
Filter keys must be fields of the delivered payload (see `help_payloads.md`). A key the payload does not contain, such as `campaign_id`, never matches, so every event is skipped.
Pass `{}` or omit `filter` entirely to receive all events with no filtering.
---
## Response
On success, returns the newly created subscription ID with HTTP 201:
```json
{"id": "9f1c2a42baedb56c7e65b3eeaf1a8814"}
```
`id` is the subscription ID. Store this to retrieve or delete the subscription later.
---
## Validation Errors
```json
{
"success": false,
"result_text": "Validation failed, see 'result_data' -> 'errors'",
"result_data": {
"errors": [
"'type' is required"
]
}
}
```
Common validation errors:
| Error | Cause |
|---|---|
| `'type' is required` | `type` was missing or blank. |
| `Invalid webhook type: '{value}'` | `type` is not one of the 20 supported values. |
| `'url' is required` | `url` was missing or blank. |
| `'url' must start with http:// or https://` | `url` does not begin with a valid scheme. |
| `'filter' values must be scalar` | A `filter` value was an array or object instead of a string, number, or boolean. |
---
## Duplicate Subscription Error
If a subscription with the same `type` and `url` already exists, the API returns HTTP 400:
```json
{
"success": false,
"result_text": "A webhook subscription with this type and URL already exists",
"result_data": {
"id": "9f1c2a42baedb56c7e65b3eeaf1a8814"
}
}
```
`result_data.id` is the ID of the existing subscription.
---
## Examples
### Subscribe to all form submissions from a specific form
A college wants to be notified in their admissions CRM every time a prospect submits the online inquiry form.
```bash
curl -X POST -ks \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "form_submission",
"url": "https://crm.ridgeline.edu/webhooks/envoke/admissions",
"filter": { "form_id": "abc123def456" }
}' \
"$BASE/v1/hooks.json"
```
### Subscribe to email opens for a specific message
Track which students opened the fall course registration reminder to follow up with non-openers.
```bash
curl -X POST -ks \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "email_open",
"url": "https://portal.ridgeline.edu/webhooks/engagement",
"filter": { "message_id": "789" }
}' \
"$BASE/v1/hooks.json"
```
### Subscribe to email clicks on an alumni giving message
Alert the giving office when an alumnus clicks the donation link in the annual appeal email. This example filters by the message's external ID instead of its integer ID.
```bash
curl -X POST -ks \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "email_click",
"url": "https://giving.ridgeline.edu/webhooks/envoke",
"filter": { "message_external_id": "f5a3c1e9b7d5f3a1c9e7b5d3f1a9c7e5" }
}' \
"$BASE/v1/hooks.json"
```
### Subscribe to hard bounces
Keep the CRM's email addresses clean by flagging every contact whose address hard-bounces.
```bash
curl -X POST -ks \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "email_bounce",
"url": "https://crm.ridgeline.edu/webhooks/envoke/bounces",
"filter": { "bounce_type": "hard" }
}' \
"$BASE/v1/hooks.json"
```
### Subscribe to spam reports
Flag contacts in the CRM as soon as they report an Envoke email as spam.
```bash
curl -X POST -ks \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "email_spam_report",
"url": "https://crm.ridgeline.edu/webhooks/envoke/complaints"
}' \
"$BASE/v1/hooks.json"
```
### Subscribe to all new contacts (no filter)
Sync every new contact to the student information system as soon as they are added.
```bash
curl -X POST -ks \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "new_contact",
"url": "https://sis.ridgeline.edu/webhooks/contacts"
}' \
"$BASE/v1/hooks.json"
```
### Subscribe to new leads
Notify the enrollment management team when a new lead is created.
```bash
curl -X POST -ks \
-u "$API_ID:$API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "new_lead",
"url": "https://crm.ridgeline.edu/webhooks/leads"
}' \
"$BASE/v1/hooks.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).*
---
URL: https://envoke-developers.pages.dev/v1/hooks/delete/
Markdown: https://envoke-developers.pages.dev/v1/hooks/delete.md
# Hooks API — Delete
Use this endpoint to permanently remove a webhook subscription from your account.
---
## Endpoint
```
DELETE https://e1.envoke.com/v1/hooks/{ID}.json
```
Replace `{ID}` with the 32-character hex subscription ID. Authentication is via HTTP Basic Auth using your API ID and API Key.
---
## Important
**Deletion is permanent.** Once a subscription is deleted, Envoke will no longer deliver events to that URL for that event type. The deletion cannot be undone via the API — you must create a new subscription if you want to resume delivery.
**Deletion is idempotent.** If the subscription ID does not exist, the API still returns a success response. This makes it safe to call DELETE more than once.
---
## Response
On success (or when the ID does not exist):
```json
{
"success": true,
"result_text": "Deleted",
"result_data": []
}
```
---
## Example
```bash
curl -X DELETE -ks \
-u "$API_ID:$API_KEY" \
"$BASE/v1/hooks/9f1c2a42baedb56c7e65b3eeaf1a8814.json"
```
Replace `9f1c2a42baedb56c7e65b3eeaf1a8814` with the ID of the subscription you want to delete. Use `GET /v1/hooks.json` to list your subscriptions and find the correct ID.
---
## Automatic Deletion
Envoke automatically removes a subscription if your endpoint responds with HTTP **410 Gone**. Use this to programmatically deregister a subscription from the receiving side when it is no longer needed.
---
*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).*
---
URL: https://envoke-developers.pages.dev/v1/hooks/payloads/
Markdown: https://envoke-developers.pages.dev/v1/hooks/payloads.md
# Hooks API — Webhook Payloads
This article describes the payload Envoke delivers to your endpoint for each event type.
---
## Delivery Format
Envoke delivers webhook payloads as **HTTP POST** requests to your registered URL. The request body is always a **JSON array**, even when a single event is delivered.
```
POST https://your-endpoint.example.com/webhook
Content-Type: application/json
[ { ... event object ... } ]
```
Email activity events (`email_send`, `email_open`, `email_click`) may be batched — a single POST can contain multiple events from the same send or activity processing run. `email_bounce` events are batched when the failure happens while Envoke is sending (all such failures from one sending run arrive in one POST); bounces reported later by the recipient's mail server arrive one object per request. `email_spam_report` events deliver one object per request. `email_delivery` events, when they become available, will be batched. All other event types deliver exactly one object per request. Always read the body as an array.
On a non-2xx response, Envoke retries once after 10 minutes. If your endpoint returns HTTP 410, the subscription is automatically removed.
---
## Contact Events
### `new_contact`
Fires when a new contact is added to your account by any means (form, import, API, or manual entry). The payload is the full contact record — the same object the Contacts API returns for that contact.
**Payload fields:**
| Field | Type | Description |
|---|---|---|
| `id` | integer | Envoke internal contact ID. |
| `external_id` | string | Envoke-generated 32-character hex identifier. |
| `remote_id` | string | Your system's identifier for this contact, if set. Empty string if not set. |
| `date_created` | string | Date and time the contact was created (`YYYY-MM-DD HH:MM:SS`). |
| `first_name` | string | |
| `last_name` | string | |
| `title` | string | Job title or academic title. |
| `email` | string | |
| `company` | string | |
| `phone` | string | |
| `address_1` | string | |
| `address_2` | string | |
| `city` | string | |
| `province` | string | Province or state. |
| `postal_code` | string | |
| `country` | string | Two-letter ISO country code. |
| `website` | string | |
| `language` | string | Preferred language code (e.g. `"en"`, `"fr"`). |
| `feedback` | string | Internal notes field. |
| `consent_status` | string | Current consent type name (e.g. `"Express"`, `"Implied"`, `"No Consent"`). |
| `consent_description` | string | Description of how consent was obtained. |
| `consent_effective_date` | string | When the current consent became effective. |
| `consent_expiry` | string | When the consent expires, if applicable. Empty string if no expiry. |
| `interests` | array | Contact's interest group memberships. |
| `custom_fields` | array | Custom field values. |
| `autoresponders` | array | Autoresponder assignments. |
| `timestamp` | integer | Unix timestamp of when the contact was created. |
**Example scenario:** Northfield University subscribes to `new_contact` to sync incoming contacts to their student information system as soon as they appear in Envoke.
**Example payload:**
```json
[
{
"id": 84291,
"external_id": "3f8a1c042d7e9b56a0e14c3d8f2b5a91",
"remote_id": "NF-STU-2024-00847",
"date_created": "2024-09-03 08:41:22",
"timestamp": 1725352882,
"first_name": "Miguel",
"last_name": "Torres",
"title": "",
"email": "m.torres@students.northfield.edu",
"company": "Northfield University",
"phone": "555-204-3817",
"address_1": "14 Birchwood Avenue",
"address_2": "",
"city": "Northfield",
"province": "ON",
"postal_code": "N2R 4K7",
"country": "CA",
"website": "",
"language": "en",
"feedback": "",
"consent_status": "Express",
"consent_description": "Submitted online enrollment inquiry form on September 3, 2024.",
"consent_effective_date": "2024-09-03 08:41:22",
"consent_expiry": "",
"interests": [
{ "id": 12, "name": "Graduate Programs", "state": "Set" }
],
"custom_fields": {
"student_type": "Graduate"
},
"autoresponders": [],
"timestamp": 1725352882
}
]
```
---
### `consent_change`
Fires when a contact's consent status changes — for example, when they unsubscribe, re-subscribe, or their consent type is updated. The payload contains all the same fields as `new_contact` plus two additional fields that describe the reason for the change.
**Additional fields:**
| Field | Type | Description |
|---|---|---|
| `unsubscribe_reason` | string | The unsubscribe reason category selected by the contact. Empty string if not set or if consent improved. |
| `unsubscribe_reason_detail` | string | Free-text detail provided by the contact at unsubscribe. Empty string if not provided. |
**Example scenario:** Northfield University subscribes to `consent_change` to update opt-out status in their alumni CRM when a contact unsubscribes.
**Example payload:**
```json
[
{
"id": 61048,
"external_id": "9c4e2b871f3a5d0e6b82c7a14f3d9e02",
"remote_id": "ALM-2008-4421",
"date_created": "2021-06-14 10:22:09",
"first_name": "Helen",
"last_name": "Webb",
"title": "Associate Professor",
"email": "h.webb@northfield.edu",
"company": "Northfield University — Faculty of Arts",
"phone": "",
"address_1": "",
"address_2": "",
"city": "",
"province": "",
"postal_code": "",
"country": "CA",
"website": "",
"language": "en",
"feedback": "",
"consent_status": "No Consent",
"consent_description": "",
"consent_effective_date": "2025-02-18 14:07:33",
"consent_expiry": "",
"interests": [],
"custom_fields": {},
"autoresponders": [],
"timestamp": 1739884053,
"unsubscribe_reason": "Too frequent",
"unsubscribe_reason_detail": "I receive too many emails from this list."
}
]
```
---
## Submission Events
All submission event types share the same payload structure. The `submission_type` field identifies the origin:
| Subscription type | `submission_type` value |
|---|---|
| `form_submission` | `"form_submission"` |
| `preference_page_submission` | `"preference_page_submission"` |
| `api_submission` | `"api_submission"` |
| `import_submission` | `"import_submission"` |
| `ui_submission` | `"ui_submission"` |
| `submission` | One of the five values above |
The generic `submission` type delivers the same payload as whichever specific type triggered the event. Subscribing to `submission` is equivalent to subscribing to all five specific types at once.
**Common payload fields:**
| Field | Type | Description |
|---|---|---|
| `id` | string | Unique identifier for this submission event. |
| `contact_id` | integer | Envoke internal contact ID. |
| `contact_external_id` | string | Envoke-generated 32-character hex identifier for the contact. |
| `contact_remote_id` | string | Your system's identifier for this contact, if set. Empty string if not set. |
| `contact_created` | boolean | `true` if this submission created the contact; `false` if it updated an existing one. |
| `submission_type` | string | The submission origin. See the table above. |
| `submission_time` | string | Date and time of the submission (`YYYY-MM-DD HH:MM:SS`). |
| `timestamp` | integer | Unix timestamp of the submission. |
| `first_name` | string | |
| `last_name` | string | |
| `title` | string | |
| `email` | string | |
| `company` | string | |
| `phone` | string | |
| `address_1` | string | |
| `address_2` | string | |
| `city` | string | |
| `province` | string | |
| `postal_code` | string | |
| `country` | string | |
| `website` | string | |
| `language` | string | |
| `feedback` | string | |
| `consent_status` | string | |
| `consent_description` | string | |
| `consent_effective_date` | string | |
| `consent_expiry` | string | |
| `visitor_id` | string | Visitor tracking ID. Empty string when not applicable (e.g. imports, manual adds). |
| `visit_id` | string | Visit tracking ID. Empty string when not applicable. |
| `pageview_id` | string | Pageview tracking ID. Empty string when not applicable. |
**Additional fields for `form_submission` only:**
| Field | Type | Description |
|---|---|---|
| `form_id` | string | The ID of the form that was submitted. |
| `form_name` | string | The name of the form in Envoke. |
| `form_url` | string | The URL of the page where the form was submitted. |
---
### `form_submission`
**Example scenario:** Northfield University subscribes to `form_submission` filtered by `form_id` to push prospective student inquiries from the graduate admissions form directly into their Slate CRM.
```json
[
{
"id": "5e8f2a1d3c0b4e9a7f6d2c4b1a8e5d03",
"contact_id": 92714,
"contact_external_id": "a7d3e1f0b2c4e6a8d0f2b4c6e8a0d2f4",
"contact_remote_id": "",
"contact_created": true,
"submission_type": "form_submission",
"submission_time": "2025-01-15 11:23:47",
"timestamp": 1736940227,
"first_name": "Priya",
"last_name": "Sharma",
"title": "",
"email": "priya.sharma@gmail.com",
"company": "",
"phone": "555-318-0042",
"address_1": "",
"address_2": "",
"city": "Hamilton",
"province": "ON",
"postal_code": "L8P 1A1",
"country": "CA",
"website": "",
"language": "en",
"feedback": "",
"consent_status": "Express",
"consent_description": "Checked the consent checkbox on the Graduate Admissions Inquiry form.",
"consent_effective_date": "2025-01-15 11:23:47",
"consent_expiry": "",
"form_id": "f3b2e1a0d4c5b6a7e8f9d0c1b2a3e4f5",
"form_name": "Graduate Admissions Inquiry",
"form_url": "https://www.northfield.edu/graduate/apply",
"visitor_id": "67a1b2c3d4e5f6a7b8c9d0e1",
"visit_id": "67a1b2c3d4e5f6a7b8c9d0e2",
"pageview_id": "67a1b2c3d4e5f6a7b8c9d0e3"
}
]
```
---
### `preference_page_submission`
**Example scenario:** Northfield University subscribes to `preference_page_submission` to update their alumni database when graduates adjust their communication preferences.
```json
[
{
"id": "2c4e6a8b0d2f4a6c8e0a2c4e6a8b0d2f",
"contact_id": 55830,
"contact_external_id": "b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1",
"contact_remote_id": "ALM-2015-8803",
"contact_created": false,
"submission_type": "preference_page_submission",
"submission_time": "2025-03-22 16:04:11",
"timestamp": 1742659451,
"first_name": "James",
"last_name": "Okwu",
"title": "",
"email": "james.okwu@outlook.com",
"company": "Westridge Financial Group",
"phone": "",
"address_1": "",
"address_2": "",
"city": "Toronto",
"province": "ON",
"postal_code": "M5V 2T6",
"country": "CA",
"website": "",
"language": "en",
"feedback": "",
"consent_status": "Express",
"consent_description": "Re-confirmed consent on Alumni Preference Page.",
"consent_effective_date": "2025-03-22 16:04:11",
"consent_expiry": "",
"visitor_id": "68b2c3d4e5f6a7b8c9d0e1f2",
"visit_id": "68b2c3d4e5f6a7b8c9d0e1f3",
"pageview_id": "68b2c3d4e5f6a7b8c9d0e1f4"
}
]
```
---
### `api_submission`
**Example scenario:** Northfield University's student information system (SIS) syncs newly enrolled students to Envoke via the API. The `api_submission` hook confirms each sync in their enrollment management platform.
```json
[
{
"id": "1a3c5e7b9d1f3a5c7e9b1d3f5a7c9e1b",
"contact_id": 103445,
"contact_external_id": "c2e4a6b8d0f2c4e6a8b0d2f4c6e8a0b2",
"contact_remote_id": "SIS-2025-1142",
"contact_created": true,
"submission_type": "api_submission",
"submission_time": "2025-09-02 07:15:30",
"timestamp": 1756696530,
"first_name": "Lucas",
"last_name": "Brennan",
"title": "",
"email": "l.brennan@students.northfield.edu",
"company": "Northfield University",
"phone": "",
"address_1": "",
"address_2": "",
"city": "",
"province": "",
"postal_code": "",
"country": "CA",
"website": "",
"language": "en",
"feedback": "",
"consent_status": "Express",
"consent_description": "Express consent provided at enrollment.",
"consent_effective_date": "2025-09-02 07:15:30",
"consent_expiry": "",
"visitor_id": "",
"visit_id": "",
"pageview_id": ""
}
]
```
---
### `import_submission`
**Example scenario:** Northfield University imports an updated alumni mailing list each year from a spreadsheet. The `import_submission` hook notifies their alumni engagement platform of each record.
```json
[
{
"id": "4d6f8b2a0c4e6d8f2a4c6e8b0d2f4a6c",
"contact_id": 48902,
"contact_external_id": "d0e2c4a6b8f0d2e4c6a8b0f2d4e6c8a0",
"contact_remote_id": "ALM-1998-0044",
"contact_created": false,
"submission_type": "import_submission",
"submission_time": "2025-02-10 09:00:14",
"timestamp": 1739181614,
"first_name": "Diane",
"last_name": "Kowalski",
"title": "Director of Operations",
"email": "dkowalski@northviewlogistics.com",
"company": "Northview Logistics",
"phone": "555-492-1780",
"address_1": "88 Industrial Parkway",
"address_2": "Suite 200",
"city": "Kitchener",
"province": "ON",
"postal_code": "N2H 3R4",
"country": "CA",
"website": "https://www.northviewlogistics.com",
"language": "en",
"feedback": "",
"consent_status": "Implied",
"consent_description": "Alumni — implied consent under CASL.",
"consent_effective_date": "2025-02-10 09:00:14",
"consent_expiry": "2027-02-10 09:00:14",
"visitor_id": "",
"visit_id": "",
"pageview_id": ""
}
]
```
---
**Additional fields for `ui_submission` only:**
| Field | Type | Description |
|---|---|---|
| `is_bulk_update` | boolean | `true` if the contact was added or updated as part of a bulk manual operation; `false` if added individually. |
### `ui_submission`
**Example scenario:** A staff member in the Alumni Relations office manually adds a community partner who called in to request information about the Distinguished Alumni Speaker Series.
```json
[
{
"id": "9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f",
"contact_id": 71256,
"contact_external_id": "e8a0b2d4f6e8a0b2d4f6c8e0a2b4d6f8",
"contact_remote_id": "",
"contact_created": true,
"submission_type": "ui_submission",
"submission_time": "2025-04-07 14:52:00",
"timestamp": 1744037520,
"first_name": "Robert",
"last_name": "Nakamura",
"title": "Executive Director",
"email": "r.nakamura@northfieldarts.org",
"company": "Northfield Arts Foundation",
"phone": "555-783-2200",
"address_1": "31 Gallery Row",
"address_2": "",
"city": "Northfield",
"province": "ON",
"postal_code": "N2R 1P3",
"country": "CA",
"website": "https://www.northfieldarts.org",
"language": "en",
"feedback": "Called requesting information about the Distinguished Alumni Speaker Series.",
"consent_status": "Express",
"consent_description": "Verbal consent given over phone on April 7, 2025.",
"consent_effective_date": "2025-04-07 14:52:00",
"consent_expiry": "",
"visitor_id": "",
"visit_id": "",
"pageview_id": "",
"is_bulk_update": false
}
]
```
---
## Email Activity Events
All email activity event types share a common set of fields.
**Common fields:**
| Field | Type | Description |
|---|---|---|
| `activity_type` | string | The event type: `"email_send"`, `"email_open"`, `"email_click"`, `"email_autoresponder_send"`, `"email_bounce"`, `"email_spam_report"`, or `"email_delivery"`. |
| `timestamp` | integer | Unix timestamp when the event occurred. |
| `message_id` | integer | Envoke internal ID of the email campaign. |
| `message_external_id` | string | Envoke-generated 32-character hex identifier for the campaign. |
| `message_name` | string | The internal name of the campaign in Envoke. |
| `message_subject` | string | The email subject line. May contain unresolved merge field tokens. |
| `sender` | string | The sender identity configured on the campaign. Name and address (`Name `). |
| `contact_id` | integer | Envoke internal contact ID. |
| `contact_external_id` | string | Envoke-generated 32-character hex identifier for the contact. |
| `contact_email` | string | The email address the message was sent to. |
| `contact_remote_id` | string or null | Your system's identifier for this contact. `null` if not set. |
---
### `email_send`
Fires when an email is sent to a contact. Send events are batched — a single POST to your endpoint will typically contain multiple objects, one per recipient.
**Example scenario:** Northfield University subscribes to `email_send` filtered by `message_id` to log each delivery of the monthly alumni newsletter in their engagement platform.
```json
[
{
"activity_type": "email_send",
"timestamp": 1715432791,
"message_id": 2048,
"message_external_id": "7e4a1f2c9b3d5e0a8f6c2b4d9e1a3f5c",
"message_name": "Alumni Newsletter — May 2025",
"message_subject": "What's happening at Northfield this spring",
"sender": "Northfield Alumni Relations ",
"contact_id": 55830,
"contact_external_id": "b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1",
"contact_email": "james.okwu@outlook.com",
"contact_remote_id": "ALM-2015-8803"
},
{
"activity_type": "email_send",
"timestamp": 1715432804,
"message_id": 2048,
"message_external_id": "7e4a1f2c9b3d5e0a8f6c2b4d9e1a3f5c",
"message_name": "Alumni Newsletter — May 2025",
"message_subject": "What's happening at Northfield this spring",
"sender": "Northfield Alumni Relations ",
"contact_id": 48902,
"contact_external_id": "d0e2c4a6b8f0d2e4c6a8b0f2d4e6c8a0",
"contact_email": "dkowalski@northviewlogistics.com",
"contact_remote_id": "ALM-1998-0044"
}
]
```
---
### `email_open`
Fires when a contact opens an email. Open events are batched — a single POST may contain opens from multiple contacts processed in the same activity run.
**Example scenario:** Northfield University tracks opens of the course registration deadline reminder to identify students who may need a follow-up call.
```json
[
{
"activity_type": "email_open",
"timestamp": 1715518340,
"message_id": 2051,
"message_external_id": "3c7b9e2f1a4d6c8a0e2b4f6d8a0c2e4b",
"message_name": "Fall Registration Deadline Reminder",
"message_subject": "Action required: course registration closes Friday",
"sender": "Northfield Registrar ",
"contact_id": 84291,
"contact_external_id": "3f8a1c042d7e9b56a0e14c3d8f2b5a91",
"contact_email": "m.torres@students.northfield.edu",
"contact_remote_id": "NF-STU-2024-00847"
}
]
```
---
### `email_click`
Fires when a contact clicks a tracked link in an email. Click events are batched. Includes the URL and anchor text of the clicked link in addition to the common fields.
**Additional fields:**
| Field | Type | Description |
|---|---|---|
| `link_url` | string or null | The destination URL of the clicked link. |
| `link_text` | string or null | The anchor text of the clicked link. |
**Example scenario:** Northfield University's development office subscribes to `email_click` filtered by `message_id` to alert gift officers the moment an alumnus clicks the donation button in the annual giving appeal.
```json
[
{
"activity_type": "email_click",
"timestamp": 1716047220,
"message_id": 2063,
"message_external_id": "f5a3c1e9b7d5f3a1c9e7b5d3f1a9c7e5",
"message_name": "2025 Annual Giving Appeal",
"message_subject": "Your gift helps the next generation of Northfield students",
"sender": "Northfield Annual Fund ",
"contact_id": 55830,
"contact_external_id": "b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1",
"contact_email": "james.okwu@outlook.com",
"contact_remote_id": "ALM-2015-8803",
"link_url": "https://give.northfield.edu/annual-fund?utm_source=envoke",
"link_text": "Make a gift today"
}
]
```
---
### `email_autoresponder_send`
Fires when an autoresponder email is sent to a contact. Includes the autoresponder ID and name in addition to the common fields.
**Additional fields:**
| Field | Type | Description |
|---|---|---|
| `autoresponder_id` | integer | Envoke internal ID of the autoresponder. |
| `autoresponder_name` | string | The name of the autoresponder in Envoke. |
**Example scenario:** Northfield University sends a welcome sequence to newly enrolled students. They subscribe to `email_autoresponder_send` to log each autoresponder delivery in their student success platform.
```json
[
{
"activity_type": "email_autoresponder_send",
"timestamp": 1725282930,
"message_id": 1984,
"message_external_id": "a2e4c6b8d0f2a4c6e8b0d2f4a6c8e0b2",
"message_name": "Welcome to Northfield — Message 1",
"message_subject": "Welcome to Northfield University",
"sender": "Northfield Admissions ",
"contact_id": 103445,
"contact_external_id": "c2e4a6b8d0f2c4e6a8b0d2f4c6e8a0b2",
"contact_email": "l.brennan@students.northfield.edu",
"contact_remote_id": "SIS-2025-1142",
"autoresponder_id": 47,
"autoresponder_name": "New Student Welcome Sequence"
}
]
```
---
### `email_bounce`
Fires when an email to a contact fails: the recipient's mail server bounces it, or Envoke's sending server cannot hand it over. Temporary deferrals, where the recipient's server asks Envoke to try again later, do not fire this event because the message may still be delivered. Includes the kind of bounce, the reason, and whether Envoke has stopped sending to the contact, in addition to the common fields.
**Additional fields:**
| Field | Type | Description |
|---|---|---|
| `bounce_type` | string | The kind of failure. `"hard"`: the recipient's server permanently refused the message, for example because the address does not exist. `"soft"`: the recipient's server returned a failure that may not be permanent, for example a full mailbox. `"smtp_error"`: Envoke's sending server could not hand the message to the recipient's server. `"invalid_address"`: the address was refused as invalid at send time. `"rejected"`: the message was rejected before delivery. |
| `bounce_reason` | string | The diagnostic text reported for the failure, up to 255 characters, starting at the SMTP reply (for example `550 5.1.1 ...`). Raw SMTP transcript details are removed. May be an empty string. |
| `contact_blocked` | boolean | `true` when, after this failure, the contact's fail score is at or above your account's limit, so Envoke no longer sends email to this contact. It stays `true` for any later bounce while the contact remains over the limit. |
**Example scenario:** Northfield University subscribes to `email_bounce` filtered by `bounce_type` `hard` so their CRM can flag alumni whose email addresses no longer work.
```json
[
{
"activity_type": "email_bounce",
"timestamp": 1761148800,
"message_id": 2048,
"message_external_id": "7e4a1f2c9b3d5e0a8f6c2b4d9e1a3f5c",
"message_name": "Alumni Newsletter — May 2025",
"message_subject": "What's happening at Northfield this spring",
"sender": "Northfield Alumni Relations ",
"contact_id": 55830,
"contact_external_id": "b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1",
"contact_email": "james.okwu@outlook.com",
"contact_remote_id": "ALM-2015-8803",
"bounce_type": "hard",
"bounce_reason": "550 5.1.1 The email account that you tried to reach does not exist",
"contact_blocked": true
}
]
```
---
### `email_spam_report`
Fires when a contact reports an email as spam. A `consent_change` event also fires for the same report; `email_spam_report` adds the message the contact reported. When a mailbox provider reports a complaint, only the contact the email was sent to fires this event, even if other contacts in your account share the same email address. Includes the source of the report in addition to the common fields.
**Additional fields:**
| Field | Type | Description |
|---|---|---|
| `source` | string | Where the report came from. `"feedback_loop"`: the contact's mailbox provider forwarded the complaint, for example after the contact pressed "Report spam" in their inbox. `"envoke"`: the contact chose the report-spam option on the Envoke preference page. |
**Example scenario:** Northfield University subscribes to `email_spam_report` so their advancement team can review which messages draw complaints and suppress those alumni from future appeals.
```json
[
{
"activity_type": "email_spam_report",
"timestamp": 1761148800,
"message_id": 2048,
"message_external_id": "7e4a1f2c9b3d5e0a8f6c2b4d9e1a3f5c",
"message_name": "Alumni Newsletter — May 2025",
"message_subject": "What's happening at Northfield this spring",
"sender": "Northfield Alumni Relations ",
"contact_id": 48902,
"contact_external_id": "d0e2c4a6b8f0d2e4c6a8b0f2d4e6c8a0",
"contact_email": "dkowalski@northviewlogistics.com",
"contact_remote_id": "ALM-1998-0044",
"source": "feedback_loop"
}
]
```
---
### `email_delivery`
> **Coming soon:** `email_delivery` is reserved for an upcoming release. You can create a subscription for it now, but no events are delivered yet.
Fires when the recipient's mail server accepts an email. Delivery events will be batched — a single POST may contain deliveries for many contacts. Includes the accepting server and its response in addition to the common fields.
**Additional fields:**
| Field | Type | Description |
|---|---|---|
| `relay` | string | The host name of the mail server that accepted the message, for example `mx.example.com`. |
| `response` | string or null | The accepting server's response, up to 255 characters. `null` if not available. |
For this event, `timestamp` is the time the recipient's server accepted the message.
**Example scenario:** Northfield University subscribes to `email_delivery` to record proof of delivery for each tuition deadline notice sent to students.
```json
[
{
"activity_type": "email_delivery",
"timestamp": 1761148803,
"message_id": 2051,
"message_external_id": "3c7b9e2f1a4d6c8a0e2b4f6d8a0c2e4b",
"message_name": "Fall Registration Deadline Reminder",
"message_subject": "Action required: course registration closes Friday",
"sender": "Northfield Registrar ",
"contact_id": 84291,
"contact_external_id": "3f8a1c042d7e9b56a0e14c3d8f2b5a91",
"contact_email": "m.torres@students.northfield.edu",
"contact_remote_id": "NF-STU-2024-00847",
"relay": "mx.example.com",
"response": "250 2.0.0 OK 1761148803 x.y - gsmtp"
}
]
```
---
*Full reference, OpenAPI description, and Postman collection: [developers.envoke.com](https://developers.envoke.com).*
---
URL: https://envoke-developers.pages.dev/email-html/overview/
Markdown: https://envoke-developers.pages.dev/email-html/overview.md
# Writing Your Own Email HTML
These pages are for developers and designers who write email HTML themselves and bring it into Envoke through the Templates, Messages, or Send API, or paste it into the Edit HTML page. They explain what the HTML needs so that it can be edited in Envoke's drag-and-drop editor, and what Envoke does to it between your code and the inbox.
---
## Two Editors
Every message and template in Envoke opens in one of two editors, and when HTML arrives through the API, one line decides which:
```html
```
| Your HTML | Opens in |
|---|---|
| Contains `` or single quotes do not match. The `content` value is never read; `230609` is the value Envoke's own templates use.
The meta tag only chooses the editor. For the sections and blocks to be editable, the HTML also needs the structure described in [Making HTML Editable](https://envoke-developers.pages.dev/email-html/editable-templates/).
---
## What Envoke Does to Your HTML
| Stage | What happens | Read more |
|---|---|---|
| Saved through the API or the Edit HTML page | Stored exactly as sent. Nothing is checked or cleaned, apart from the APIs' 200 KB size limit. | [Getting HTML into Envoke](https://envoke-developers.pages.dev/email-html/getting-html-into-envoke/) |
| Opened in the drag-and-drop editor | The editor adds the classes and attributes it needs and replaces Envoke's style block with its current version. | [What the Editor Adds and Changes](https://envoke-developers.pages.dev/email-html/editable-templates/#what-the-editor-adds-and-changes) |
| Saved in the drag-and-drop editor | The document is rewritten: a standard doctype and `` tag, `
```
- `TEMPLATE_VERSION` makes the HTML open in the drag-and-drop editor (see [Two Editors](https://envoke-developers.pages.dev/email-html/overview/#two-editors)). Copy it exactly.
- `{@msg-subject}`, written exactly like this, is replaced by the message's subject when the email is sent.
- The style block with `id="envoke-styles"` gives your HTML the same base styles and mobile rules the editor uses. Copy it from [Envoke's Style Block](https://envoke-developers.pages.dev/email-html/css-and-mobile/#envokes-style-block). The editor replaces it with its current version every time the message is opened, so the copy you ship only matters until then, for example for HTML sent straight from the Send API.
- Do not add other `
Hello {fname},
Here is what is new this month.
|
|
|
|
|