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
Section titled “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, and tags and subscriptions through the Tag API and Subscription API. 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 using your API ID and API Key.
Fields
Section titled “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
Section titled “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). 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.
- 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). 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).
external_id
Section titled “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
Section titled “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).
remote_id
Section titled “remote_id”- Type: String, max 60 characters
- Read-only: No
- Required on create: Only when
key_field=remote_idis 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
Section titled “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
Section titled “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.
- Type: String, max 50 characters
- Read-only: No
- Required on create: No
- Example:
"Section Lead"
Too long: Title must not exceed 50 characters.
- Type: String, max 60 characters
- Read-only: No
- Required on create: Yes, including when
key_field=remote_idis 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
Section titled “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.
- 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
Section titled “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
Section titled “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.
- Type: String, max 50 characters
- Read-only: No
- Required on create: No
- Example:
"Toronto"
Too long: City must not exceed 50 characters.
country
Section titled “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): '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); a contact with no country is treated as being in your account’s default consent country.
province
Section titled “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
Section titled “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
Section titled “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
Section titled “language”- Type: String
- Read-only: No
- Required on create: No
- Accepted values (write):
enorfr, or the namesEnglishorFrench. Case-insensitive. - Returned value (read):
"en","fr", or"". - Example:
"en"
Any other value is not saved and produces a warning (see Warnings): 'de' is not a valid value for language; field will not be updated.
feedback
Section titled “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
Section titled “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 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_description
Section titled “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
Section titled “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). - 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
Section titled “consent_expiry”- Type: String (
YYYY-MM-DD) - Read-only: Yes — calculated from
consent_statusandconsent_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 for when it is blank.
consent_force_downgrade
Section titled “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). Sent on its own, it does nothing.
custom_fields
Section titled “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). |
| Filter and sort | custom_fields.{name} (see 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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “interests and autoresponders”interests
Section titled “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 and Subscription API 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
Section titled “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.
| 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
Section titled “Error Responses”Validation errors (unknown fields, invalid values, missing required fields, bad filters):
{ "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.
{ "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
Section titled “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
Section titled “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:
{ "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 bodyidsent withoutkey_fieldis not looked up first, so a missing ID gives the validation error shown forPATCH /contacts/{id}. - PATCH
/contacts(list body) — the request returns HTTP400and the missing contact is reported inresult_dataunderCONTACT_OBJECT_{n}(see 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 noresult_datakey.
Full reference, OpenAPI description, and Postman collection: developers.envoke.com.

