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

# Search Contacts

> Find contacts by email, custom field values, or other criteria.

Find contacts by email address, custom field values, or other criteria using the search API.

## Goal

By the end of this guide you will be able to search for contacts by email (useful for dedup checks), filter by custom field values, and paginate through large result sets.

## Prerequisites

* An API key with **`contacts:read`** scope
* Your API base URL (found on the Settings > API Keys page)
* A **contact structure ID** — search operates within a single contact structure. Retrieve yours with `GET /api/contact-structure` (see [Manage Custom Fields](/scenarios/manage-custom-fields))

**Scope note:** Contact search uses `POST` but is semantically a read operation. It requires only `contacts:read` scope (not `contacts:write`).

## How search requests work

Every search request has two main parts: a **filter** that decides which contacts match, and a **source** array that decides which fields to return for each matching contact.

### The `source` array

`source` is a required projection that tells the API which fields to return in each contact. Pass an array containing one or more of these case-sensitive values:

* `_id`: the contact's unique identifier
* `key`: the contact's email address
* `fields`: custom field values
* `tags`: tag assignments
* `lists`: list memberships
* `contactStatus`: primary status (`Active`, `Inactive`, etc.)
* `contactSubStatus`: secondary status
* `createdAt`: creation timestamp
* `updatedAt`: last-update timestamp

Rules:

* `source` is required and must contain at least one value.
* Values are case-sensitive. `_id` works; `_ID` returns a validation error.
* `_id` is always returned, even when you don't include it in `source`.
* Any unknown value rejects the entire request. There's no partial success.

## Steps

### 1. Search by email address

This is the most common search pattern, useful for deduplication before creating a contact.

```bash theme={null}
curl -X POST "<your-api-base-url>/api/contact/search" \
  -H "X-API-Key: bme_us_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v" \
  -H "Content-Type: application/json" \
  -d '{
    "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1",
    "page": 1,
    "pageSize": 10,
    "source": ["_id", "key", "fields", "createdAt", "updatedAt"],
    "contactSpecification": {
      "filters": [
        {
          "criterias": [
            {
              "columnToFilter": "KEY",
              "operator": "EQ",
              "values": ["jane.smith@example.com"]
            }
          ]
        }
      ]
    }
  }'
```

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

```json theme={null}
{
  "contacts": [
    {
      "_id": "66f1a6e4698f1bca60424901",
      "key": "jane.smith@example.com",
      "fields": [
        { "_id": "64a1b2c3d4e5f6a7b8c9d100", "value": "Jane" },
        { "_id": "64a1b2c3d4e5f6a7b8c9d101", "value": "Smith" }
      ],
      "createdAt": "2026-03-28T14:30:00.000Z",
      "updatedAt": "2026-03-28T14:30:00.000Z",
      "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1"
    }
  ],
  "totalRecords": 1
}
```

**Dedup pattern:** Before creating a contact, search by email using `"operator": "EQ"`. If `totalRecords` is 0, the email is not in use and you can safely create the contact.

### 2. Search by email domain

Find all contacts from a specific email domain.

```bash theme={null}
curl -X POST "<your-api-base-url>/api/contact/search" \
  -H "X-API-Key: bme_us_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v" \
  -H "Content-Type: application/json" \
  -d '{
    "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1",
    "page": 1,
    "pageSize": 25,
    "source": ["_id", "key", "fields"],
    "contactSpecification": {
      "filters": [
        {
          "criterias": [
            {
              "columnToFilter": "EMAIL_DOMAIN",
              "operator": "EQ",
              "values": ["example.com"]
            }
          ]
        }
      ]
    }
  }'
```

### 3. Search by custom field values

Filter contacts based on custom field values. Use the field's `_id` from your contact structure.

