> ## Documentation Index
> Fetch the complete documentation index at: https://developers.benchmarkemail.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Manage Contacts

> Create, read, update, and delete contacts using the API.

Create, read, update, and delete contacts in your Benchmark Email account using the API.

## Goal

By the end of this guide you will be able to perform full CRUD operations on contacts: retrieve your contact structure (to get field IDs), create new contacts, retrieve them individually, update their fields or status, and delete them.

## Prerequisites

* An API key with **`contacts:write`** scope (for full CRUD) or **`contacts:read`** scope (for read-only access)
* Your API base URL (found on the Account Settings > API Keys page)

## About contact status

Every contact has a `status` field that tracks whether they're reachable. The shape depends on the contact's state.

**Active contacts** have only a primary status:

```json theme={null}
"status": {
  "primary": "Active"
}
```

**Inactive contacts** have both a primary status and a secondary status. The secondary explains *why* the contact is inactive:

```json theme={null}
"status": {
  "primary": "Inactive",
  "secondary": "Unsubscribe"
}
```

A few rules to know:

* **Status values are case-sensitive.** `Active`, `Inactive`, and `Unsubscribe` must use exact capitalization. Lowercase values such as `"active"` are rejected with a validation error.
* **Customers can only set `Unsubscribe` as the secondary value via the API.** This is the only customer-initiated reason for inactivating a contact. Other secondary values appear on contacts the system has inactivated automatically based on email events, and cannot be set through API calls. The values you may see on contacts the system has inactivated include:
  * `Bounce` — the contact's email address hard-bounced
  * `Complaint-FBL` — the contact's mailbox provider reported a spam complaint via the Feedback Loop (FBL) mechanism
* **When transitioning a contact to Inactive, both `primary` and `secondary` are required.** Sending only `primary: "Inactive"` returns a validation error.
* **Once a contact is Inactive, it cannot be reactivated via the API.** This is a deliberate compliance protection — customers who unsubscribe or hard-bounce should not be re-emailed. To re-add a contact, they must opt in again through your sign-up flow.

## Steps

### 1. Fetch your contact structure

Before creating contacts, retrieve your contact structure to get the field IDs you will need. Every contact belongs to a contact structure, and field values are set by referencing each field's `_id`.

```bash theme={null}
curl "<your-api-base-url>/api/contact-structure" \
  -H "X-API-Key: bme_us_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v"
```

**Response** (`200 OK`):

```json theme={null}
[
  {
    "_id": "64a1b2c3d4e5f6a7b8c9d0e1",
    "label": "Default Contacts",
    "keyName": "Email",
    "keyType": "email",
    "fields": [
      { "_id": "64a1b2c3d4e5f6a7b8c9d100", "label": "First Name", "dataType": "text", "predefinedField": "firstName" },
      { "_id": "64a1b2c3d4e5f6a7b8c9d101", "label": "Last Name", "dataType": "text", "predefinedField": "lastName" }
    ],
    "tags": [
      { "_id": "64a1b2c3d4e5f6a7b8c9d300", "label": "VIP" }
    ]
  }
]
```

Save the contact structure `_id` and the field `_id` values. You will use these when creating and updating contacts. See [Manage Custom Fields](./manage-custom-fields.md) for more details on contact structures.

### 2. Create a contact

Send a `POST` request with the contact's email address (`key`), the contact structure it belongs to, and optionally any custom field values and list assignments.

```bash theme={null}
curl -X POST "<your-api-base-url>/api/contact" \
  -H "X-API-Key: bme_us_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "jane.smith@example.com",
    "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1",
    "fields": [
      { "_id": "64a1b2c3d4e5f6a7b8c9d100", "value": "Jane" },
      { "_id": "64a1b2c3d4e5f6a7b8c9d101", "value": "Smith" }
    ],
    "lists": [
      { "_id": "64a1b2c3d4e5f6a7b8c9d200" }
    ]
  }'
```

**Response** (`200 OK`):

```json theme={null}
{
  "_id": "66f1a6e4698f1bca60424901",
  "key": "jane.smith@example.com",
  "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1",
  "fields": [
    { "_id": "64a1b2c3d4e5f6a7b8c9d100", "value": "Jane" },
    { "_id": "64a1b2c3d4e5f6a7b8c9d101", "value": "Smith" }
  ],
  "lists": [
    { "_id": "64a1b2c3d4e5f6a7b8c9d200" }
  ],
  "tags": [],
  "status": {
    "primary": "Active"
  },
  "createdAt": "2026-03-28T14:30:00.000Z",
  "updatedAt": "2026-03-28T14:30:00.000Z",
  "__v": 0
}
```

