# 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://developers.envoke.com/guides/authentication/). The examples read the credentials from the `API_ID` and `API_KEY` shell variables set up in [Make a Request](https://developers.envoke.com/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://developers.envoke.com/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://developers.envoke.com/guides/authentication/#api-limits).
- **Tools.** The examples use `curl`, and step 3 also uses [`jq`](https://jqlang.org) 1.6 or later to send an HTML file as JSON.

---

## 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. Look each one up before creating it:

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" "https://e1.envoke.com/v2/customfields?filter%5Bname%5D=Plot_Number&fields=id,name"
curl -X GET -gs -u "$API_ID:$API_KEY" "https://e1.envoke.com/v2/tags?filter%5Bname%5D=Volunteer&fields=id,name"
```

Each returns `[]` when nothing has that name, or an array of the matches with their IDs. `filter[name]` matches the whole name; for custom fields it ignores letter case, so the lookup can return `plot_number`. Create only what is missing, and from then on use each name exactly as the API returns it.

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, in any letter case, fails with `A custom field named 'Plot_Number' already exists`. See [Custom Field API - Insert](https://developers.envoke.com/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://developers.envoke.com/v2/tags/insert/). Points to know about names:

- **Tag names are not unique.** The Tag API does not refuse a name that is already used, so creating `Volunteer` again gives you a second tag with the same name. That is why the lookup comes first. If the lookup returns more than one tag, choose one and use its ID.
- **Refer to the tag by its ID.** The Contact API resolves the names in `interests` more loosely than `filter[name]`: it ignores case, treats a space and an underscore as the same, and looks at every kind of list in your account, including subscriptions, import lists, and poll answers, not only tags. A name can therefore pick an item other than your tag. Step 2 sets the tag by the ID returned here, `9101`, which always means this tag.
- **Refer to the custom field by its name.** The Contact API does not accept a custom field's ID; `custom_fields` is keyed by name (see [`custom_fields`](https://developers.envoke.com/v1/contacts/fields/#custom_fields)). Custom field names are unique in the account, whatever their letter case.

---

## 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": {"9101": "Set"}}' \
  "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 contact is given `Express` consent here because the Send API sends to an existing contact only when their consent allows sending (see [Consent fields](https://developers.envoke.com/v1/contacts/fields/#consent-fields)).

**Do not send consent on every sync.** Sending `consent_status` `Express` for a contact whose status is `Revoked`, because they unsubscribed, makes `Express` their current status again: the contact is opted back in, and the response does not say so. Send consent only when you have new consent from the person, such as a form they have just signed. When a sync runs from your own records, read the contact first and leave the consent fields out for a contact that already exists:

```bash
curl -X GET -gs \
  -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?filter%5Bremote_id%5D=BCG-0417&fields=id,consent_status"
```

The response is `[]` when no contact has the remote ID; create the contact with its consent, as above. Otherwise it is an array with the contact's `id` and current `consent_status`; send the upsert without `consent_status` and `consent_description`, which leaves the contact's consent unchanged. Leave out `limit=1` here: with it, a match is returned as a bare object instead of an array.

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`. That contact usually has no `remote_id` yet, for example because the person signed up through a form. Look it up with `GET https://e1.envoke.com/v1/contacts?filter%5Bemail%5D=ama.osei%40example.org&fields=id,email,remote_id`; if it is the same person, give it your identifier with `PATCH https://e1.envoke.com/v1/contacts?key_field=email` and the body `{"email": "ama.osei@example.org", "remote_id": "BCG-0417"}`, then repeat the upsert, which now updates it. See [Email already used by another contact](https://developers.envoke.com/v1/contacts/insert/#email-already-used-by-another-contact).
- **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`. A consent change that is not made current, or is blocked (for example for a contact who reported spam), is not reported in the response. See [Upsert: update when the contact exists](https://developers.envoke.com/v1/contacts/insert/#upsert-update-when-the-contact-exists) and [When a change takes effect](https://developers.envoke.com/v1/contacts/fields/#when-a-change-takes-effect).
- **`interests` adds unless told otherwise.** The object form `{"9101": "Set"}` adds the tag with ID `9101`; `{"9101": "Unset"}` removes it. The list form, `["Volunteer"]`, also works, by name, and only adds. In a read, `interests` lists names.
- **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://developers.envoke.com/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://developers.envoke.com/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://developers.envoke.com/v1/contacts/fields/#error-responses).

---

## Step 3: Create a Template

Write the email as HTML that Envoke's drag-and-drop editor can open, so that people in your team can change the wording in Envoke later without touching your code. Save it as `plot-assignment.html`:

```html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="TEMPLATE_VERSION" content="230609">
<title>{@msg-subject}</title>
<style type="text/css" id="envoke-styles"> .inner-column { line-height: 1.3; } .image-edit { line-height: 0; } .image-edit.image-empty { display: none; } a { text-decoration: none; } u a { text-decoration: underline; } p, h1, h2, h3, h4, li { margin: 0; padding: 0; margin-bottom: 0; } h1, h2, h3, h4 { font-weight: normal; } ul, ol { margin-top: 0; margin-bottom: 0; } .desktop-hide { display:none !important; } @media screen and (max-width: 540px) { table { width:100% !important; } td.block { width:100% !important; display:block !important; } img.responsive-width { width:100% !important; height:auto !important; margin:0 !important; } table.nvkButtonTable { width: auto !important; } .mobile-hide { display:none !important; } .desktop-hide { display:block !important; } .block-inner .inner-column { padding-left: 20px !important; padding-right: 20px !important; } .block-inner .inner-column.mobile-full-width { padding-left: 0 !important; padding-right: 0 !important; } p, li, p span, li span { font-size:16px !important; } .column-table-rows .inner-column { height: auto !important; } } </style>
</head>
<body class="nvkbody" style="padding: 0px; margin: 0px; background-color: #f4f4f4; color: #333333; font-family: Arial, Helvetica, sans-serif; font-size: 15px;">
<div class="nvkbody wrapper" style="width: 100%; background-color: #f4f4f4; color: #333333; font-family: Arial, Helvetica, sans-serif; font-size: 15px;">
<center>
<table class="layout-table" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td align="center" valign="top">
<table class="layout-table" align="center" cellpadding="0" cellspacing="0" width="680">
<tr>
<td align="center" valign="top">

<div class="block-outer">
  <table class="layout-table outer-table" align="center" cellpadding="0" cellspacing="0" width="660">
    <tr>
      <td class="block-inner-background block layout-column outer-column" style="background-color: #ffffff; color: #333333;" align="center" valign="top">
        <table class="layout-table" align="center" cellpadding="0" cellspacing="0" width="660">
          <tr>
            <td class="block-inner-background" style="background-color: #ffffff; color: #333333;" align="left" valign="top">

              <div class="block-inner">
                <table class="inner-table" align="center" cellpadding="0" cellspacing="0" width="660">
                  <tr>
                    <td class="inner-column" style="padding: 30px 20px;" align="left" valign="top">
                      <div class="content-edit" rel="#bck-overlay">
                        <p>Hi {fname},</p>
                        <p>&nbsp;</p>
                        <p>Your plot for the season is {@customfield-Plot_Number}. The garden opens on Saturday at 9:00 a.m.</p>
                      </div>
                    </td>
                  </tr>
                </table>
              </div>

              <div class="block-inner">
                <table class="inner-table" align="center" cellpadding="0" cellspacing="0" width="660">
                  <tr>
                    <td class="inner-column" style="padding: 20px; background-color: #f4f4f4;" align="left" valign="top">
                      <div class="content-edit" rel="#bck-overlay">
                        <p style="text-align: center;"><span style="font-size: 13px;">{@brand-address}</span></p>
                        <p style="text-align: center;"><span style="font-size: 13px;"><u><a href="{@pref-0}">Update your email preferences or unsubscribe</a></u></span></p>
                      </div>
                    </td>
                  </tr>
                </table>
              </div>

            </td>
          </tr>
        </table>
      </td>
    </tr>
  </table>
</div>

</td>
</tr>
</table>
</td>
</tr>
</table>
</center>
</div>
</body>
</html>
```

Create a template from the file. `jq` reads the file and builds the JSON body, escaping the HTML; any JSON library does the same:

```bash
jq -n --rawfile html plot-assignment.html '{name: "Plot Assignment", html: $html}' | \
  curl -X POST -gs \
  -u "$API_ID:$API_KEY" \
  -H "Content-Type: application/json" \
  -d @- \
  "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 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. Template names are not checked for uniqueness. See [Template API - Insert](https://developers.envoke.com/v2/templates/insert/).

What makes the HTML editable, and what it holds:

- **The editor's structure.** The `TEMPLATE_VERSION` meta tag makes the template open in the drag-and-drop editor, and the `block-outer`, `block-inner`, and `content-edit` classes mark the section, the two rows, and the regions a user can edit. With the meta tag but without these classes, the template opens in the editor with nothing that can be edited. [Making HTML Editable](https://developers.envoke.com/email-html/editable-templates/) explains the structure and has a complete template with an image and a button; the long `<style>` line is [Envoke's Style Block](https://developers.envoke.com/email-html/css-and-mobile/#envokes-style-block), copied as it is.
- **Merge tags.** `{fname}` is the contact's first name, `{@brand-address}` your account's postal address, and `{@pref-0}` the link to the contact's email preferences page, all filled in by Envoke when the email is sent (see [Merge Tags](https://developers.envoke.com/email-html/merge-tags/)). Because the HTML has its own `{@pref-0}` link, Envoke does not add its standard unsubscribe footer (see [The Footer](https://developers.envoke.com/email-html/unsubscribe-and-footer/)).
- **The custom field.** `{@customfield-Plot_Number}` is replaced with the contact's `Plot_Number` value from step 2, read when the email is sent. The name after `customfield-` must match the custom field's name exactly, including its capitalisation: `{@customfield-plot_number}` would be replaced with nothing (see [Custom Fields](https://developers.envoke.com/email-html/merge-tags/#custom-fields)).

---

## Step 4: Subscribe to the Result

Subscribe a webhook before sending, so that your endpoint hears about the email. Create one subscription per event type for the whole account, with no filter, and let your endpoint pick out the events it cares about. An `email_send` event fires when Envoke hands an email to its sending server for delivery 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/3f9c1e7a5b2d4c6e8a0f1b3d5e7a9c2b/email-activity"}' \
  "https://e1.envoke.com/v1/hooks.json"
```

The long random part of the URL's path is a secret shared only by Envoke and your endpoint, because deliveries carry no signature (see step 6). Envoke returns HTTP `201` with the subscription's ID:

```json
{"id": "4f6a8c0e2b4d6f8a1c3e5b7d9f1a3c5e"}
```

Your endpoint now receives an `email_send` event for every email your account sends, from the Send API and from Envoke itself. It routes each event by `contact_remote_id` (here `BCG-0417`), or by `message_id`, the message ID that the send request returns in step 5.

**Why not one filtered subscription per contact.** A subscription can have a `filter`, such as `{"contact_remote_id": "BCG-0417"}`, but a subscription is unique by `type` and `url`, whatever its filter. A second subscription for another contact on the same URL fails with HTTP `400`, `A webhook subscription with this type and URL already exists`, and the first subscription's ID in `result_data.id`, so the second contact's events never arrive. Use a filter to narrow all of the account's events, such as `{"bounce_type": "hard"}` on `email_bounce`, not to follow individual contacts.

Subscriptions cannot be changed; delete one and create another instead. Every delivered event counts towards your API limits, so subscribe only to the types you use. See [Hooks API - Create](https://developers.envoke.com/v1/hooks/insert/) and [`filter`](https://developers.envoke.com/v1/hooks/fields/#filter).

**Sent is not delivered.** `email_send` means the email has left Envoke, not that it reached the inbox: the recipient's mail server can still refuse it, and that fires an `email_bounce` event, sometimes well after the send. The `email_delivery` type, for the recipient's server accepting the email, is reserved for an upcoming release; a subscription to it is accepted but receives no events yet. Until then, subscribe to `email_bounce` as well, the same way and on the same URL (your endpoint tells the types apart by `activity_type`), and treat an email as delivered when it has an `email_send` event and no `email_bounce` event. The Reports API counts [`delivered`](https://developers.envoke.com/v2/reports/fields/#delivered) the same way, as sends minus bounces.

---

## 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": ["ama.osei@example.org"]}' \
  "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. Envoke fills in the merge tags as each email is sent, so the plot number comes from the contact record and does not need to be sent again here. `enable_merge_fields` is needed for `{@customfield-Plot_Number}` and `{@brand-address}`; `{fname}` and `{@pref-0}` are filled in either way. 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://developers.envoke.com/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://developers.envoke.com/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.** The Send API does not need an existing contact: an address that matches none is created as a contact, 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.
- **Custom field values are read at send time.** Each email shows the value stored on the contact when it is sent, and a contact with no value gets nothing in its place. Update the contact before sending, as in step 2. For text that belongs to one send only and is not kept on the contact, a recipient can carry its own [`replace`](https://developers.envoke.com/v1/send/fields/#replace) values instead.
- **Do not repeat a send blindly.** The Send API has no idempotency key: each request that succeeds creates a new message and sends it. A `429` response was refused before anything happened and can be sent again after a wait. But `Internal error; please contact Envoke support`, `Invalid request`, a `5xx` status, or no response at all can leave a message that already exists. 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; [Retrying a request](https://developers.envoke.com/v1/send/send/#retrying-a-request) shows how to find out who it reached before sending to anyone 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. Like `email_send`, `Sent` does not mean delivered.

---

## 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 <hello@example.org>",
    "contact_id": "9001",
    "contact_external_id": "5c1e9a3f7b2d4e6a8c0f1b3d5e7a9c2e",
    "contact_email": "ama.osei@example.org",
    "contact_remote_id": "BCG-0417"
  }
]
```

`message_id` is the `id` from step 5, as a string, and `contact_remote_id` is the identifier you gave the contact in step 2 (`""` for a contact without one). `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://developers.envoke.com/v1/hooks/payloads/#email_send).

Your endpoint should handle these rules (see [Delivery Format](https://developers.envoke.com/v1/hooks/payloads/#delivery-format)):

- **Read the body as an array.** `email_send` events are batched, so one POST can carry many events, for many contacts and for messages your integration did not send. Route each one by `contact_remote_id` or `message_id`, and ignore the rest.
- **Return a status below `400` within 60 seconds.** Anything else, or no answer, is retried once after 10 minutes, and then the events are dropped. A `410` response removes the subscription.
- **Expect repeats and late arrivals.** A retry delivers the same events again, with the same `timestamp`, and events are not guaranteed to arrive in order. Email events have no event ID: treat two events with the same `activity_type`, `message_id`, `contact_id`, and `timestamp` as one, and order events by `timestamp`.
- **Deliveries carry no signature or secret.** The request has no header that identifies it as coming from Envoke, so anyone who learns the URL can post to it. Keep the random secret in the URL's path, as in step 4, and answer requests to any other path with an error; treat each event as a notification, and confirm anything important through the API before acting on it.
- **Deliveries count towards your API limits.** Each event Envoke delivers counts as one request towards your account's [API limits](https://developers.envoke.com/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://developers.envoke.com/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://developers.envoke.com/v2/tags/contacts/) adds up to 1,000 contacts to a tag in one request.
- [Message Stats](https://developers.envoke.com/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.*
