# Audience probes

Two POST endpoints that measure what a project's audience actually contains before you filter it: attribute coverage (fill rate, distinct values, most frequent values, numeric and date ranges) and event volumes (which event types arrive, when the last one arrived, and the real values their parameters carry).

**Language:** en
**Audience:** developer
**TLDR:** Probes measure shape, never size. POST /project/{project}/audience/coverage returns per-attribute fill rate, distinct values and top values; POST /project/{project}/event/volumes returns per-event-type counts, lastSeenAt, and (with `event`) the real parameter values, which ARE sampled above 10,000 contacts and report it in event.sampling, not in the top-level one. Both take PROJECT_AUDIENCE_READ. Probe to pick a value, then use /audience/count for any number you quote.
**Search keywords:** probe, probes, audience probe, attribute coverage, coverage, fill rate, filled, empty attribute, unfilled, distinct values, cardinality, top values, most frequent values, vocabulary, event volumes, volumes, event types received, lastSeenAt, last seen, never received, audience analysis, analyse an audience, analyze audience, profile the audience, data quality, what data do I have, what is in my audience, which attributes are usable, discovery, exploration, sampling, sampled, estimate, estimated, approximate, refresh, throttled, empty segment, segment matches nobody, filter returns nothing, zero contacts, no matches, silent empty filter, event parameters, parameter values, product category values, topValuesOmitted, denominator, event.sampling, two sampling blocks, observedCount, reliable, detection floor, scaled count, is the data exact, are the numbers exact, id vs name, product-id, product-name, utm-id, utm-campaign, filter by id, campaign id, product id, value missing from the list, absence
**Related pages:** /developers/product-api/audience/query-filter, /developers/product-api/audience/event-query-filter
**Docs index (every page):** https://docs.instasent.com/llms.txt
**This zone's index:** https://docs.instasent.com/developers/product-api/llms-full.txt
**This page:** https://docs.instasent.com/developers/product-api/audience/probes/ (HTML) · https://docs.instasent.com/developers/product-api/audience/probes.md (Markdown)

A filter that selects nobody looks exactly like a filter that works. The attribute uid is real, the operator is real, the event type is real, the request returns `200`, and the segment is empty. Nothing errors, because nothing is wrong: the field is simply filled on 4% of the audience, or the value you matched on is spelled `churned` in the data and `inactive` in your head.

**Probes** are the two endpoints that close that gap. They measure what the audience actually contains, attribute by attribute and event type by event type, so you compose a filter against the data that is there rather than against the schema that says it could be.