Responses also include `accountId`, `createdBy`, and `modifiedBy` fields, which are auto-populated by the system and read-only. They're omitted from the samples here for clarity.

**Key points:**

* `key` is the contact's email address (required)
* `contactStructureId` is required — it determines which custom fields are available
* `fields` uses the field `_id` from your contact structure (see Step 1)
* `lists` optionally assigns the contact to one or more lists at creation time
* New contacts start with `__v: 0`. You'll need this version value when updating the contact via PUT.

### 3. Get a contact by ID

Retrieve a single contact using its `_id`.

```bash theme={null}
curl "<your-api-base-url>/api/contact/66f1a6e4698f1bca60424901" \
  -H "X-API-Key: bme_us_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v"
```

**Response** (`200 OK`):

```json theme={null}
{
  "_id": "66f1a6e4698f1bca60424901",
  "key": "jane.smith@example.com",
  "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1",
  "fields": [
    { "_id": "64a1b2c3d4e5f6a7b8c9d100", "value": "Jane" },
    { "_id": "64a1b2c3d4e5f6a7b8c9d101", "value": "Smith" }
  ],
  "lists": [
    { "_id": "64a1b2c3d4e5f6a7b8c9d200" }
  ],
  "tags": [],
  "status": {
    "primary": "Active"
  },
  "createdAt": "2026-03-28T14:30:00.000Z",
  "updatedAt": "2026-03-28T14:30:00.000Z",
  "__v": 3
}
```

The `__v` field is the contact's current version. You'll need this exact value when updating the contact via PUT (see Step 5).

### 4. Update a contact's status

Use `PATCH` to update a contact's status. The most common case is unsubscribing a contact:

```bash theme={null}
curl -X PATCH "<your-api-base-url>/api/contact/66f1a6e4698f1bca60424901" \
  -H "X-API-Key: bme_us_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v" \
  -H "Content-Type: application/json" \
  -d '{
    "status": {
      "primary": "Inactive",
      "secondary": "Unsubscribe"
    }
  }'
```

**Response** (`200 OK`):

```json theme={null}
{
  "_id": "66f1a6e4698f1bca60424901",
  "key": "jane.smith@example.com",
  "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1",
  "fields": [
    { "_id": "64a1b2c3d4e5f6a7b8c9d100", "value": "Jane" },
    { "_id": "64a1b2c3d4e5f6a7b8c9d101", "value": "Smith" }
  ],
  "lists": [
    { "_id": "64a1b2c3d4e5f6a7b8c9d200" }
  ],
  "tags": [],
  "status": {
    "primary": "Inactive",
    "secondary": "Unsubscribe"
  },
  "createdAt": "2026-03-28T14:30:00.000Z",
  "updatedAt": "2026-03-28T15:10:00.000Z",
  "__v": 4
}
```

**Notes:**

