# Contact API - Update

Use these endpoints to update existing contacts. You can update one contact by its ID, one contact found by email or another key field, or several contacts in one request.

---

## Endpoints

| Method | URL | Description |
|---|---|---|
| PATCH | `https://e1.envoke.com/v1/contacts/{ID}` | Update one contact by ID. |
| PATCH | `https://e1.envoke.com/v1/contacts` | Update one contact found by key field (object body), or several contacts (list body). |

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

### Query Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `write_empty_values` | boolean | `false` | When `1`, `true`, `yes`, or `on`, an empty string (`""`) clears a field. Otherwise empty values are ignored (see [Clearing Values](#clearing-values)). |
| `key_field` | string | *(see below)* | `id`, `email`, or `remote_id`. Used only by `PATCH /v1/contacts` with an object body. |

### Which update is performed

| URL | Body | Update |
|---|---|---|
| `/v1/contacts/{ID}` | JSON object | [Update by ID](#update-by-id) |
| `/v1/contacts` | JSON list of objects | [Update several contacts](#update-several-contacts) |
| `/v1/contacts` | JSON object | [Update one contact found by key field](#update-one-contact-found-by-key-field) |

A list body sent to `/v1/contacts/{ID}` is treated as an update of several contacts, and the `{ID}` in the URL is ignored.

---

## Request Body

Send only the fields you want to change. Any writable field from the [Fields reference](https://envoke-developers.pages.dev/v1/contacts/fields/) can be included, with the same accepted values as on [create](https://envoke-developers.pages.dev/v1/contacts/insert/). Each field is compared with the stored value, and only fields that differ are written; a request that changes nothing still succeeds.

`external_id`, `date_created`, and `consent_expiry` are read-only and silently ignored. `id` is used to identify the contact, never written.

Note: Custom field values that cannot be stored, nurture campaigns skipped for lack of consent, and consent changes that are preempted or blocked are not reported; the response still reports success.

---

## Update by ID

```
PATCH https://e1.envoke.com/v1/contacts/{ID}
```

**Request.** A JSON object. Replace `{ID}` with the Envoke contact ID. `key_field` is ignored. If the body includes `id`, it must match `{ID}` (see [ID Mismatch](#id-mismatch)).

**Response.** A short result, not the full contact. `result_data.id` is an integer on this endpoint, and `result_data.warnings` lists any `country` or `language` warnings (`[]` when none):

```json
{
  "success": true,
  "result_text": "The contact was successfully updated.",
  "result_data": {
    "id": 1001,
    "submit_type": "update",
    "warnings": []
  }
}
```

When no profile column changed (only custom fields, tags, nurture campaigns, or consent, or nothing at all), `result_data` also contains `"no_affected_rows": true`. When an EU country change resets the contact's consent, `result_data` also contains `inapplicable_consent_updated` and `consent_fix_message` (see [example](#change-country-to-an-eu-country)).

**Errors.**
- **Not found:** validation envelope with `No contact was found with ID: '999999'`.
- **Body errors** (unknown field, invalid value): validation envelope with `result_data.errors`.
- **Field validation** (length, email format, duplicate): the field-validation envelope, with an extra `warnings` key: `{"success":false,"result_data":{"fields":{"fname":"First name must not exceed 50 characters"},"warnings":[]},"result_text":"First name must not exceed 50 characters"}`.
- **ID mismatch:** `{"success":false,"result_data":[],"result_text":"Contact 'id': 1001 does not match 'id' in request body (1002)"}`.

---

## Update several contacts

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

**Request.** A JSON list of contact objects. Each object is matched on its own:
- If it has an `id`, that ID is used directly (it is not looked up first).
- Otherwise it is matched by `email`.

`key_field` is ignored for list bodies, so contacts cannot be matched by `remote_id` this way. Each object is then updated exactly as in [Update by ID](#update-by-id).

**Response.** When every contact is updated, HTTP `200`. The envelope has two extra top-level keys, `successes` and `failures`, listing contact IDs, and `result_data` holds each contact's result keyed by ID:

```json
{
  "success": true,
  "successes": ["1006", "1007"],
  "failures": [],
  "result_data": {
    "1006": { "id": 1006, "submit_type": "update", "warnings": [] },
    "1007": { "id": 1007, "submit_type": "update", "warnings": [] }
  },
  "result_text": "All contacts were successfully updated"
}
```

**Partial failure.** If any contact fails, the whole response is HTTP `400` with `success: false`, even though the other contacts were updated. The failed ones are listed in `failures`:
- A contact that could not be found is keyed `CONTACT_OBJECT_{n}`, where `{n}` is its zero-based position in the list, with the lookup error in `errors`.
- A contact that was found but failed validation is keyed by its ID, with the same `errors` or `fields` and `warnings` content as [Update by ID](#update-by-id).

```json
{
  "success": false,
  "successes": ["1006", "1007"],
  "failures": ["CONTACT_OBJECT_2"],
  "result_data": {
    "1006": { "id": 1006, "submit_type": "update", "warnings": [] },
    "1007": { "id": 1007, "submit_type": "update", "warnings": [] },
    "CONTACT_OBJECT_2": {
      "errors": ["No contact was found with the 'email': 'missing.member@example.com'"]
    }
  },
  "result_text": "2 contacts were successfully updated; 1 contacts were not updated"
}
```

**Errors** (per contact, in `result_data`):
- `No contact was found with the 'email': '{email}'`
- `No contact was found with ID: '{id}'` (an `id` that does not exist)
- `Multiple contacts were found with the 'email': '{email}'.  (IDs: 1006, 1012)` (two spaces before the bracket)
- `An 'id' or 'email' (or alternate field specified by key_field query parameter) must be provided in order to update a contact`

---

## Update one contact found by key field

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

**Request.** A JSON object. The contact is found by `key_field`:

| `key_field` | Lookup |
|---|---|
| *(not set)* | By `id` if the body has one (used directly, not looked up first); otherwise by `email`. |
| `id` | By `id`, looked up. |
| `email` | By `email`. |
| `remote_id` | By `remote_id`. |

Because the key value is used only to find the contact, a body with `id` and a new `email` (and no `key_field`) changes that contact's email address.

**Response.** The full, updated contact, in the same shape as a [read](https://envoke-developers.pages.dev/v1/contacts/read/). Warnings are appended to `result_text` after `. `, which follows the success message's own period:

```json
{
  "success": true,
  "result_text": "The contact was successfully updated.. 'Narnia' is not a valid value for country; field will not be updated",
  "result_data": {
    "id": "1002",
    "email": "tenor.remote@example.com",
    "remote_id": "HCS-0002",
    "first_name": "Dan",
    "country": ""
  }
}
```

*(`result_data` shortened; the response contains every field.)*

**Errors.** Lookup failures use a plain envelope with `result_data: []`, not the validation envelope:

| `result_text` | Cause |
|---|---|
| `No contact was found with the 'email': 'nobody@example.com'` | No contact has that key value. With `key_field=id`, the message names `'id'`. |
| `Multiple contacts were found with the 'remote_id': 'HCS-0002'.  (IDs: 1002, 1010)` | More than one contact has the key value. |
| `Specified key_field 'remote_id' field was not provided.` | `key_field` is set but the body does not include that field. |
| `An 'id' or 'email' (or alternate field specified by key_field query parameter) must be provided in order to update a contact` | No `key_field`, and the body has neither `id` nor `email`. |
| `Invalid 'key_field' value: 'external_id'` | `key_field` is not `id`, `email`, or `remote_id`. |
| `The specified contact ID 1002 does not match record identified by key_field 'email': 'alto.lead@example.com' (ID: 1001)` | See [ID Mismatch](#id-mismatch). |

Once the contact is found, update errors are the same as for [Update by ID](#update-by-id). When the body has an `id` and no `key_field`, a missing ID gives the validation envelope with `No contact was found with ID: '{id}'`.

---

## Clearing Values

| Value sent | Without `write_empty_values` | With `write_empty_values=1` |
|---|---|---|
| `null` | Ignored | Ignored |
| `""`, `0`, `"0"`, `false`, `[]` | Ignored | `""` clears a text field |
| Custom field value `""` | Deletes the custom field value | Deletes the custom field value |

- A custom field is cleared by sending `""` as its value inside `custom_fields`. This works with or without `write_empty_values`.
- `email` cannot be cleared: `Email must not be empty`.
- `country` and `language` cannot be cleared; an empty value leaves them unchanged.
- Tags, subscriptions, and nurture campaigns are removed with the object form (`{ "Alumni": "Unset" }`), not by sending an empty list.

---

## Consent Changes

Consent is written only when `consent_status`, `consent_description`, or `consent_effective_date` differs from the stored value. When one of them changes, all the consent fields in the body are applied together, and the contact's current status is used if `consent_status` was not sent. `consent_force_downgrade` on its own does nothing.

Whether a new status becomes current follows the transition rules in [Consent fields](https://envoke-developers.pages.dev/v1/contacts/fields/#consent-fields). A downgrade (for example `Express` to an implied type) is recorded in the contact's history but not made current unless `consent_force_downgrade` is `true`. A change away from a spam-reported status, or from `Revoked` to `Not Provided`, is blocked.

Note: A preempted or blocked consent change is not reported. The response is the normal success message, and the contact's `consent_status` is unchanged.

---

## ID Mismatch

- **`PATCH /v1/contacts/{ID}`:** if the body includes `id` and it differs from `{ID}`, the request fails with `{"success":false,"result_data":[],"result_text":"Contact 'id': 1001 does not match 'id' in request body (1002)"}`. `"1001"` and `1001` count as the same.
- **`PATCH /v1/contacts` with `key_field=email` or `remote_id`:** if the body includes `id` and it differs from the contact the key field found, the request fails with `{"success":false,"result_data":[],"result_text":"The specified contact ID 1002 does not match record identified by key_field 'email': 'alto.lead@example.com' (ID: 1001)"}`.

---

## Validation Errors

```json
{
  "success": false,
  "result_text": "Validation failed, see 'result_data' -> 'errors'",
  "result_data": {
    "errors": [
      "No contact was found with ID: '999999'"
    ]
  }
}
```

Common errors:

| Error | Cause |
|---|---|
| `Invalid request` | No body, an empty body, `{}`, or `[]`. Plain envelope with `result_data: []`. |
| `Invalid JSON` | The body is not valid JSON. Plain envelope with `result_data: []`. |
| `No contact was found with ID: '{id}'` | The ID does not match any contact in your account. |
| `'nickname' is not a valid contact field` | The body has a key that is not a contact field. |
| `Email must not be empty` | `email` was sent as `""` with `write_empty_values=1`. Field-validation envelope. |
| `contact with email = alto.lead@example.com already exists` | The new email belongs to another contact. Field-validation envelope. |

The body validation errors listed for [create](https://envoke-developers.pages.dev/v1/contacts/insert/#validation-errors) (consent status, effective date, custom fields, interests, autoresponders) apply to updates too.

---

## Examples

### Update by ID

```bash
curl -X PATCH -gs \
  -u "$API_ID:$API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "Alto Section Lead", "phone": "416-555-0199"}' \
  "https://e1.envoke.com/v1/contacts/1001"
```

### Update by email

```bash
curl -X PATCH -gs \
  -u "$API_ID:$API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "alto.lead@example.com", "company": "Harbourfront Choral Society"}' \
  "https://e1.envoke.com/v1/contacts"
```

Returns the full contact in `result_data`.

### Update by remote ID

```bash
curl -X PATCH -gs \
  -u "$API_ID:$API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"remote_id": "HCS-0002", "first_name": "Dan"}' \
  "https://e1.envoke.com/v1/contacts?key_field=remote_id"
```

### Clear a value

```bash
curl -X PATCH -gs \
  -u "$API_ID:$API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"address_2": ""}' \
  "https://e1.envoke.com/v1/contacts/1001?write_empty_values=1"
```

Without `write_empty_values=1` the same request succeeds but leaves `address_2` unchanged.

### Clear a custom field

```bash
curl -X PATCH -gs \
  -u "$API_ID:$API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"custom_fields": {"Season_Ticket_Number": ""}}' \
  "https://e1.envoke.com/v1/contacts/1001"
```

### Remove a tag

```bash
curl -X PATCH -gs \
  -u "$API_ID:$API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"interests": {"Alumni": "Unset"}}' \
  "https://e1.envoke.com/v1/contacts/1001"
```

Other tags and subscriptions are left as they are.

### Stop a nurture campaign

```bash
curl -X PATCH -gs \
  -u "$API_ID:$API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"autoresponders": {"Welcome Sequence": "Dequeue"}}' \
  "https://e1.envoke.com/v1/contacts/1001"
```

### Force a consent downgrade

```bash
curl -X PATCH -gs \
  -u "$API_ID:$API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"consent_status": "Implied - Transaction", "consent_description": "Ticket purchase only; express consent withdrawn by phone", "consent_force_downgrade": true}' \
  "https://e1.envoke.com/v1/contacts/1001"
```

The contact had `Express` consent. Without `consent_force_downgrade` the request returns the same success message but `consent_status` stays `Express`.

### Update several contacts, one of which is missing

```bash
curl -X PATCH -gs \
  -u "$API_ID:$API_KEY" \
  -H "Content-Type: application/json" \
  -d '[{"email": "bulk.a@example.com", "city": "Hamilton"}, {"email": "bulk.b@example.com", "city": "Hamilton"}, {"email": "missing.member@example.com", "city": "Hamilton"}]' \
  "https://e1.envoke.com/v1/contacts"
```

The first two contacts are updated; the response is HTTP `400` (see [Partial failure](#update-several-contacts)).

### Change country to an EU country

```bash
curl -X PATCH -gs \
  -u "$API_ID:$API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"country": "Germany"}' \
  "https://e1.envoke.com/v1/contacts/1004"
```

The contact had `Implied - Inquiry` consent, which does not apply in the EU region, so it is set to `Not Provided`:

```json
{
  "success": true,
  "result_text": "The contact was successfully updated. Their consent status was updated because Implied - Inquiry consent does not apply to contacts in the EU.",
  "result_data": {
    "id": 1004,
    "submit_type": "update",
    "inapplicable_consent_updated": true,
    "consent_fix_message": " Their consent status was updated because Implied - Inquiry consent does not apply to contacts in the EU.",
    "warnings": []
  }
}
```

---

*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).*
