Contact API - Insert
Use this endpoint to create a new contact in your account, or, with upsert, to create the contact or update it if it already exists.
Endpoint
Section titled “Endpoint”POST https://e1.envoke.com/v1/contactsAuthentication is via HTTP Basic Auth using your API ID and API Key. The request body must be a single JSON object.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
key_field |
string | email |
The field used to check whether the contact already exists: email or remote_id. Any other value is a validation error. |
upsert |
boolean | false |
When 1, true, yes, or on, an existing contact matched by key_field is updated instead of rejected. An empty value (?upsert=) is false. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Email address. Required even when key_field=remote_id. |
remote_id |
string | With key_field=remote_id |
Your own identifier for the contact. Max 60 characters. |
first_name |
string | No | Max 50 characters. |
last_name |
string | No | Max 50 characters. |
title |
string | No | Max 50 characters. |
company |
string | No | Max 80 characters. |
phone |
string | No | Max 25 characters. |
address_1 |
string | No | Max 100 characters. |
address_2 |
string | No | Max 100 characters. |
city |
string | No | Max 50 characters. |
country |
string | No | ISO code or country name. Unrecognised values produce a warning. |
province |
string | No | Max 50 characters. |
postal_code |
string | No | Max 15 characters. |
website |
string | No | Max 255 characters. |
language |
string | No | en or fr (or English, French). Other values produce a warning. |
feedback |
string | No | Free-text notes. HTML is stripped. |
consent_status |
string | No | A writable consent type. Defaults to Not Provided. |
consent_description |
string | No | How consent was obtained. Max 500 characters. |
consent_effective_date |
datetime | No | When consent was given; not in the future. Defaults to now. |
consent_force_downgrade |
boolean | No | Has no effect on a new contact. Applies when an upsert updates an existing contact (see Update). |
custom_fields |
object | No | Values keyed by custom field name. |
interests |
array or object | No | Tags, subscriptions, and mandatory consent items to set. |
autoresponders |
array or object | No | Nurture campaigns to add and queue. |
id, external_id, date_created, and consent_expiry are read-only and silently ignored. See the Fields reference for accepted values and the full rules for each field.
Note: Custom field values that cannot be stored and nurture campaigns that are skipped for lack of consent are not reported; the contact is still created and the response reports success.
Response
Section titled “Response”On success, the API returns the new contact in full, in the same shape as a read:
{ "success": true, "result_text": "The contact was successfully created.", "result_data": { "id": "1005", "external_id": "4b8d2f6a0c1e3a5b7d9f1e3c5a7b9d0f", "date_created": "2026-10-05T15:20:44+00:00", "first_name": "Priya", "last_name": "Nair", "email": "upsert.target@example.com", "consent_status": "Not Provided", "consent_effective_date": "2026-10-05T15:20:44+00:00", "consent_expiry": "", "custom_fields": {}, "interests": [], "autoresponders": [] }}(result_data shortened; the response contains every field, with "" for those not set.)
result_data.id is the Envoke contact ID. Store it if you need to update or delete the contact later.
Warnings. An unrecognised country or language value does not fail the request: the field is left empty, and a warning is appended to result_text after a space. Several warnings are joined with . :
The contact was successfully created. 'Narnia' is not a valid value for country; field will not be updated. 'de' is not a valid value for language; field will not be updatedWhen consent_status is an implied type that does not apply to an EU contact, a consent sentence is appended in the same way (see Consent fields and the example below).
Validation Errors
Section titled “Validation Errors”{ "success": false, "result_text": "Validation failed, see 'result_data' -> 'errors'", "result_data": { "errors": [ "Email is required" ] }}Common validation errors:
| Error | Cause |
|---|---|
Email is required |
email was missing or empty. |
Email must be a string |
email was a number, list, object, or boolean. |
Invalid 'key_field' value: 'external_id' |
key_field is not email or remote_id. |
Specified key_field 'remote_id' field was not provided. |
key_field=remote_id was set but the body has no remote_id. |
email value not unique ('alto.lead@example.com' duplicates existing contact with id: '1001') |
A contact with that email already exists and upsert is not set. |
Specified key_field 'remote_id' field value not unique ('HCS-0002' duplicates existing contact with id: '1002') |
As above, with key_field=remote_id. |
Multiple contacts already exist with 'remote_id' value 'HCS-0002'. (IDs: 1002, 1010) |
More than one contact has the key value. Returned even with upsert. More than 10 matches are shown as a count, e.g. (12 records). Note the two spaces before the bracket. |
'nickname' is not a valid contact field |
The body has a key that is not a contact field. |
Invalid 'consent_status': 'Opted In' (possible values: ...) |
Not a writable consent type. The message lists the valid values. |
consent_effective_date cannot be in the future. |
The effective date is later than now. |
Invalid date format for consent_effective_date |
The effective date could not be read. |
Invalid 'custom_field': 'Voice Range' (for a list of valid custom fields try the endpoint '/customfields') |
No custom field has that name. |
'custom_fields' must be an object |
custom_fields is not an object. |
Invalid 'interest': 'Volunteers' (for a list of valid interests try the endpoint '/interests') |
No tag, subscription, or mandatory consent item has that name. |
Invalid 'autoresponder': 'Welcome' (for a list of valid autoresponders try the endpoint '/autoresponders') |
No nurture campaign has that name. |
'interests' must be an object or array |
interests is a string or number. autoresponders gives 'autoresponders' must be an object or array. |
The email, key_field, and duplicate checks run first and return on their own. The remaining body errors in the table are then collected and returned together. Field lengths, email format, and the account’s duplicate check run afterwards and use the field-validation envelope, keyed by database column:
{ "success": false, "result_text": "Email invalid", "result_data": { "fields": { "email": "Email invalid" } }}result_text |
Cause |
|---|---|
Email invalid |
email is not a valid address. |
First name must not exceed 50 characters |
A field is too long. Key fname; see the column-name table for other keys. |
contact with email = alto.lead@example.com already exists |
With key_field=remote_id, the email belongs to another contact and your account checks emails for duplicates. result_data.fields is []. |
A missing, empty, or {} body returns {"success":false,"result_data":[],"result_text":"Invalid request"}. Invalid JSON returns Invalid JSON in the same shape.
Examples
Section titled “Examples”Create a contact with only an email address
Section titled “Create a contact with only an email address”curl -X POST -gs \ -u "$API_ID:$API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "to.delete@example.com"}' \ "https://e1.envoke.com/v1/contacts"The contact is created with consent_status Not Provided.
Create a full contact with express consent
Section titled “Create a full contact with express consent”curl -X POST -gs \ -u "$API_ID:$API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "alto.lead@example.com", "first_name": "Maya", "last_name": "Okafor", "title": "Section Lead", "phone": "416-555-0142", "address_1": "48 Harbour Lane", "address_2": "Unit 5", "city": "Toronto", "province": "ON", "postal_code": "M5V 0Z9", "country": "Canada", "language": "English", "consent_status": "Express", "consent_description": "Signed up at the spring concert", "consent_effective_date": "2026-09-12T18:30:00Z"}' \ "https://e1.envoke.com/v1/contacts"country is returned as "CA" and language as "en".
Create with custom fields, tags, and a subscription
Section titled “Create with custom fields, tags, and a subscription”curl -X POST -gs \ -u "$API_ID:$API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "alto.lead@example.com", "consent_status": "Express", "custom_fields": {"Voice_Part": "Alto", "Season_Ticket_Number": "HCS-ST-0417"}, "interests": ["Volunteer", "Alumni", "Concert News"], "autoresponders": ["Welcome Sequence"]}' \ "https://e1.envoke.com/v1/contacts"Create keyed on your own ID
Section titled “Create keyed on your own ID”curl -X POST -gs \ -u "$API_ID:$API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "tenor.remote@example.com", "remote_id": "HCS-0002", "first_name": "Daniel", "last_name": "Reyes"}' \ "https://e1.envoke.com/v1/contacts?key_field=remote_id"The existence check uses remote_id. If your account checks emails for duplicates, an email that belongs to another contact still fails with contact with email = ... already exists.
Caution: the parameter is key_field, not keyfield. An older version of the support article spelled it keyfield. That parameter is ignored without an error, so ?keyfield=remote_id checks for an existing contact by email, and with upsert it updates whichever contact has that email.
Duplicate email
Section titled “Duplicate email”curl -X POST -gs \ -u "$API_ID:$API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "alto.lead@example.com"}' \ "https://e1.envoke.com/v1/contacts"{ "success": false, "result_text": "Validation failed, see 'result_data' -> 'errors'", "result_data": { "errors": ["email value not unique ('alto.lead@example.com' duplicates existing contact with id: '1001')"] }}Upsert: create when the contact does not exist
Section titled “Upsert: create when the contact does not exist”curl -X POST -gs \ -u "$API_ID:$API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "upsert.target@example.com", "first_name": "Priya", "last_name": "Nair"}' \ "https://e1.envoke.com/v1/contacts?upsert=1"No contact has that email, so it is created: result_text is The contact was successfully created. and result_data is the new contact (see Response).
Upsert: update when the contact exists
Section titled “Upsert: update when the contact exists”curl -X POST -gs \ -u "$API_ID:$API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "upsert.target@example.com", "company": "Harbourfront Choral Society", "consent_status": "Express"}' \ "https://e1.envoke.com/v1/contacts?upsert=1"The same request now matches the existing contact and updates it: result_text is The contact was successfully updated. and result_data is the full, updated contact. An upsert that updates behaves like an update, with these differences:
- Empty values are ignored and
write_empty_valueshas no effect on POST. A custom field value of""still deletes that value. - A missing
consent_statusis not defaulted toNot Provided; the contact keeps its current status. idin the body is ignored, so an upsert never matches byid. It matches byemail, or byremote_idwithkey_field=remote_id.- Warnings are appended after
., giving a double period:The contact was successfully updated.. 'Narnia' is not a valid value for country; field will not be updated.
EU contact with implied consent
Section titled “EU contact with implied consent”curl -X POST -gs \ -u "$API_ID:$API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "paris.member@example.org", "first_name": "Camille", "last_name": "Laurent", "country": "France", "consent_status": "Implied - Inquiry"}' \ "https://e1.envoke.com/v1/contacts"Implied consent does not apply in the EU region, so the contact is created with Not Provided instead:
{ "success": true, "result_text": "The contact was successfully created. Their consent status was set to Not Provided because Implied - Inquiry does not apply to contacts in the EU.", "result_data": { "id": "1003", "email": "paris.member@example.org", "country": "FR", "consent_status": "Not Provided", "consent_description": "Implied - Inquiry consent does not apply to contacts in the EU.", "consent_expiry": "" }}(result_data shortened; the response contains every field.)
Note: The curl examples above are formatted for readability. All requests must use properly encoded URLs.
Full reference, OpenAPI description, and Postman collection: developers.envoke.com.

