curl --request POST \
--url https://brew.new/api/v1/contacts/search \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"filters": [
{
"field": "plan",
"operator": "equals",
"value": "enterprise"
}
],
"sort": "createdAt",
"order": "desc",
"limit": 50
}
'{
"data": [
{
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe",
"subscribed": true,
"validationStatus": "valid",
"lastValidatedAt": "2026-04-08T12:01:00.000Z",
"suppressed": false,
"suppressedReason": null,
"consent": {
"source": "form",
"capturedAt": "2026-04-08T12:00:00.000Z",
"policyVersion": "2026-03"
},
"createdAt": "2026-04-08T12:00:00.000Z",
"updatedAt": "2026-04-08T12:05:00.000Z",
"importId": null,
"customFields": {
"plan": "enterprise",
"revenue": 4200
}
},
{
"email": "john@example.com",
"firstName": "John",
"lastName": "Smith",
"subscribed": true,
"validationStatus": "valid",
"lastValidatedAt": "2026-04-08T12:01:00.000Z",
"suppressed": false,
"suppressedReason": null,
"consent": {
"source": "form",
"capturedAt": "2026-04-08T12:00:00.000Z",
"policyVersion": "2026-03"
},
"createdAt": "2026-04-08T12:00:00.000Z",
"updatedAt": "2026-04-08T12:05:00.000Z",
"importId": null,
"customFields": {
"plan": "starter"
}
}
],
"pagination": {
"limit": 50,
"cursor": "eyJsYXN0SWQiOiIxMjMiLCJsYXN0U29ydCI6MTcxMjU5MjAwMDAwMH0=",
"hasMore": true
}
}Get contacts
The single “Get Contacts” read. Structured search over the brand’s contacts: free-text search, filters ({ field, operator, value } combined with logic: "and" | "or" | "none"; the allowed operators depend on the field’s type — see the operator schema — and an unsupported pairing is a 400, never a dropped clause), sort + order, and cursor pagination. Returns { data, pagination }.
search with a whole email address matches only that contact, like filters: [{ field: "email", operator: "equals", value: "…" }] or GET /v1/contacts/{email}. Any other search text matches the contacts whose email, first or last name contains any of its words, which can include other contacts, in sort order, so act on a contact found by its address. Omit every filter to list everything.
Pass an optional audienceId to scope the search to a saved audience’s members — its stored filter set is evaluated as one unit with its own logicalOperator (an OR audience stays an OR) and then ANDed with filters, so the page is exactly who a send to that audience reaches (an unknown / cross-brand id, or an audience a send would refuse → 400).
Set count: true to get { count } instead of a page.
include: ["openProfile"] attaches each returned contact’s smart-send open-time profile (openProfile, as on getContact; null when they have no opens yet). Each row costs one profile read, so a page then holds at most 10 contacts (pagination.limit reports the size read); it needs the emails scope as well (403 INSUFFICIENT_PERMISSIONS without it) and is refused with count: true.
curl --request POST \
--url https://brew.new/api/v1/contacts/search \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"filters": [
{
"field": "plan",
"operator": "equals",
"value": "enterprise"
}
],
"sort": "createdAt",
"order": "desc",
"limit": 50
}
'{
"data": [
{
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe",
"subscribed": true,
"validationStatus": "valid",
"lastValidatedAt": "2026-04-08T12:01:00.000Z",
"suppressed": false,
"suppressedReason": null,
"consent": {
"source": "form",
"capturedAt": "2026-04-08T12:00:00.000Z",
"policyVersion": "2026-03"
},
"createdAt": "2026-04-08T12:00:00.000Z",
"updatedAt": "2026-04-08T12:05:00.000Z",
"importId": null,
"customFields": {
"plan": "enterprise",
"revenue": 4200
}
},
{
"email": "john@example.com",
"firstName": "John",
"lastName": "Smith",
"subscribed": true,
"validationStatus": "valid",
"lastValidatedAt": "2026-04-08T12:01:00.000Z",
"suppressed": false,
"suppressedReason": null,
"consent": {
"source": "form",
"capturedAt": "2026-04-08T12:00:00.000Z",
"policyVersion": "2026-03"
},
"createdAt": "2026-04-08T12:00:00.000Z",
"updatedAt": "2026-04-08T12:05:00.000Z",
"importId": null,
"customFields": {
"plan": "starter"
}
}
],
"pagination": {
"limit": 50,
"cursor": "eyJsYXN0SWQiOiIxMjMiLCJsYXN0U29ydCI6MTcxMjU5MjAwMDAwMH0=",
"hasMore": true
}
}Authorizations
Send your Brew API key as Authorization: Bearer brew_xxx.
Headers
The brand this request acts on. REQUIRED for organization-scoped credentials (otherwise 400 BRAND_ID_REQUIRED — there is no default brand); list ids with GET /v1/brands. Brand-scoped credentials may omit it, and sending a different brand returns 403 BRAND_SCOPE_MISMATCH. A brand outside your organization returns 404 BRAND_NOT_FOUND.
1 - 64Body
A whole email address matches only that contact. Other text matches the contacts whose email, first or last name contains any of its words, in sort order.
1Show child attributes
Show child attributes
Scope to a saved audience's members. The audience's stored filter set is evaluated as one unit with its own logicalOperator (an OR audience stays an OR), then ANDed with filters. Unknown / cross-brand id, or an audience a send would refuse (unusable filters, a cohort still building) → 400.
1and, or, none 1asc, desc With count: true: also count per value of up to two fields (a contact field, a custom field, or emailDomain), largest group first.
1 - 2 elements1 - 200With count: true: also count per UTC createdAt day, week (Monday start) or month.
day, week, month 1 <= x <= 1001openProfile attaches each returned contact's smart-send open-time profile (needs the emails permission as well). A page then holds at most 10 contacts; not with count: true.
1 elementopenProfile Was this page helpful?