```bash theme={null}
curl -X POST "<your-api-base-url>/api/contact/search" \
  -H "X-API-Key: bme_us_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v" \
  -H "Content-Type: application/json" \
  -d '{
    "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1",
    "page": 1,
    "pageSize": 25,
    "source": ["_id", "key", "fields", "tags"],
    "contactSpecification": {
      "filters": [
        {
          "criterias": [
            {
              "columnToFilter": "FIELD_ID",
              "id": "64a1b2c3d4e5f6a7b8c9d102",
              "operator": "EQ",
              "values": ["Enterprise"]
            }
          ]
        }
      ]
    }
  }'
```

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

```json theme={null}
{
  "contacts": [
    {
      "_id": "66f1a6e4698f1bca60424905",
      "key": "alice@enterprise-corp.com",
      "fields": [
        { "_id": "64a1b2c3d4e5f6a7b8c9d100", "value": "Alice" },
        { "_id": "64a1b2c3d4e5f6a7b8c9d101", "value": "Chen" },
        { "_id": "64a1b2c3d4e5f6a7b8c9d102", "value": "Enterprise" }
      ],
      "tags": [
        { "_id": "64a1b2c3d4e5f6a7b8c9d300" }
      ],
      "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1"
    }
  ],
  "totalRecords": 1
}
```

### 4. Combine multiple criteria

Use multiple criteria objects to build complex filters. Criteria within the same group are AND-ed; separate groups are OR-ed.

```bash theme={null}
curl -X POST "<your-api-base-url>/api/contact/search" \
  -H "X-API-Key: bme_us_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v" \
  -H "Content-Type: application/json" \
  -d '{
    "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1",
    "page": 1,
    "pageSize": 25,
    "source": ["_id", "key", "fields", "createdAt"],
    "contactSpecification": {
      "filters": [
        {
          "criterias": [
            {
              "columnToFilter": "FIELD_ID",
              "id": "64a1b2c3d4e5f6a7b8c9d102",
              "operator": "EQ",
              "values": ["Enterprise"]
            },
            {
              "columnToFilter": "CREATED_AT",
              "operator": "GT",
              "values": ["2026-01-01T00:00:00.000Z"]
            }
          ]
        }
      ]
    }
  }'
```

This searches for contacts where the custom field equals "Enterprise" **AND** the contact was created after January 1, 2026.

### 5. Search within a specific list

Filter contacts that belong to a specific list.

```bash theme={null}
curl -X POST "<your-api-base-url>/api/contact/search" \
  -H "X-API-Key: bme_us_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v" \
  -H "Content-Type: application/json" \
  -d '{
    "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1",
    "page": 1,
    "pageSize": 25,
    "source": ["_id", "key", "fields"],
    "contactSpecification": {
      "filters": [
        {
          "criterias": [
            {
              "columnToFilter": "LIST_ID",
              "operator": "IN",
              "values": ["66f1a6e4698f1bca60425001"]
            }
          ]
        }
      ]
    }
  }'
```

### 6. Paginate through results

For large result sets, increment the `page` parameter to retrieve subsequent pages. Pages are 1-indexed.

```bash theme={null}
# Page 1 (first 50 results)
curl -X POST "<your-api-base-url>/api/contact/search" \
  -H "X-API-Key: bme_us_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v" \
  -H "Content-Type: application/json" \
  -d '{
    "contactStructureId": "64a1b2c3d4e5f6a7b8c9d0e1",
    "page": 1,
    "pageSize": 50,
    "source": ["_id", "key"],
    "contactSpecification": {
      "filters": [
        {
          "criterias": [
            {
              "columnToFilter": "CONTACT_STATUS",
              "operator": "EQ",
              "values": ["active"]
            }
          ]
        }
      ]
    },
    "sortField": [
      {
        "column": "CREATED_AT",
        "order": "desc"
      }
    ]
  }'
```

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

```json theme={null}
{
  "contacts": [
    { "_id": "66f1a6e4698f1bca60424950", "key": "newest@example.com" },
    { "_id": "66f1a6e4698f1bca60424949", "key": "second@example.com" }
  ],
  "totalRecords": 3847
}
```

Use `totalRecords` to calculate the total number of pages: `Math.ceil(totalRecords / pageSize)`. Then loop through pages 1 to N.

