# Contact API - Read

Use the read endpoints to retrieve contacts from your account. You can list contacts with filtering, sorting, and pagination, or retrieve a single contact by ID.

---

## Endpoints

| Method | URL | Description |
|---|---|---|
| GET | `https://e1.envoke.com/v1/contacts` | Return a list of contacts. |
| GET | `https://e1.envoke.com/v1/contacts/{ID}` | Return a single contact. |

Authentication is via HTTP Basic Auth using your API ID and API Key.

---

## List Contacts

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

Returns a JSON array of contacts. Supports filtering, sorting, pagination, and field selection. With no `sort`, the order is not defined; use `sort` when you page through results.

### Query Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `limit` | integer | `10` | Maximum number of results to return. Hard cap is `100` unless `?fields=` or `?result_type=kvp` is also set (see note below). `limit=1` returns a single object, not an array (see below). |
| `skip` | integer | `0` | Number of records to skip. Use with `limit` and `sort` to paginate. |
| `fields` | string | *(all)* | Comma-separated list of fields to include in each result, e.g. `id,email`. Use names from the [Fields reference](https://envoke-developers.pages.dev/v1/contacts/fields/). |
| `result_type` | string | *(array)* | Set to `kvp` to receive an object of contact ID to email address (`{ "1001": "alto.lead@example.com" }`). |
| `filter[{field}]` | string | — | Filter on a field. See [Filterable and sortable fields](#filterable-and-sortable-fields) and [Filter Operators](#filter-operators). |
| `sort[{field}]` | string | — | Sort on a field. Value: `asc` or `desc` (case-insensitive), or `1` or `-1`. Repeat for a secondary sort. |

**Note — limit behaviour with `?fields=` and `?result_type=kvp`:** When either of these parameters is present, the default limit increases to 10,000 (returning every record up to that number if no explicit limit is specified) and a `?limit=` value above 100 is honoured as-is.

**Note — `limit=1` returns an object.** With `limit=1` the response is the matching contact as a bare JSON object, not an array of one. `result_type=kvp` has no effect when `limit=1`; `fields` still applies. When nothing matches, the response is `[]`.

**Note — filters without `filter[...]`.** When the request has no `filter` parameter, every query parameter other than `limit`, `skip`, `sort`, `fields`, and `result_type` is treated as a filter, so `?email=alto.lead%40example.com` is the same as `?filter[email]=alto.lead%40example.com`. Any other parameter is rejected as an unknown filter field (for example `Field 'key_field' is not valid for filtering`). When `filter` is present, the other parameters are ignored as filters. Use `filter[...]` for custom fields: dots and spaces in a bare parameter name are not preserved.

### Filterable and sortable fields

| Field | Filter operators | Sortable |
|---|---|---|
| `id`, `external_id`, `remote_id`, `first_name`, `last_name`, `title`, `email`, `company`, `phone`, `address_1`, `address_2`, `city`, `country`, `province`, `postal_code`, `website`, `language`, `feedback` | equals, `(ne)` | Yes |
| `date_created` | equals, `(ne)`, `(gt)`, `(lt)`, `(gte)`, `(lte)` | Yes |
| `consent_status` | equals, `(ne)` | Yes |
| `consent_description` | equals, `(ne)` | No |
| `consent_effective_date`, `consent_expiry` | equals, `(ne)`, `(gt)`, `(lt)`, `(gte)`, `(lte)` | No |
| `custom_fields.{name}` | equals, `(ne)`, `(regex)` | Yes |
| `interests` | equals, `(ne)` | No |
| `autoresponders` | equals, `(ne)` | No |

- **`country`** is matched against the stored 2-letter code, so filter with `CA`, not `Canada`.
- **`custom_fields.{name}`** uses the custom field's name exactly as defined in your account, including case (names contain only letters, digits, and underscores): `filter[custom_fields.Voice_Part]=Alto`.
- **`interests`** takes the exact name (case-sensitive) of a tag, subscription, or mandatory consent item, and matches contacts that have it; `(ne)` matches contacts that don't. IDs are not accepted. An unknown name gives `Invalid 'interest' provided: 'Volunteers'`.
- **`autoresponders`** works the same way with a nurture campaign name. An unknown name gives `Invalid 'autoresponder' provided: 'Welcome'`.
- **Date fields:** `date_created` and `consent_effective_date` values are read as UTC unless they include a time zone. `consent_expiry` is compared as a plain `YYYY-MM-DD` date. See [Dates](https://envoke-developers.pages.dev/v1/contacts/fields/#dates).

### Filter Operators

| Operator | Example | Description |
|---|---|---|
| *(none)* | `?filter[city]=Toronto` | Equals. |
| `(ne)` | `?filter[city][(ne)]=Toronto` | Not equal. |
| `(regex)` | `?filter[custom_fields.Season_Ticket_Number][(regex)]=^HCS-ST-` | Matches a MySQL regular expression. Custom fields only. |
| `(gt)` | `?filter[date_created][(gt)]=2026-01-01T00:00:00Z` | Greater than. Date fields only. |
| `(lt)` | `?filter[date_created][(lt)]=2026-07-01T00:00:00Z` | Less than. Date fields only. |
| `(gte)` | `?filter[consent_effective_date][(gte)]=2026-09-01` | Greater than or equal. Date fields only. |
| `(lte)` | `?filter[consent_expiry][(lte)]=2027-03-31` | Less than or equal. Date fields only. |
| `(or)` | `?filter[(or)][0][email]=a%40example.com&filter[(or)][1][email]=b%40example.com` | Matches if any of the grouped conditions match. |
| `(not)` | `?filter[(not)][consent_status]=Express` | Matches if the grouped conditions do not all match. |

*Note: URL-encode brackets in production requests (e.g. `%5B` for `[`).*

Each operator also has a `$` spelling (`$ne`, `$regex`, `$gt`, `$or`, and so on) that behaves the same way. These docs use the `(op)` spelling.

The `(in)` operator is not supported on any contacts field. To match one of several values, use `(or)`.

**Combining conditions.**
- Separate filters are ANDed: `?filter[city]=Toronto&filter[consent_status]=Express`.
- Two operators on one field are ANDed, which gives a range: `?filter[date_created][(gte)]=2026-01-01&filter[date_created][(lt)]=2026-07-01`.
- A query string cannot repeat a key, so to use the same field twice, put each condition in its own numbered group: `?filter[0][interests]=Volunteer&filter[1][interests]=Alumni` matches contacts with both tags. Groups inside `(or)` are numbered the same way.
- `(not)` wraps everything inside it: `?filter[(not)][city]=Toronto&filter[(not)][language]=fr` excludes contacts who are in Toronto *and* French-speaking.

Note: `(ne)` on a custom field matches only contacts that have a value for that field; contacts with no value are not returned.

### Errors

Invalid list parameters return HTTP `400` with every problem listed at once:

```json
{
  "success": false,
  "result_text": "Validation failed, see 'result_data' -> 'errors'",
  "result_data": {
    "errors": [
      "Field 'nickname' is not valid for filtering",
      "'REGEXP' is not a valid operator for the field 'email'"
    ]
  }
}
```

| Error | Cause |
|---|---|
| `Invalid 'limit': 'ten'` | `limit` is not a whole number. |
| `Invalid 'skip': 'abc'` | `skip` is not a whole number. |
| `Invalid 'sort': 'email'` | `sort` was sent without a field, e.g. `?sort=email`. |
| `'up' is not a valid sort direction` | Sort direction is not `asc`, `desc`, `1`, or `-1`. |
| `Field 'interests' is not valid for sorting` | The field cannot be sorted, or does not exist. |
| `Field 'nickname' is not valid for filtering` | The field does not exist. Also returned for an unrecognised operator: `?filter[email][(foo)]=x` gives `Field '(foo)' is not valid for filtering`. |
| `'REGEXP' is not a valid operator for the field 'email'` | The operator is not supported on that field. The message names the SQL operator: `REGEXP` for `(regex)`, `IN` for `(in)`, and `>`, `<`, `>=`, `<=` for the range operators. |
| `Invalid date format in filter condition for date_created` | A date filter value could not be read as a date. |
| `Invalid 'interest' provided: 'Volunteers'` | No tag, subscription, or mandatory consent item has that exact name. |
| `Invalid 'autoresponder' provided: 'Welcome'` | No nurture campaign has that exact name. |
| `Field 'email' requires a scalar value` | An operator is given a list or group where a single value is expected, e.g. `filter[email][(ne)][]=alto.lead@example.com`. |
| `Field '0' is not valid for filtering` | A field is given a list without an operator, e.g. `filter[email][]=alto.lead@example.com`; the list position is read as the field name. |

### Response

A JSON array of contact objects. Returns an empty array (`[]`) if no contacts match.

```json
[
  {
    "id": "1001",
    "external_id": "9f2c4e7a1b3d5f6a8c0e2b4d6f8a1c3e",
    "date_created": "2026-10-05T14:03:11+00:00",
    "remote_id": "",
    "first_name": "Maya",
    "last_name": "Okafor",
    "title": "Section Lead",
    "email": "alto.lead@example.com",
    "company": "",
    "phone": "416-555-0142",
    "address_1": "48 Harbour Lane",
    "address_2": "Unit 5",
    "city": "Toronto",
    "country": "CA",
    "province": "ON",
    "postal_code": "M5V 0Z9",
    "website": "",
    "language": "en",
    "feedback": "",
    "consent_status": "Express",
    "consent_description": "Signed up at the spring concert",
    "consent_effective_date": "2026-09-12T18:30:00+00:00",
    "consent_expiry": "",
    "custom_fields": { "Voice_Part": "Alto", "Season_Ticket_Number": "HCS-ST-0417" },
    "interests": ["Volunteer", "Alumni", "Concert News"],
    "autoresponders": []
  }
]
```

`consent_expiry` is `""` when the consent does not expire or when expiry does not apply to the contact (see [Consent fields](https://envoke-developers.pages.dev/v1/contacts/fields/#consent-fields)).

---

## Get a Single Contact

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

Returns a single contact object (the same shape as one item in the list response). Replace `{ID}` with the Envoke contact ID. `fields` can be used to select fields; filter, sort, and paging parameters are ignored.

If the ID is not found, the API returns an empty array (`[]`).

---

## Examples

### Return the first page of contacts

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts"
```

Returns up to 10 contacts. Use `limit` (up to 100) and `skip` to page through the rest.

### Paginate through results

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?sort%5Bid%5D=asc&limit=100&skip=100"
```

### Sort by email

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?sort%5Bemail%5D=asc"
```

### Find a contact by email

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?filter%5Bemail%5D=alto.lead%40example.com"
```

### Find a contact by remote ID

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?filter%5Bremote_id%5D=HCS-0002"
```

### Exclude a city

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?filter%5Bcity%5D%5B%28ne%29%5D=Toronto"
```

### Filter by a custom field value

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?filter%5Bcustom_fields.Voice_Part%5D=Alto"
```

With `(regex)`, contacts whose season ticket number starts with `HCS-ST-`:

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?filter%5Bcustom_fields.Season_Ticket_Number%5D%5B%28regex%29%5D=%5EHCS-ST-"
```

### Contacts with a tag, and without it

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?filter%5Binterests%5D=Volunteer"
```

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?filter%5Binterests%5D%5B%28ne%29%5D=Volunteer"
```

### Contacts with two tags

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?filter%5B0%5D%5Binterests%5D=Volunteer&filter%5B1%5D%5Binterests%5D=Alumni"
```

### Either of two email addresses

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?filter%5B%28or%29%5D%5B0%5D%5Bemail%5D=alto.lead%40example.com&filter%5B%28or%29%5D%5B1%5D%5Bemail%5D=tenor.remote%40example.com"
```

### Everyone without express consent

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?filter%5B%28not%29%5D%5Bconsent_status%5D=Express"
```

### Contacts created in the first half of 2026

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?filter%5Bdate_created%5D%5B%28gte%29%5D=2026-01-01T00:00:00Z&filter%5Bdate_created%5D%5B%28lt%29%5D=2026-07-01T00:00:00Z"
```

### Consent given after a date

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?filter%5Bconsent_effective_date%5D%5B%28gt%29%5D=2026-09-01T00:00:00Z"
```

### Return only specific fields

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?fields=id,email,consent_status"
```

Response:

```json
[
  { "id": "1001", "email": "alto.lead@example.com", "consent_status": "Express" },
  { "id": "1002", "email": "tenor.remote@example.com", "consent_status": "Express" }
]
```

With `fields` set and no `limit`, this returns up to 10,000 contacts.

### Return as a key-value map

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?result_type=kvp"
```

Response:

```json
{ "1001": "alto.lead@example.com", "1002": "tenor.remote@example.com" }
```

### Look up one contact's ID by email

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts?email=alto.lead%40example.com&fields=id&limit=1"
```

Response (an object, because `limit=1`):

```json
{ "id": "1001" }
```

### Get a single contact by ID

```bash
curl -X GET -gs -u "$API_ID:$API_KEY" \
  "https://e1.envoke.com/v1/contacts/1001"
```

---

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