# 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 <boxoffice@example.org>", "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).*
