# 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

```
POST https://e1.envoke.com/v1/contacts
```

Authentication is via HTTP Basic Auth using your API ID and API Key. The request body must be a single JSON object.

### 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

| 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](https://envoke-developers.pages.dev/v1/contacts/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](https://envoke-developers.pages.dev/v1/contacts/fields/) 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

On success, the API returns the new contact in full, in the same shape as a [read](https://envoke-developers.pages.dev/v1/contacts/read/#response):

```json
{
  "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 updated
```

When `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](https://envoke-developers.pages.dev/v1/contacts/fields/#eu-contacts) and the example below).

---

## Validation Errors

```json
{
  "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:

```json
{
  "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](https://envoke-developers.pages.dev/v1/contacts/fields/#error-responses) 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

### Create a contact with only an email address

```bash
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

```bash
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

```bash
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

```bash
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

```bash
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"
```

```json
{
  "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

```bash
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](#response)).

### Upsert: update when the contact exists

```bash
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](https://envoke-developers.pages.dev/v1/contacts/update/), with these differences:

- Empty values are ignored and `write_empty_values` has no effect on POST. A custom field value of `""` still deletes that value.
- A missing `consent_status` is not defaulted to `Not Provided`; the contact keeps its current status.
- `id` in the body is ignored, so an upsert never matches by `id`. It matches by `email`, or by `remote_id` with `key_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

```bash
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:

```json
{
  "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](https://developers.envoke.com).*
