# Counting and retrieval

Which endpoint answers which question about the audience: the exact count of a filter, a quick search, a cursor traverse of a large result set, a single contact by id or identifier, and what the subscription plan lets each of them return.

**Language:** en
**Audience:** developer
**TLDR:** Use /audience/count for an exact total with no rows (works under the anonymized data policy, and takes filterCompliance to count who can actually receive; add ladder: true for the audience, matched, reachable and consented rungs, where consented is who receives). Use /audience/search for a first page of up to 50 contacts and /audience/scroll for large traverses with a cursor, up to 100 per page; both need PROJECT_AUDIENCE_LIST for scroll and are redacted by plan. Fetch one contact by audience id, user id, phone or email with the direct lookups.
**Search keywords:** count, count contacts, how many contacts, audience count, total contacts, search contacts, find contacts, list contacts, scroll, cursor, pagination, paginate, iterate contacts, export contacts, bulk read, retrieve contact, get contact, lookup by phone, lookup by email, lookup by user id, audience id, aggregations, anonymized, data policy, redacted, PII, which endpoint should I use, 50 limit, 100 limit, filterCompliance, reachable, ladder, count ladder, consent ladder, matched, consented, who actually receives, why is my count low, unknownSegments
**Related pages:** /platform-api/product-api/audience/query-filter, /platform-api/product-api/audience/segments
**Docs index (every page):** https://docs.instasent.com/llms.txt
**This zone's index:** https://docs.instasent.com/platform-api/product-api/llms-full.txt
**This page:** https://docs.instasent.com/platform-api/product-api/audience/counting-and-retrieval/ (HTML) · https://docs.instasent.com/platform-api/product-api/audience/counting-and-retrieval.md (Markdown)

Once a filter selects the right people, there are four different things you might want back, and they are four different endpoints with four different costs and permissions. Picking the wrong one is the usual cause of a `403` you did not expect or a page-two that never arrives.

## Which endpoint for which question

| The question                  | Endpoint                                                | What comes back                       |
| ----------------------------- | ------------------------------------------------------- | ------------------------------------- |
| How many match this filter?   | `POST /audience/count`                                  | A single exact total, no contact rows |
| Show me a first page          | `POST /audience/search`                                 | Up to 50 contacts, from the beginning |
| Give me all of them           | `POST /audience/scroll`                                 | Up to 100 per page, cursor-paginated  |
| All of them, inside a segment | `POST /audience/segment/{uid}/scroll`                   | Same, restricted to a segment         |
| This one person               | `GET /audience/{audienceId}` and the identifier lookups | One contact                           |
| A breakdown, not rows         | `POST /audience/aggregations`                           | Aggregation results only              |

## Counting

`POST /project/{project}/audience/count` takes the [audience query filter](/platform-api/product-api/audience/query-filter) and returns only the total. It is the endpoint whose number you may act on: exact, uncapped and live, with no sampling anywhere near it.

[`POST /project/{project}/audience/count` - The exact number of contacts matching a filter.](/platform-api/product-api/reference)

```bash
curl -X POST "$BASE/audience/count" \
  -H "Authorization: Bearer $INSTASENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "filterCompliance": { "sms": "no-opt-out" } }'
```

Four things worth knowing:

