Appearance
List contacts
Your audience, newest first.
http
GET /v1/contactsRequires a Full access API key.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | No | How many to return. Default 20, min 1, max 100. |
after | string | No | Return contacts after this contact id. Cannot be combined with before. |
before | string | No | Return contacts before this contact id. Cannot be combined with after. |
segment_id | string | No | Only contacts in this segment. |
topic_id | string | No | Only contacts subscribed to this topic. |
subscribed | boolean | No | true for contacts who are globally subscribed, false for those who have unsubscribed. Omit for both. |
Filters combine with AND — segment_id plus subscribed=true gives you the segment's subscribed members, which is the audience a broadcast to that segment would actually reach.
Note that after/before take a contact id, not an email address, even though the path endpoints accept both. A cursor is a position in the list, and that position is keyed on the id.
Example
bash
curl "https://api.sendgrail.com/v1/contacts?limit=50&subscribed=true" \
-H "Authorization: Bearer $SENDGRAIL_API_KEY"js
const url = new URL('https://api.sendgrail.com/v1/contacts');
url.searchParams.set('limit', '50');
url.searchParams.set('subscribed', 'true');
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.SENDGRAIL_API_KEY}` },
});
const { data, has_more } = await res.json();Response
json
{
"object": "list",
"has_more": true,
"data": [
{
"object": "contact",
"id": "3f1c9a0e-6c5e-4a2b-9f2d-2b1a7c8e4d31",
"email": "ada@example.com",
"first_name": "Ada",
"last_name": "Lovelace",
"unsubscribed": false,
"unsubscribed_at": null,
"bounced_at": null,
"source": "api",
"properties": {
"company_name": "Analytical Engines Ltd",
"seats": 12
},
"created_at": "2026-07-14T19:44:32+00:00"
},
{
"object": "contact",
"id": "c07a1d52-9b3f-4e18-8f60-1d2e5a4b7c99",
"email": "grace@example.com",
"first_name": "Grace",
"last_name": "Hopper",
"unsubscribed": true,
"unsubscribed_at": "2026-07-10T08:12:04+00:00",
"bounced_at": null,
"source": "csv",
"properties": {
"company_name": "Acme Corp",
"seats": null
},
"created_at": "2026-07-02T11:03:19+00:00"
}
]
}has_more tells you whether another page exists — there is no total, because a keyset paginator never counts the table. To walk every page, pass the last id you saw as after until has_more is false. See Pagination.
Filtering by topic is not the same as filtering by subscription
subscribed is the global flag: a contact who has unsubscribed from you altogether. topic_id returns only the contacts subscribed to that one topic, which is a separate opt-in. A contact can be globally subscribed and still opted out of a single topic — filter on both if you want the people a topic-scoped broadcast would reach.
See Contact topics for how a topic's default mode decides the state of a contact who has never touched it.