* `PATCH` only accepts the `status` field. To update other fields (email, custom fields, lists, tags), use `PUT` (see Step 5).
* Both `primary` and `secondary` are required when transitioning a contact to Inactive.
* Inactive contacts cannot be reactivated via the API. See [About contact status](#about-contact-status) above.

### 5. Replace a contact (full update)

Use `PUT` to update any field on a contact: email, custom fields, lists, tags, or status. Unlike `PATCH`, `PUT` accepts the full contact body.

**Important:** PUT replaces the entire contact. Any field you omit from the request body will be removed from the contact's stored data. To avoid accidentally losing data, follow these three steps:

1. (If needed) Find the contact's `_id` via [Search Contacts](./search-contacts.md).
2. Send a `GET` request to retrieve the contact's full data, including its current `__v` version.
3. Send a `PUT` request using the GET response as your starting body. Change only the fields you want to update, and include `__v` exactly as you received it from the GET.

```bash theme={null}
curl -X PUT "<your-api-base-url>/api/contact/66f1a6e4698f1bca60424901" \
  -H "X-API-Key: bme_us_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "jane.smith@example.com",
    "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1",
    "fields": [
      { "_id": "64a1b2c3d4e5f6a7b8c9d100", "value": "Jane" },
      { "_id": "64a1b2c3d4e5f6a7b8c9d101", "value": "Smith-Jones" }
    ],
    "lists": [
      { "_id": "64a1b2c3d4e5f6a7b8c9d200" },
      { "_id": "64a1b2c3d4e5f6a7b8c9d201" }
    ],
    "tags": [],
    "status": {
      "primary": "Active"
    },
    "__v": 3
  }'
```

**Response** (`200 OK`):

```json theme={null}
{
  "_id": "66f1a6e4698f1bca60424901",
  "key": "jane.smith@example.com",
  "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1",
  "fields": [
    { "_id": "64a1b2c3d4e5f6a7b8c9d100", "value": "Jane" },
    { "_id": "64a1b2c3d4e5f6a7b8c9d101", "value": "Smith-Jones" }
  ],
  "lists": [
    { "_id": "64a1b2c3d4e5f6a7b8c9d200" },
    { "_id": "64a1b2c3d4e5f6a7b8c9d201" }
  ],
  "tags": [],
  "status": {
    "primary": "Active"
  },
  "createdAt": "2026-03-28T14:30:00.000Z",
  "updatedAt": "2026-03-28T16:00:00.000Z",
  "__v": 4
}
```

**Notes on `__v`:**

* `__v` is required on every PUT request. It's the contact's optimistic-concurrency version, which prevents one update from silently overwriting another.
* Always use the `__v` from a *fresh* GET — not a value cached from earlier. Successful PATCH or PUT calls increment `__v`, so a stale value will be rejected.
* If `__v` does not match the contact's current version, the API returns a `400 ConcurrencyError`. Re-run the GET, rebuild your PUT body, and retry.

**Notes on status in PUT:**

* Status follows the same rules as PATCH: capitalized values, both `primary` and `secondary` required for Inactive.
* For an Active contact, send `"status": { "primary": "Active" }` (no `secondary`).
* PUT cannot change an Inactive contact back to Active — see [About contact status](#about-contact-status).

### 6. Delete a contact

```bash theme={null}
curl -X DELETE "<your-api-base-url>/api/contact/66f1a6e4698f1bca60424901" \
  -H "X-API-Key: bme_us_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v"
```

**Response** (`200 OK`): Returns the deleted contact object.

### 7. List all contacts

Retrieve all contacts for your account. This endpoint returns all matching contacts in a single response (no pagination). For large contact databases, use [Search Contacts](./search-contacts.md) instead, which supports pagination and filtering.

```bash theme={null}
curl "<your-api-base-url>/api/contact" \
  -H "X-API-Key: bme_us_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v"
```

**Response** (`200 OK`):

```json theme={null}
[
  {
    "_id": "66f1a6e4698f1bca60424901",
    "key": "jane.smith@example.com",
    "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1",
    "fields": [
      { "_id": "64a1b2c3d4e5f6a7b8c9d100", "value": "Jane" },
      { "_id": "64a1b2c3d4e5f6a7b8c9d101", "value": "Smith" }
    ],
    "lists": [
      { "_id": "64a1b2c3d4e5f6a7b8c9d200" }
    ],
    "tags": [],
    "status": {
      "primary": "Active"
    },
    "createdAt": "2026-03-28T14:30:00.000Z",
    "updatedAt": "2026-03-28T14:30:00.000Z",
    "__v": 0
  }
]
```

## Common errors

| Status | Error                  | Cause                                                                                                          | Fix                                                                                                                                                                          |
| ------ | ---------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | `UnauthorizedError`    | Invalid, expired, or inactive API key                                                                          | Verify your key is correct and active in Settings > API Keys                                                                                                                 |
| `403`  | `ForbiddenError`       | Key lacks the required scope                                                                                   | Use a key with `contacts:write` for create/update/delete operations                                                                                                          |
| `400`  | `RequiredFieldError`   | A required field is missing from the request body                                                              | Check the error's `field` and `message` for the specific field name                                                                                                          |
| `400`  | `ModelValidationError` | A field value fails validation (wrong type, invalid enum value, missing case-paired field, blocked transition) | Read the `message` and `debug` for specifics. Common cases: lowercase status values, transitioning Inactive → Active, missing `secondary` when setting `primary: "Inactive"` |
| `400`  | `ConcurrencyError`     | The `__v` value in your PUT body does not match the contact's current version                                  | Run a fresh `GET`, rebuild your PUT body using the new `__v`, and retry                                                                                                      |
| `400`  | `DuplicateFieldError`  | A contact with the same email already exists in this structure                                                 | Use [Search Contacts](./search-contacts.md) to look up the existing contact, then update it instead of creating a new one                                                    |
| `404`  | `RecordNotFound`       | Contact ID does not exist                                                                                      | Verify the contact ID; it may have been deleted                                                                                                                              |
| `429`  | `TooManyRequestsError` | Rate limit or monthly quota exceeded                                                                           | Wait for the `Retry-After` period and retry. See [Rate Limits](../rate-limits.md)                                                                                            |

## Next steps

* [Search Contacts](./search-contacts.md) — find contacts by email or custom field values
* [Manage Lists](./manage-lists.md) — organize contacts into lists
* [Manage Custom Fields](./manage-custom-fields.md) — define the fields available on your contacts
* [Migration from Legacy](./migration-from-legacy.md) — end-to-end workflow for importing your data