### Request body reference

| Field                  | Required | Description                                                                                                                                                                        |
| ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `contactStructureId`   | Yes      | The contact structure to search within                                                                                                                                             |
| `page`                 | Yes      | Page number (1-indexed)                                                                                                                                                            |
| `pageSize`             | Yes      | Results per page                                                                                                                                                                   |
| `source`               | Yes      | Field projection. Pass an array of one or more case-sensitive values from: `_id`, `key`, `fields`, `tags`, `lists`, `contactStatus`, `contactSubStatus`, `createdAt`, `updatedAt`. |
| `contactSpecification` | Yes      | Object with a `filters` array of filter groups (see operators below)                                                                                                               |
| `sortField`            | No       | Array of sort criteria                                                                                                                                                             |
| `showFieldIds`         | No       | Limit which field IDs appear in `fields` array                                                                                                                                     |

### Available filter operators

| Operator      | Description            | Example use                     |
| ------------- | ---------------------- | ------------------------------- |
| `EQ`          | Equals                 | Exact email match               |
| `NEQ`         | Not equals             | Exclude a specific value        |
| `SW`          | Starts with            | Email prefix search             |
| `CONTAINS`    | Contains substring     | Partial name match              |
| `GT`          | Greater than           | Created after a date            |
| `LT`          | Less than              | Created before a date           |
| `BETWEEN`     | Between two values     | Date range                      |
| `NOT_BETWEEN` | Not between two values | Exclude a date range            |
| `IS_EMPTY`    | Field is empty         | Find contacts with missing data |
| `NOT_EMPTY`   | Field is not empty     | Find contacts with data present |
| `IN`          | In a set of values     | Match any of several list IDs   |
| `NIN`         | Not in a set           | Exclude specific values         |

### Available filter columns

| Column               | Description                                                         |
| -------------------- | ------------------------------------------------------------------- |
| `KEY`                | Email address                                                       |
| `EMAIL_DOMAIN`       | Email domain (e.g., `example.com`)                                  |
| `FIELD_ID`           | Custom field value (requires `id` parameter with the field's `_id`) |
| `TAG_ID`             | Tag assignment                                                      |
| `LIST_ID`            | List membership                                                     |
| `CONTACT_STATUS`     | Primary status (`active`, `inactive`, etc.)                         |
| `CONTACT_SUB_STATUS` | Secondary status                                                    |
| `CREATED_AT`         | Creation timestamp                                                  |
| `UPDATED_AT`         | Last update timestamp                                               |

## Common errors

| Status | Error                  | Cause                                                | Fix                                                                                                                   |
| ------ | ---------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `401`  | `UnauthorizedError`    | Invalid or inactive API key                          | Verify your key in Settings > API Keys                                                                                |
| `403`  | `ForbiddenError`       | Key lacks `contacts:read` scope                      | Search requires `contacts:read` scope                                                                                 |
| `400`  | `RequiredFieldError`   | A required body field is missing                     | Ensure `contactStructureId`, `page`, `pageSize`, `source`, and `contactSpecification` are all in the request body     |
| `400`  | `ModelValidationError` | A field's value fails schema validation              | Check the error's `field` and `message`. For `source`, confirm it's a non-empty array of valid, case-sensitive values |
| `400`  | `ValidationError`      | Other validation failure (e.g., bad filter operator) | Read the `message` for specifics                                                                                      |
| `429`  | `TooManyRequestsError` | Rate limit exceeded                                  | Wait for the `Retry-After` period. See [Rate Limits](/rate-limits)                                                    |

## Next steps

* [Manage Contacts](/scenarios/manage-contacts) — create, update, or delete the contacts you find
* [Manage Lists](/scenarios/manage-lists) — organize contacts into lists
* [Manage Custom Fields](/scenarios/manage-custom-fields) — understand your contact structure's field IDs
* [Migration from Legacy](/scenarios/migration-from-legacy) — use search for dedup during bulk import