> **Warning**: A probe tells you a value **exists** and is worth filtering on. It never tells you **how many people it reaches**. Its figures describe shape, they may be sampled estimates, and they must not be quoted as an audience size. For a size, post the filter to [`/audience/count`](/developers/product-api/audience/counting-and-retrieval#counting), which is exact and live.

## The two probes

[`POST /project/{project}/audience/coverage` - Attribute coverage: which of the attributes this project declares are actually usable.](/developers/product-api/reference)

[`POST /project/{project}/event/volumes` - Event volumes: which event types this project actually receives, and what values their parameters carry.](/developers/product-api/reference)

Both take the `PROJECT_AUDIENCE_READ` scope, both accept an optional filter `root` to narrow what is being measured, and both sit in a restrictive [rate-limit](/developers/product-api/rate-limits) class whose ceiling follows your plan.

They are the companion of the specs endpoints: the [attribute catalogue](/developers/product-api/audience/contacts-and-attributes#the-attribute-catalogue) and the [event catalogue](/developers/product-api/audience/events#the-event-catalogue) answer *what shape can this project hold*. Probes answer *what did it actually receive*. An attribute that exists in the specs and is filled on 2% of contacts is present in the catalogue and absent in practice.

## Probe before you filter

The working loop is the same on both sides, and it always ends on a count:

#### 1. Probe

Ask coverage which attributes are filled and what values they hold, or ask volumes which event types arrive and what their parameters carry.

#### 2. Read the real value

Take the literal string the data uses, not the one your product vocabulary uses.

#### 3. Compose the filter

Write the [audience query filter](/developers/product-api/audience/query-filter) against that attribute and that value.

#### 4. Count

Post it to [`/audience/count`](/developers/product-api/audience/counting-and-retrieval#counting). That number, and only that number, is the size you may act on or quote.

## Attribute coverage

`POST /project/{project}/audience/coverage` returns one entry per attribute: how many contacts have it filled in, how many distinct values it holds, its most frequent values when it holds few enough of them to be a vocabulary rather than an identifier, and the range of a numeric or date field.

### The request

Every key is optional. An empty body profiles the attributes worth profiling by default: enabled, visible, non-internal, and of a data type a measurement means something on. Identity attributes such as `_email` or `_user_id` are left out of that default, because a list of everyone's email addresses is not a vocabulary.

- `root` — `object`
  An [audience query filter](/developers/product-api/audience/query-filter) root, same grammar as `/audience/search`. Narrows the population being probed, so you can ask "what does this *part* of the audience contain". A call carrying a root is computed live and never served from cache.
- `datasource` — `string`
  A connector name, uid or id. Narrows the population to the contacts this data source **contributed to**. An unknown value is refused with the list of what exists.
- `attributes` — `string[]`
  Attribute uids to probe. Naming one explicitly also measures attributes the default selection would skip, and overrides the name-based rule that withholds top values.
- `refresh` — `boolean`, default: `false`
  Force a recomputation. Throttled to one every five minutes; a denial is not an error.

```bash
curl -X POST "$BASE/audience/coverage" \
  -H "Authorization: Bearer $INSTASENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "attributes": ["lifecycle_stage"] }'
```

### The response

Everything lives under `metadata`. The two blocks you read before any figure are `denominator` (the population the fill rates are over, and its definition) and `sampling` (how the figures were produced).

```json
{
  "metadata": {
    "computedAt": "2026-08-28T09:12:04+00:00",
    "denominator": {
      "count": 48213,
      "definition": "non-deleted contacts, no channel restriction"
    },
    "sampling": {
      "applied": true,
      "fraction": 0.5,
      "buckets": "0-99",
      "band": "20k-50k",
      "scanned": 24106,
      "scaled": true,
      "minReliableCount": 6
    },
    "attributes": [
      {
        "uid": "lifecycle_stage",
        "label": "Lifecycle stage",
        "type": "keyword",
        "multivalue": false,
        "fillRate": 0.1204,
        "filled": 5805,
        "observedFilled": 2903,
        "approximate": true,
        "reliable": true,
        "distinctValues": 3,
        "topValues": [
          { "value": "churned",    "count": 3120, "observedCount": 1560, "approximate": true, "reliable": true, "countError": 0 },
          { "value": "paused",     "count": 1660, "observedCount": 830,  "approximate": true, "reliable": true, "countError": 0 },
          { "value": "onboarding", "count": 1026, "observedCount": 513,  "approximate": true, "reliable": true, "countError": 0 }
        ],
        "topValuesOther": 0,
        "summary": "12% of contacts have lifecycle_stage, estimated from a 50% sample"
      }
    ],
    "cached": true,
    "computedAgo": "8 minutes",
    "summary": "1 attributes profiled over 48213 contacts, 0 of them filled on at least half. Every figure here is estimated from a 50% sample and describes shape, never audience size."
  }
}
```

### Reading an attribute entry

- `fillRate` — `number`
  The fraction of the denominator that has this attribute filled in, rounded to four decimals. This is the number that decides whether the attribute is worth filtering on at all.
- `filled` — `integer`
  `fillRate` projected onto the whole denominator. Under sampling this is an estimate, and it is still not an audience size: it is what the shape implies, not what a count returns.
- `observedFilled` — `integer`
  The raw number of contacts actually scanned that had it filled. Under sampling this is smaller than `filled` by the sampling fraction; without sampling the two agree.
- `approximate` — `boolean`
  Whether the figures for this attribute came from a sample.
- `reliable` — `boolean`
  Whether `observedFilled` cleared the detection floor (`sampling.minReliableCount`). `false` means the attribute is backed by too few observed contacts for the estimate to be trusted, not that it is empty. Always `true` when nothing was sampled, where a value is simply either there or not.
- `distinctValues` — `integer`
  Estimated number of distinct values. Published for every attribute measured, including the ones that get no top values, because "about forty thousand distinct values" is itself the answer to whether a field is a vocabulary or an identifier.
- `topValues` — `object[]`
  The ten most frequent values, each with a `count` (scaled to the denominator when sampling applies), the raw `observedCount`, its own `approximate` and `reliable` flags, and `countError`, the upper bound of how far the count could be off because of what other shards did not report.
- `topValuesOther` — `integer`
  Contacts holding a value outside the ten listed. Above zero, the list is the most frequent values and not all of them.
- `topValuesOmitted` — `string`
  Present **instead of** `topValues`, saying why they were withheld. See [The absence of top values is not an absence of values](#the-absence-of-top-values-is-not-an-absence-of-values).
- `topValuesSemantics` — `string`
  Present on multivalue attributes. The counts are contacts **holding** the value, and one contact can hold several, so they do not sum to the audience.
- `stats` — `object`
  Numeric and date ranges: `avg`, plus `min` and `max` when nothing was sampled. Under sampling the extremes are replaced by `extremesOmitted`.
- `summary` — `string`
  The whole entry as one sentence, with the sampling qualifier inside it rather than beside it. Quote this rather than reassembling your own sentence from the fields.

The envelope also carries `cached` and `computedAgo` (unfiltered profiles are cached, for longer on larger audiences), `datasource` when you narrowed by one, and `truncated` when the attribute cap was reached, naming the attributes left out.

### Worked example: the filter that matched nobody

You want to reach contacts who went quiet. The project has a custom `lifecycle_stage` attribute, so the obvious filter is `lifecycle_stage = inactive`. It returns `200` and selects nobody, and nothing in the response explains why.

Probe first:

```bash
curl -X POST "$BASE/audience/coverage" \
  -H "Authorization: Bearer $INSTASENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "attributes": ["lifecycle_stage"] }'
```

The answer above says two things at once. The attribute is filled on **12%** of contacts, so whatever you build on it addresses roughly an eighth of the audience at best. And its vocabulary is `churned`, `paused`, `onboarding`. There is no `inactive`, and there never was.

So the filter that matches is:

```json
{
  "version": "0.0.1",
  "root": {
    "type": "group",
    "children": [
      {
        "type": "attribute_condition",
        "key": "lifecycle_stage",
        "operator": "matches-string",
        "values": ["churned", "paused"]
      }
    ]
  }
}
```

Post that to `/audience/count` for the number. The probe said `churned` is worth about 3,120 contacts and `paused` about 1,660; those are the shape talking. The count is the answer.

## Event volumes

`POST /project/{project}/event/volumes` returns one entry per **declared** event type: how many arrived inside the window, whether anything arrived at all, and when the last one did. A type that is declared and never received comes back as an explicit zero, which the event catalogue cannot tell you, and that difference is what decides whether an event is worth building an automation or a filter on.

### The request

- `root` — `object`
  An [audience event query filter](/developers/product-api/audience/event-query-filter) root. Narrows the events being probed. A call carrying a root is computed live and never cached.
- `datasource` — `string`
  A connector name, uid or id. Keeps events this data source **produced**.
- `window` — `integer`
  Days to look back. Omit for the default, which is wider on small audiences. Whatever results is clamped by the subscription.
- `event` — `string`
  One event type uid. Adds a second block carrying the values that type's parameters actually hold.
- `refresh` — `boolean`, default: `false`
  Force a recomputation, throttled to one every five minutes.

```bash
curl -X POST "$BASE/event/volumes" \
  -H "Authorization: Bearer $INSTASENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

### The response

```json
{
  "metadata": {
    "computedAt": "2026-08-28T09:20:11+00:00",
    "window": {
      "days": 15,
      "requestedDays": 15,
      "band": "20k-50k",
      "clamped": false,
      "clampedBy": null,
      "from": "2026-08-13T09:20:11+00:00",
      "to": "2026-08-28T09:20:11+00:00",
      "anchor": "now"
    },
    "sampling": {
      "applied": false,
      "covers": "the per-type counts in this payload, nothing else",
      "reason": "the type counts are never sampled: the aggregation is over about thirty distinct values and is cheap at any size",
      "parameterValues": "sampled above 10,000 contacts; read event.sampling when you pass `event`"
    },
    "eventTypes": [
      {
        "uid": "ecommerce_order_create",
        "label": "Order created",
        "count": 12894,
        "received": true,
        "lastSeenAt": "2026-08-28T08:57:02+00:00",
        "effectiveWindowDays": 15,
        "windowLimitedByRetention": false
      },
      {
        "uid": "ecommerce_checkout_abandon",
        "label": "Checkout abandoned",
        "count": 0,
        "received": false,
        "lastSeenAt": null,
        "effectiveWindowDays": 15,
        "windowLimitedByRetention": false
      }
    ],
    "cached": true,
    "computedAgo": "2 minutes",
    "summary": "1 of 2 declared event types received anything in the last 15 days. A zero means nothing arrived in this window, not that the type does not exist."
  }
}
```

The `window` block is the one to read: it reports the window that was **actually queried**, anchored to the instant of the call rather than to a calendar boundary.

The `sampling` block beside it says what it covers: the **type counts only**, and those genuinely are never sampled, because the aggregation runs over about thirty distinct values and costs the same whatever the document count. So `applied: false` is honest. It ships anyway, with `covers` and `reason`, so a consumer never has to infer the method from a missing key, and its `parameterValues` key points at the block that answers the other half of the question. That other half is [The response carries two sampling blocks](#the-response-carries-two-sampling-blocks).

### One level deeper: what the parameters carry

Pass `event` and the response gains an `event` block for that type, listing the real values of the parameters worth listing: product id and name, category, tags and vendor, the campaign id and name behind an attribution, its source and medium, statuses, methods. That is what turns "this project receives orders" into "this project sells these categories", which is the difference between guessing a filter value and reading one.

> **Tip**: Where a thing has both an id and a name, both halves come back and they are not interchangeable. **Filter on the id** (`product-id`, `utm-id`): it is stable and unique. **Read the name** (`product-name`, `utm-campaign`): it is neither. A condition written against a name breaks the moment the customer renames the product, and a campaign title reused across campaigns quietly matches more than one.

```bash
curl -X POST "$BASE/event/volumes" \
  -H "Authorization: Bearer $INSTASENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "event": "ecommerce_order_create" }'
```

```json
{
  "metadata": {
    "event": {
      "uid": "ecommerce_order_create",
      "count": 12894,
      "lastSeenAt": "2026-08-28T08:57:02+00:00",
      "parameters": [
        {
          "uid": "product-id",
          "values": [
            { "value": "WR3MTBK080", "count": 2140, "observedCount": 1070, "approximate": true, "reliable": true, "countError": 0 },
            { "value": "A11113U001", "count": 1618, "observedCount": 809,  "approximate": true, "reliable": true, "countError": 0 }
          ],
          "other": 9136,
          "truncated": true
        },
        {
          "uid": "product-name",
          "values": [
            { "value": "Men's Wool Runner, True Black", "count": 2140, "observedCount": 1070, "approximate": true, "reliable": true, "countError": 0 },
            { "value": "Anytime No Show Sock 3-Pack",    "count": 1618, "observedCount": 809,  "approximate": true, "reliable": true, "countError": 0 }
          ],
          "other": 9136,
          "truncated": true
        },
        {
          "uid": "product-category",
          "values": [
            { "value": "Skincare",   "count": 5210, "observedCount": 2605, "approximate": true, "reliable": true,  "countError": 0 },
            { "value": "Fragrance",  "count": 3140, "observedCount": 1570, "approximate": true, "reliable": true,  "countError": 0 },
            { "value": "Haircare",   "count": 1902, "observedCount": 951,  "approximate": true, "reliable": true,  "countError": 0 },
            { "value": "Gift sets",  "count": 16,   "observedCount": 8,    "approximate": true, "reliable": false, "countError": 0 }
          ],
          "other": 2626,
          "truncated": true
        }
      ],
      "sampling": {
        "applied": true,
        "fraction": 0.5,
        "buckets": "0-99",
        "band": "20k-50k",
        "scaled": true,
        "minReliableCount": 6,
        "unit": "contacts"
      }
    }
  }
}
```

Three things to note.

`other` above zero means those are the most frequent values, not all of them.

This block **is** sampled once the audience passes 10,000 contacts, and it carries its own `sampling` to say so. That is the block to read, not the top-level one: the example above was measured over half the audience, which is why every `count` is twice its `observedCount` and `Gift sets` comes back `reliable: false`. The rule and the fractions are in [The response carries two sampling blocks](#the-response-carries-two-sampling-blocks).

And when it does sample, it samples **by contact**, never by event: a fraction of the people and all of their events. That is sound for which values occur and noisier for how many events there are, because one heavy buyer's whole history enters or leaves as a block. The `unit: "contacts"` field says so explicitly.

A parameter that carries no `values` but a `valuesOmitted` string could not be aggregated. An event type whose parameters are all unlisted returns `parametersOmitted` rather than an empty list, so "nothing worth listing by value" never reads as "this event carries nothing".

### Worked example: from "does this arrive" to a count

You want to message people who bought skincare recently.

#### 1. Is the event even arriving?

Post an empty body to `/event/volumes`. `ecommerce_order_create` shows 12,894 in the window and a `lastSeenAt` of this morning, so the type is live. Had it come back `count: 0` with a null `lastSeenAt`, the honest next step would be widening `window`, not concluding the shop sells nothing.

#### 2. What are the real category names?

Probe again with `"event": "ecommerce_order_create"`. `product-category` holds `Skincare`, `Fragrance`, `Haircare` and more (`other` is above zero). The catalogue is capitalised and singular, so a filter written against `skincare` or `Skin care` would have matched nobody. Had you wanted one specific product rather than a category, the same answer carries `product-id` next to `product-name`: take the id for the condition and keep the name for the sentence you write around it.

#### 3. Compose the filter

Write a `group_event` condition on `ecommerce_order_create` with `product-category` matching `Skincare` over your timeframe. The grammar is on the [audience query filter](/developers/product-api/audience/query-filter#complex-event-filtering) page.

#### 4. Count before you promise

Post it to `/audience/count`. Expect a number lower than the 5,210 the probe reported: that figure counts orders, not people, it was itself scaled up from half the audience, and a segment evaluates a contact's own recent history rather than the full event index. The count is the size; the probe was only ever the way you found the value.

## Rules that keep a probe honest

Ten things a response shape will not tell you. They are the difference between a probe that informs a decision and a probe that manufactures a confident wrong answer.

### Read the window from the response, never from the request

`window.days` in the response is the window that was queried. It defaults **wider on small audiences** (90 days below 10,000 contacts, 30 up to 20,000, 15 above that), on the reasoning that the ambiguous zero hurts the low-volume account most. Whatever you ask for is then clamped by the subscription, and the clamp is reported rather than silently applied: `clamped`, `clampedBy` and `requestedDays` tell you it happened. Narrating "no purchases in the last 90 days" from a request that was served over 30 is the single easiest way to be precisely wrong.

### A null `lastSeenAt` means nothing arrived in *this* window

It does not mean the type was never seen, and nothing in the response can tell the two apart. Distinguishing them would take a scan outside the window, which is exactly what this endpoint exists not to do. Widen `window` before concluding a type is unused. The same holds for `count: 0` and `received: false`.

`effectiveWindowDays` is smaller than `window.days` when that event type is pruned sooner than the window reaches, and `windowLimitedByRetention` flags it. Read the effective figure, not the requested one.

### `datasource` means two different things

The same key, on the two endpoints, answers two different questions on two different indexes:

| Endpoint             | `datasource` selects                    |
| -------------------- | --------------------------------------- |
| `/audience/coverage` | contacts this source **contributed to** |
| `/event/volumes`     | events this source **produced**         |

On coverage it is membership, never provenance. An audience contact is a consolidated projection of every source that touched it, so a value counted on a contact this source contributed to may perfectly well have been supplied by another one. The response restates this in `datasource.meaning`, because the wording is the only mitigation available: "of the contacts this source contributed to" is routinely heard as "this source supplies the value", and it does not.

### The absence of top values is not an absence of values

When `topValues` is missing, `topValuesOmitted` says why, and none of the reasons is "empty":

- a known identity attribute, whose values are never listed;
- a name that reads as an identifier, a timestamp or a quantity;
- too many distinct values for a top-values list to mean anything;
- about one distinct value per contact that has it, so it reads as an identifier rather than a vocabulary;
- distinct values not measured.

The name-based skip is **overridable**: name the attribute explicitly in `attributes` and it is measured anyway. Asking for one by name is a decision, and the endpoint honours it.

### `min` and `max` disappear under sampling

A sample extreme is biased low for a maximum and high for a minimum, and no threshold repairs that. Under sampling they are omitted and the omission is stated in `stats.extremesOmitted`. `avg` survives and is still published. A "highest spend" that is really the top of a 5% sample is a figure someone repeats in a meeting, so it is not published at all.

### `sampling` is always present, `applied: false` included

A figure that does not carry its method gets quoted as exact. So the envelope ships on every response: `applied: false` still comes with `fraction: 1` rather than leaving you to infer absence from a missing key. Below 10,000 contacts nothing is sampled whatever the audience band says. Above it, the fraction narrows as the audience grows, and every figure carries whether it is approximate (`approximate`) and whether it cleared the detection floor (`reliable`, against `sampling.minReliableCount`).

An unreliable figure is a figure backed by too few observed contacts, not a zero. Treat `reliable: false` as "ask again with a narrower population", never as "this value does not occur".

On `/event/volumes` there are **two** of these envelopes, and only the nested one describes parameter values. That is the next rule.

### The response carries two sampling blocks

`/event/volumes` reports sampling twice, and reading the first as the whole story is the single easiest way to quote an estimate as an exact figure.

- The **top-level `sampling`** describes the per-type counts, and says so in its own `covers` key. It is always `applied: false`, and that is honest: the aggregation runs over about thirty distinct values and costs the same whatever the document count, so there is nothing to sample. Its `parameterValues` key points at the other block.
- **`event.sampling`**, present only when you passed `event`, describes the parameter values. Those **are** sampled once the audience passes 10,000 contacts.

The fraction comes from the audience band, the same table that sets the default window:

| Audience          | Parameter values measured over      |
| ----------------- | ----------------------------------- |
| under 10,000      | the whole audience, nothing sampled |
| 10,000 to 20,000  | 75%                                 |
| 20,000 to 50,000  | 50%                                 |
| 50,000 to 200,000 | 20%                                 |
| above 200,000     | 15%                                 |

A floor of 15% applies on the event side, which is why the two largest bands land on the same figure: on the contact side the question is what shape the audience has, and a shape survives a thin sample, while here the question is which values occur, and a value carried by few contacts disappears from a thin sample without leaving a trace.

Under sampling every value carries `observedCount`, what was actually seen, beside the `count` scaled back up to the audience, plus its own `approximate` and `reliable` flags. `reliable: false` means the observed figure did not clear `event.sampling.minReliableCount`, the detection floor for that fraction.

> **Warning**: A value **missing from a sampled list has not been shown not to occur**. Absence is not evidence here, and neither is `reliable: false`: it says the sample was too thin to measure the value dependably, never that the value is a zero. A `root` does not buy you a better sample either: unlike `/audience/coverage`, where the band follows the population you actually narrowed to, the event side takes its fraction from the project's whole audience. If a rare value matters, write the condition and post it to `/audience/count`, which is exact and never sampled.

### `refresh` is throttled, and a denial is not an error

Unfiltered profiles are cached, and the cache lives longer on larger audiences. `refresh: true` forces a recomputation, limited to one every five minutes per project and surface. When the throttle denies it you still get a `200`: the cached answer comes back carrying `refreshDeclined: true` and `nextRefreshIn` in words, so a client can act on it without interpreting an error. Check `cached` and `computedAgo` to know how old the figures are.

Any call carrying a `root` is computed live and never cached, which also makes it the more expensive of the two paths.

### Multivalue shares do not sum to the audience

On a multivalue attribute, each top value counts the contacts **holding** it, and one contact can hold several. The percentages therefore add up to more than 100, and `topValuesSemantics` says so on the entry. Never narrate them as shares of the audience.

### A probe sees more history than a segment evaluates

The event probe reads the event index, which keeps the project's history. A **segment**'s event condition is evaluated against a different, narrower store: the recent events kept on each contact. The practical consequence is one-directional and always the same. A value the probe lists can match fewer people than its count suggests, sometimes none.

> **Warning**: Use the event probe to **pick a value**, never to promise a size. Once you have the value, post the filter to `/audience/count` and quote that.

## What a probe is not

Four neighbouring surfaces, deliberately kept apart:

- **[`/audience/count`](/developers/product-api/audience/counting-and-retrieval#counting)** takes the same filter grammar and returns the exact, live total of matching contacts. It is the only endpoint whose number is a size. A probe never replaces it.
- **`/audience/aggregations`** runs *your* aggregations over the audience and needs the manually granted `PROJECT_AGGREGATIONS` scope. A probe builds its own fixed aggregations and needs only `PROJECT_AUDIENCE_READ`.
- **[Analytics](/developers/product-api/analytics/overview)** reports on what you **sent**: delivery, engagement, conversion, cost, over time. Probes report on what you **hold**: attributes and events, with no time series and no performance data.
- **`GET /specs/attributes` and `GET /specs/events`** return the project's declared schema, described in [Contacts and attributes](/developers/product-api/audience/contacts-and-attributes) and [Events](/developers/product-api/audience/events). Probes measure how much of that schema has data behind it.

## What's next

- **[Audience query filter](/developers/product-api/audience/query-filter)** (AQF): the grammar you write once the probe told you which attribute and which value, and the same grammar `/audience/coverage` accepts as `root`.
- **[Audience event query filter](/developers/product-api/audience/event-query-filter)** (EQF): the event-side grammar, and the `root` `/event/volumes` accepts.
- **[Counting and retrieval](/developers/product-api/audience/counting-and-retrieval)**: where the number you may actually quote comes from.
- **[Segments](/developers/product-api/audience/segments)**: keeping the filter the probe helped you write.
- **[Full API Reference](/developers/product-api/reference)**: request and response shapes for both endpoints.

---

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.