- **An empty body counts the whole audience.** `limit` and `offset` are ignored.
- **It carries no contact data**, which is why it stays available under the `anonymized` data policy mode, where `/audience/search` is disabled. For restricted credentials it is the supported way to answer "how many contacts match this".
- **To count who can actually *receive*, pass `filterCompliance`.** Without it the total is the raw audience, not a sending figure. The consent and reach rules are the same ones a campaign estimate applies. See [Counting who can receive](/platform-api/product-api/audience/query-filter#counting-who-can-receive-filtercompliance).
- **It is the endpoint that settles any number a probe suggested.** [Probes](/platform-api/product-api/audience/probes) report shape and may be sampled; this is the size.

### Where a count drops: the ladder

A total on its own does not say why it is small. Add `"ladder": true` at the top level of the body and the same call also returns four counts, each one a subset of the one before, so you can see at which step the contacts fall away:

```bash
curl -X POST "$BASE/audience/count" \
  -H "Authorization: Bearer $INSTASENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "ladder": true, "filterCompliance": { "sms": "no-opt-out" } }'
```

```json
{
  "metadata": {
    "totalHits": 452,
    "ladder": {
      "audience": 1200,
      "matched": 1200,
      "reachable": 610,
      "consented": 452
    },
    "unknownSegments": []
  }
}
```

| Rung        | Who it counts                                                                                                                                                 |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `audience`  | Every contact in the project.                                                                                                                                 |
| `matched`   | The contacts your filter selects: the conditions in `root` plus the scoping keys (countries, data sources, required attributes). No reach and no consent yet. |
| `reachable` | Those of `matched` the channel can reach: a valid mobile number and, for `rcs` and `whatsapp`, a device not known to lack support.                            |
| `consented` | Those of `reachable` the consent policy in `filterCompliance` allows. Always equal to `totalHits`.                                                            |

**`consented` is the number of contacts who would actually receive the message.** Quote it as the reach of a send; `matched` alone overstates it. The gaps between rungs tell you what to fix: a big drop from `matched` to `reachable` is missing or unusable phone numbers, a big drop from `reachable` to `consented` is the consent policy at work. Without `filterCompliance` (or the explicit consent and reach keys it expands to) there is no reach or consent to apply, so `matched`, `reachable` and `consented` come back equal.

`unknownSegments` lists any segment uid in `root` that matches no segment in the project. Such a condition selects nobody, so a zero with an entry here is a typo in the filter, not an empty audience.

## Searching and scrolling

Both take the same filter. They differ in how much they will give you and what it costs.

[`POST /project/{project}/audience/search` - A first page of matching contacts, up to 50.](/platform-api/product-api/reference)

`/audience/search` uses offset/limit pagination, but the offset is always forced to `0`: it is built for a quick look from the beginning, not for walking a result set. Its ceiling is 50 contacts per request and it needs `PROJECT_AUDIENCE_READ`.

[`POST /project/{project}/audience/scroll` - A cursor traverse of the whole result set, 100 contacts at a time.](/platform-api/product-api/reference)

`/audience/scroll` is the one for large traverses. It returns a cursor in the response metadata that you pass back to get the next page, up to 100 contacts per request. The cursor is a base64 string holding the internal query state and **expires after one minute of inactivity**, so a traverse has to keep moving: a job that fetches a page, spends five minutes processing it and comes back will find the cursor gone.

It needs `PROJECT_AUDIENCE_LIST`, which is gated by the subscription plan. That is the usual reason a call that works for one organization is refused for another with the same code.

> **Tip**: Scroll rather than paginate. It is the endpoint designed for volume: one hit per page, generous page size and no offset arithmetic. Reserve `/audience/search` for the interactive "show me a few" case.

## Retrieving one contact

When you already know who you are looking for, four direct lookups skip the filter entirely.

[`GET /project/{project}/audience/{audienceId}` - By audience contact id.](/platform-api/product-api/reference)

[`GET /project/{project}/audience/user/{userId}` - By the `_user_id` your systems use.](/platform-api/product-api/reference)

[`GET /project/{project}/audience/search/phone/{userPhone}` - By phone number. URL-encode the leading plus as `%2B`.](/platform-api/product-api/reference)

[`GET /project/{project}/audience/search/email/{userEmail}` - By email address.](/platform-api/product-api/reference)

The id these return is the one every other per-contact call takes, including [the contact's events](/platform-api/product-api/audience/events#reading-a-contacts-events) and direct messaging.

## Aggregations

`POST /audience/aggregations` runs **your** aggregations over the filtered audience and returns results only, with no contact rows and the limit pinned to zero. It needs `PROJECT_AGGREGATIONS`, which is not generally available and is granted manually by Instasent.

If what you want is a distribution over an attribute rather than a bespoke aggregation, [`/audience/coverage`](/platform-api/product-api/audience/probes) gives you one with the ordinary read scope, at the cost of being a probe: shape, possibly sampled, never a size.

## What the plan lets you see

Two gates decide the fields on a returned contact, and both have to be open: the **scopes** on your token and the **subscription plan** of the organization.

| Level   | Scope                         | Fields returned                             |
| ------- | ----------------------------- | ------------------------------------------- |
| Default | `PROJECT_AUDIENCE_READ`       | Full name and user id only                  |
| Basic   | `PROJECT_AUDIENCE_DATA_BASIC` | Phone, email, country, name, boolean fields |
| Full    | `PROJECT_AUDIENCE_DATA_FULL`  | Full contact data including PII             |

> **Warning**: A field missing from a response is far more often a plan or scope limit than an empty value. Before concluding the data is not there, check the level you are actually getting: contact data is **redacted down**, silently and by design, not refused. `/audience/count` is unaffected, because it returns no contact data at all.

## What's next

- **[Query filter](/platform-api/product-api/audience/query-filter)**: the filter these endpoints take, including cursor mechanics and aggregation shapes.
- **[Segments](/platform-api/product-api/audience/segments)**: scrolling a saved segment, and reading its cached size.
- **[Probes](/platform-api/product-api/audience/probes)**: check the filter is selecting on something real before you count it.

---

This is one page of the Instasent documentation. For the complete machine-readable index of every guide and API reference, fetch https://docs.instasent.com/llms.txt — start there for full context.
