Filtering contacts
The contacts endpoints accept a structured where object that lets you filter your
network by almost any attribute — tags, groups, company, activity dates, custom
fields, and more. This page documents every key the where DSL accepts, the value
shapes each one takes, and how conditions combine.
Which endpoints use where
| Endpoint | where goes in | Notes |
|---|---|---|
POST /v1/contacts/filter | JSON request body | Recommended for anything non-trivial. No length limit on the filter. |
POST /v2/contacts/filter | JSON request body | Same DSL; the response also returns a filterHash fingerprint. |
GET /v1/contacts | query string | Best for short, simple filters — long/nested filters hit URL-length limits. |
GET /v2/contacts | query string | Same as above. |
GET /v1/contacts/count · GET /v2/contacts/count | query string | Returns a count of matching contacts instead of the rows. |
POST /contacts/filter, put where (and the pagination keys take, skip,
cursor, plus orderBy, include, select) in the JSON request body. The
auto-generated endpoint reference currently lists these under "Query parameters"
because the route shares a schema with the list endpoint — but the filter handler
reads them from the body. When in doubt, prefer POST /contacts/filter with a JSON
body for the full DSL below.How where works
where is a flat object. Every top-level key is combined with logical AND — a
contact must satisfy all of them to be returned. To express OR, use the
_or key.
{
"where": {
"has_company": "acme",
"is_starred": true
}
}
Returns starred contacts whose company contains "acme".
Value-shape conventions
Most keys accept more than one value shape. The shape you pass changes what the key means:
| You pass | Meaning |
|---|---|
A bare string on a text field (has_company, has_name, …) | Case-insensitive contains (fuzzy) match |
{ "contains": "…" } | Explicit fuzzy contains |
{ "eq": "…" } | Exact match (case-sensitive equality) |
A boolean on a has_* key | Presence check — true = the field is set, false = it is empty / NULL |
| An array of ids (tags, groups, sources) | Match any of them (OR) |
{ "not": … } | Exclude — negates the match |
{ "gte": "…", "lte": "…" } (ISO 8601) | Date range (supply at least one bound) |
An array of arrays (has_tags_all, has_groups_all) | AND across rows, OR within a row |
All dates are ISO 8601 datetimes, e.g. "2026-01-31T00:00:00Z". All ids are UUIDs.
Filter keys
Identity & flags
| Key | Value shape(s) | Filters by |
|---|---|---|
in | uuid · uuid[] | Only these contact ids |
not_in | uuid · uuid[] | Everything except these contact ids |
is_starred | boolean | Starred / not starred |
is_archived | boolean | Archived / not archived |
ignore_merge | boolean | Contacts flagged to be ignored by merge |
Names & text profile fields
Text fields accept a bare string (fuzzy contains), an explicit { contains } /
{ eq } matcher, or a boolean presence check. has_name additionally accepts a
string array for an exact multi-value match.
| Key | Value shape(s) | Filters by |
|---|---|---|
has_name | string (contains) · string[] (exact list) · { contains } · { eq } | Full name |
has_company | string · boolean · { contains } · { eq } | Company |
has_job_title | string · boolean · { contains } · { eq } | Job title |
has_education | string · boolean · { contains } · { eq } | Education |
has_description | string · boolean · { contains } · { eq } | Description / bio |
has_linkedin_companies | string · boolean · { contains } · { eq } | LinkedIn-derived companies |
has_linkedin_education | string · boolean · { contains } · { eq } | LinkedIn-derived education |
has_location | string (fuzzy) · boolean | Location text |
has_location_exact | string | Location, exact match |
Each text field has a negated twin that excludes contacts whose value contains the given string(s):
| Key | Value shape(s) |
|---|---|
not_has_name · not_has_company · not_has_job_title · not_has_education · not_has_description · not_has_linkedin_companies · not_has_linkedin_education | string · string[] |
Contact info & birthday presence
| Key | Value shape(s) | Filters by |
|---|---|---|
has_email | boolean | Has at least one email address |
has_phone_number | boolean | Has at least one phone number |
has_birthday | boolean · ISO datetime | true = any birthday set; a date matches that day + month |
Social handles
Bare strings match the social slug exactly; booleans check presence.
| Key | Value shape(s) | Filters by |
|---|---|---|
has_linkedin | string (exact slug) · boolean | |
has_twitter | string · boolean | Twitter / X |
has_instagram | string · boolean |
Tags
| Key | Value shape(s) | Filters by |
|---|---|---|
has_tag | uuid | A single tag id |
has_tags | uuid[] (OR) · { "not": uuid | uuid[] } | Any of the given tag ids, or exclude them |
has_tags_all | uuid[][] | Array of arrays — AND across rows, OR within each row |
has_tags_all is how you express "tagged A and B". Each inner array is an OR
group; the outer array ANDs the groups together:
{ "has_tags_all": [ ["tagA"], ["tagB", "tagC"] ] }
Has tag A and (tag B or tag C).
Groups
| Key | Value shape(s) | Filters by |
|---|---|---|
has_groups | uuid · string[] (OR) · boolean (≥ 1 group) · { "not": string | string[] } | Group membership |
has_groups_all | uuid[][] | Array of arrays — AND across rows, OR within each row (same shape as has_tags_all) |
Source, priority & cadence
| Key | Value shape(s) | Filters by |
|---|---|---|
has_source | string · string[] | Contact source (import origin) |
has_priority_tier | number (≥ 1) · number[] (OR) · boolean | Relationship priority tier; true = any tier set, false = none |
has_frequency | string · boolean | Keep-in-touch cadence |
has_never_keep_in_touch | boolean | Marked "never keep in touch" |
Activity & date ranges
Each of these accepts a { gte, lte } ISO datetime range (supply at least one
bound). The has_last_seen_at, has_interaction, has_next_reminder, and
has_linkedin_last_message_at keys also accept a boolean presence check.
| Key | Value shape(s) | Filters by |
|---|---|---|
has_created_at | { gte, lte } | When the contact was created |
has_updated_at | { gte, lte } | When the contact was last updated |
has_last_seen_at | boolean · { gte, lte } | Last interaction time (alias of has_interaction) |
has_interaction | boolean · { gte, lte } | Interaction recency |
has_next_reminder | boolean · { gte, lte } | Upcoming reminder date |
has_linkedin_last_message_at | boolean · { gte, lte } | Last LinkedIn message |
Geographic distance
| Key | Value shape(s) | Filters by |
|---|---|---|
has_distance | { "lat": number, "lon": number, "meters": number } | Contacts within meters of the point |
Custom fields
custom_fields filtering is rolling out across the API. Use the
latest API host and confirm behavior on a small query before relying on it in
production.custom_fields filters by your user-defined fields without inflating the schema
with one key per field. It takes an array of clauses (at least one); each clause
is { field_id, operator, value? } and all clauses AND together.
operator | value shape | Meaning |
|---|---|---|
contains | string | Fuzzy contains (text fields) |
eq | string | Exact match |
in | string[] | Value is any of these |
not_in | string[] | Value is none of these |
present | (omit) | The field has a value |
absent | (omit) | The field is empty |
gte | ISO datetime string | On or after (date fields) |
lte | ISO datetime string | On or before (date fields) |
{
"custom_fields": [
{ "field_id": "…", "operator": "eq", "value": "VIP" },
{ "field_id": "…", "operator": "present" }
]
}
field_id is the id of the custom field — fetch it from the
custom fields endpoints.
Compound OR
_or compound-OR key is rolling out across the API.By default every top-level key is ANDed. Use _or when you genuinely need
(A AND B) OR (C AND D). It takes an array of filter sets (at least one), and a
contact matches if it satisfies any branch. Each branch is a full filter set,
but _or cannot nest inside a branch — one level deep only. Deeper logic can
always be flattened to distributive form: (A OR B) AND C becomes
_or: [ {A, C}, {B, C} ].
{
"where": {
"_or": [
{ "is_starred": true },
{ "has_tags": ["…tag-id…"] }
]
}
}
Starred or tagged with the given tag.
Examples
All examples authenticate with an API key as a Bearer token. See
Authentication for where to find your key. The base
URL is https://api.prod.getdex.com.
Tag AND-composition
Contacts that have (tag A or tag B) and tag C:
curl https://api.prod.getdex.com/v1/contacts/filter \
--request POST \
--header 'Authorization: Bearer dex_your_api_key' \
--header 'Content-Type: application/json' \
--data '{
"where": {
"has_tags_all": [
["11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222"],
["33333333-3333-3333-3333-333333333333"]
]
},
"take": 100
}'
Company + last-seen date range
Contacts at a company matching "acme" whom you last interacted with in H1 2026:
curl https://api.prod.getdex.com/v1/contacts/filter \
--request POST \
--header 'Authorization: Bearer dex_your_api_key' \
--header 'Content-Type: application/json' \
--data '{
"where": {
"has_company": { "contains": "acme" },
"has_last_seen_at": {
"gte": "2026-01-01T00:00:00Z",
"lte": "2026-06-30T23:59:59Z"
}
}
}'
Custom-field filter
Contacts whose "Tier" custom field equals "VIP" and whose "Renewal" date is on or after Jan 1 2026:
curl https://api.prod.getdex.com/v1/contacts/filter \
--request POST \
--header 'Authorization: Bearer dex_your_api_key' \
--header 'Content-Type: application/json' \
--data '{
"where": {
"custom_fields": [
{ "field_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "operator": "eq", "value": "VIP" },
{ "field_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "operator": "gte", "value": "2026-01-01T00:00:00Z" }
]
}
}'
Compound OR
Contacts that are starred or are in a given group and have an email on file:
curl https://api.prod.getdex.com/v1/contacts/filter \
--request POST \
--header 'Authorization: Bearer dex_your_api_key' \
--header 'Content-Type: application/json' \
--data '{
"where": {
"_or": [
{ "is_starred": true },
{
"has_groups": ["44444444-4444-4444-4444-444444444444"],
"has_email": true
}
]
}
}'
Response
The filter and list endpoints return the standard envelope with a data.items
array, a data.count, and a data.nextCursor for pagination:
{
"error": false,
"data": {
"items": [ { "id": "…", "full_name": "…" } ],
"count": 1,
"nextCursor": null
}
}
For the full response shape and all available fields, see the endpoint reference: Filter contacts (v1) · List contacts (v2).

