List Contacts
Retrieve a paginated list of contacts with optional filtering.
Request
GET /contacts
Query Parameters
All parameters are optional and combined with AND.
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number (min: 1) |
per_page | integer | 100 | Items per page (min: 1, max: 100) |
search | string | - | If valid email: exact match on email. Otherwise: partial match on first_name, last_name, phone, or full name (max 255 characters) |
email | string | - | Exact email match (case-insensitive). Unique within the account, so this returns at most one contact. |
external_id | string | - | Exact external_id match. Unique within the account, so this returns at most one contact. |
country_code | string | - | Exact ISO 3166-1 alpha-2 country code. |
language | string | - | Exact ISO 639-1 alpha-2 language code. |
timezone_uuid | string | - | Contacts assigned to the timezone with this UUID. Get the UUID from List Timezones. |
list_uuid | string | - | Members of the list with this UUID. The list must belong to your account, otherwise 422 VALIDATION_ERROR. |
not_in_list_uuid | string | - | Non-members of the list with this UUID. The list must belong to your account, otherwise 422 VALIDATION_ERROR. |
status | string | - | Subscription status: subscribed, unsubscribed, or suppressed (see below). |
created_from | string (YYYY-MM-DD) | - | Contacts created on or after this date (inclusive). |
created_to | string (YYYY-MM-DD) | - | Contacts created on or before this date (inclusive). Must be on or after created_from when both are given. |
updated_from | string (YYYY-MM-DD) | - | Contacts last updated on or after this date (inclusive). |
updated_to | string (YYYY-MM-DD) | - | Contacts last updated on or before this date (inclusive). Must be on or after updated_from when both are given. |
engaged_since | string (YYYY-MM-DD) | - | Contacts who opened or clicked at least one email on or after this date (inclusive). See below. |
not_engaged_since | string (YYYY-MM-DD) | - | Contacts with no open or click since this date, including those who have never engaged at all. Cannot be combined with engaged_since. See below. |
engagement_type | string | - | Restrict engaged_since / not_engaged_since to opened or clicked only. Omit to count either. |
sort | string | -created_at | Sort order: created_at, updated_at, email, or name. Prefix with - for descending. |
custom_field[<key>][<op>] | string | - | Filter by custom-field value. Up to 3 (see below). |
Malformed dates, an out-of-order date range, an unknown/foreign list_uuid or
not_in_list_uuid, or any invalid custom-field filter return
422 VALIDATION_ERROR.
Ordering: Results default to created_at descending (newest first) unless
sort is supplied.
Counting matches: every response includes the filtered total in
pagination.total. To get only a count, request per_page=1 with your filters
and read pagination.total.
Custom-field filters
Filter by custom-field values with the bracket syntax
custom_field[<field_key>][<operator>]=<value>. Up to 3 custom-field
filters may be combined. The allowed operators depend on the field's type:
| Field type | Operators |
|---|---|
text, select | equals, not_equals, contains, not_contains, is_not_empty, is_empty |
number | equals, not_equals, greater_than, greater_than_or_equal, less_than, less_than_or_equal, between |
date, datetime | equals, not_equals, greater_than, less_than, between |
boolean | equals, not_equals (true / false) |
list | contains, not_contains, is_empty |
Reference each field by its field_key, as returned by
List Custom Fields.
betweentakes two comma-separated values, e.g.custom_field[age][between]=18,65.is_not_empty/is_emptyignore the value;is_emptymatches contacts with no value or an empty value for the field.not_equals/not_containsare the exact complement ofequals/contains: they match every contact the positive operator would not, including contacts that have no value for the field. For example,custom_field[plan][not_equals]=proreturns every contact whoseplanis not exactlypro, contacts with noplanvalue included.dateanddatetimevalues useYYYY-MM-DD;datetimefields are matched at day granularity (the time component is ignored).
An unrecognised field_key, an operator not allowed for the field's type, a
malformed value, or more than 3 custom-field filters return
422 VALIDATION_ERROR.
Subscription status
status filters contacts by whether they can be mailed:
| Value | Matches |
|---|---|
subscribed | Contacts that can receive mail — neither unsubscribed nor undeliverable. |
unsubscribed | Contacts who have opted out of all mail. |
suppressed | Undeliverable contacts (for example, after a permanent bounce), held back from sending regardless of consent. |
subscribed is the audience that would actually receive a send. It does not
take list membership into account — combine it with list_uuid to get the
mailable members of a specific list:
status=subscribed&list_uuid=70dbd819-fac2-4dcf-b655-fb925e24410b.
These values are not mutually exclusive — a contact can be both unsubscribed
and suppressed — so don't sum the pagination.total of separate status queries
to get a grand total.
An unknown status value returns 422 VALIDATION_ERROR.
Engagement
engaged_since and not_engaged_since filter contacts by when they last opened
or clicked an email.
| Parameter | Matches |
|---|---|
engaged_since | Contacts who opened or clicked at least one email on or after the given date (inclusive). |
not_engaged_since | Contacts who have not opened or clicked any email since the given date — including contacts who have never opened or clicked at all. |
Both take a YYYY-MM-DD date and cannot be combined; supplying both returns
422 VALIDATION_ERROR.
By default both opens and clicks count toward engagement. Add
engagement_type=opened or engagement_type=clicked to count only one signal —
for example, engaged_since=2026-06-01&engagement_type=clicked returns contacts
who clicked on or after June 1. An unknown engagement_type returns
422 VALIDATION_ERROR.
To retrieve a contact's list memberships and custom-field values, use Get Contact, which returns both — along with the contact's subscription, deliverability, and engagement details.
Example Requests
Basic search:
curl -X GET "https://email.easy.tools/api/v1/contacts?page=1&per_page=50&search=john" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"
Exact lookup by your own identifier:
curl -X GET "https://email.easy.tools/api/v1/contacts?external_id=crm-4821" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"
Contacts updated in a date range, newest-updated first:
curl -X GET "https://email.easy.tools/api/v1/contacts?updated_from=2026-01-01&updated_to=2026-03-31&sort=-updated_at" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"
Filter by custom fields (VIP customers aged 18+):
curl -X GET "https://email.easy.tools/api/v1/contacts?custom_field[customer-type][equals]=vip&custom_field[age][greater_than_or_equal]=18" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"
Count contacts not in a list:
curl -X GET "https://email.easy.tools/api/v1/contacts?not_in_list_uuid=a3d8f1c6-2b9e-4d7a-8c5f-1e6b4a9d2c30&per_page=1" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"
Mailable members of a specific list:
curl -X GET "https://email.easy.tools/api/v1/contacts?status=subscribed&list_uuid=70dbd819-fac2-4dcf-b655-fb925e24410b" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"
Contacts who clicked since a given date:
curl -X GET "https://email.easy.tools/api/v1/contacts?engaged_since=2026-06-01&engagement_type=clicked" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"
Count contacts with no opens or clicks since a date, still subscribed:
curl -X GET "https://email.easy.tools/api/v1/contacts?not_engaged_since=2026-03-01&status=subscribed&per_page=1" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"
Response
Success Response (200 OK)
{
"data": [
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"phone": "+48123456789",
"external_id": "customer_123",
"language": "en",
"country_code": "US",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z"
},
{
"uuid": "660e8400-e29b-41d4-a716-446655440001",
"email": "john.smith@example.com",
"first_name": "John",
"last_name": "Smith",
"phone": null,
"external_id": null,
"language": "pl",
"country_code": null,
"created_at": "2025-01-14T09:00:00Z",
"updated_at": "2025-01-14T09:00:00Z"
}
],
"pagination": {
"current_page": 1,
"per_page": 50,
"total": 2
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
uuid | string | Contact's unique identifier |
email | string | Contact's email address |
first_name | string|null | Contact's first name |
last_name | string|null | Contact's last name |
phone | string|null | Contact's phone number |
external_id | string|null | External identifier for integration purposes |
language | string|null | ISO 639-1 language code (2 characters) |
country_code | string|null | ISO 3166-1 alpha-2 country code (2 characters) |
created_at | string | ISO 8601 timestamp (UTC) |
updated_at | string | ISO 8601 timestamp (UTC) |
Pagination Object
| Field | Type | Description |
|---|---|---|
current_page | integer | Current page number |
per_page | integer | Number of items per page |
total | integer | Total number of contacts |
Error Responses
Validation Error (422 Unprocessable Entity)
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The given data was invalid",
"details": {
"per_page": ["The per page field must not be greater than 100."],
"list_uuid": ["The list uuid field must be a valid UUID."]
}
}
}