# 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:** `{ "<custom field name>": "<value>" }`
- **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).*
