Skip to content

List contacts

Your audience, newest first.

http
GET /v1/contacts

Requires a Full access API key.

Query parameters

Cursor-paginated:

ParameterTypeRequiredDescription
limitnumberNoHow many to return. Default 20, min 1, max 100.
afterstringNoReturn contacts after this contact id. Cannot be combined with before.
beforestringNoReturn contacts before this contact id. Cannot be combined with after.
segment_idstringNoOnly contacts in this segment.
topic_idstringNoOnly contacts subscribed to this topic.
subscribedbooleanNotrue 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.

Transactional email on your own domain.