> This page is for version v2 (default).
> For other versions, use one of these documentation indexes:
> - v2 (default): https://docs.paid.ai/v-2/llms.txt
> - v1: https://docs.paid.ai/v-1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.paid.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.paid.ai/_mcp/server.

# Chart signal volume

> Count signals into period buckets, grouped by signal name or by a field in your signal payload

`GET /v2/signals/summary` counts your signals into period buckets. It returns
them as a series you can chart.

The endpoint reads a pre-aggregated hourly rollup, not your raw signals. A
90-day window therefore answers in well under a second, which makes it safe on
a page that loads on every view.

Use it for "signals per day", "signals per team per week", or any volume chart
your product shows a customer. For a wide per-customer spreadsheet over
arbitrary payload fields, use the
[batch export](/v2/api-reference/signals/create-signal-summary-export) instead.

## Prerequisites

* Have your [API key](https://app.paid.ai/settings/api-keys) ready. The
  endpoint needs the `read:analytics` scope.

## A first call

```bash
curl -G https://api.agentpaid.io/api/v2/signals/summary \
  -H "Authorization: Bearer $PAID_API_KEY" \
  -d startDate=2026-08-01 \
  -d endDate=2026-08-31 \
  -d interval=day
```

```json
{
  "interval": "day",
  "data": [
    {
      "periodStart": "2026-08-01T00:00:00.000Z",
      "periodEnd": "2026-08-01T23:59:59.999Z",
      "signalCount": 4213,
      "quantityTotal": 5102
    }
  ],
  "pagination": { "limit": 1000, "hasMore": false, "nextCursor": null }
}
```

`signalCount` counts one per signal. `quantityTotal` adds up the quantity each
signal reported, so one signal that reports a batch of 100 adds 100. The two
are equal when every signal reports a single occurrence.

## Buckets and time basis

Signals count against their **effective timestamp** in UTC. This is the same
basis billing uses, so these counts reconcile with what you are invoiced for.

`interval` accepts `hour`, `day`, `week` (Monday start) and `month`. An hour is
the finest period available, because the rollup behind these figures is itself
hourly.

The first and last bucket report bounds clipped to your window. A window that
starts at noon reports a first bucket that starts at noon.

Only periods that recorded signals appear. A quiet day is absent rather than
zero.

### How long a window may be

One request may span at most **250 periods**. A longer window returns
`400 WINDOW_TOO_LARGE`. Use a coarser interval, or split the window across two
calls.

| `interval` | Longest window             |
| ---------- | -------------------------- |
| `hour`     | 250 hours, about 10 days   |
| `day`      | 250 days, about 8 months   |
| `week`     | 250 weeks, about 4.8 years |
| `month`    | 250 months, about 20 years |

## Grouping

`groupBy=signalName` cuts each period by the signal's own name:

```bash
curl -G https://api.agentpaid.io/api/v2/signals/summary \
  -H "Authorization: Bearer $PAID_API_KEY" \
  -d startDate=2026-08-01 -d endDate=2026-08-31 -d interval=day \
  -d groupBy=signalName
```

`groupBy=dimension` cuts each period by a field in your signal payload. Name
the field in `dimensionKey`:

```bash
curl -G https://api.agentpaid.io/api/v2/signals/summary \
  -H "Authorization: Bearer $PAID_API_KEY" \
  -d startDate=2026-08-01 -d endDate=2026-08-31 -d interval=day \
  -d groupBy=dimension -d dimensionKey=team_id
```

Each point then carries a `groupKey` holding that signal's `team_id`. A signal
whose payload lacks the field, or holds it empty, groups under an empty
`groupKey`.

> **Note**
>
> Payload keys must be declared for grouping before they can be used. An
> undeclared key returns `400 DIMENSION_NOT_DECLARED` and names the keys that
> are declared. Contact Paid to declare a new one.

### The `other` bucket

A grouped request keeps the **100 groups with the most signals** over the whole
window. It sums every other group into a single series under the key `other`.

This bound keeps a grouped chart readable. One organization sends signals
across more than a thousand teams. Ungrouped, a 90-day daily series over those
teams would be roughly 200,000 points.

Each response reports the fold:

```json
{
  "interval": "day",
  "groupBy": "dimension",
  "dimensionKey": "team_id",
  "groups": { "kept": 100, "truncated": 37 },
  "data": [],
  "pagination": { "limit": 1000, "hasMore": false, "nextCursor": null }
}
```

`groups.truncated` is `0` when every group kept its own figure. Read it before
you trust a per-group number.

> **Warning**
>
> Each period still totals correctly, because `other` carries the counts of
> every group it stands for. A single group inside `other` does **not**: it has
> no figure of its own in the response, and a chart drawn from a truncated
> response will show it as absent.
>
> To name every group, narrow the request with `signalNames`, with `customerId`,
> or with a shorter window, until `groups.truncated` is `0`.

> **Note**
>
> A payload value that is itself literally `other` is counted in the same bucket
> as the remainder.

## Filtering

| Parameter                           | Effect                                                      |
| ----------------------------------- | ----------------------------------------------------------- |
| `customerId` / `externalCustomerId` | Narrow to one customer. Give one, not both                  |
| `signalNames`                       | Narrow to these signal names. Repeat the parameter for more |

```bash
curl -G https://api.agentpaid.io/api/v2/signals/summary \
  -H "Authorization: Bearer $PAID_API_KEY" \
  -d startDate=2026-08-01 -d endDate=2026-08-31 -d interval=day \
  -d signalNames=ticket_resolved -d signalNames=ticket_opened
```

## Paging

Results are paged explicitly and are never truncated in silence. When more
points exist, `pagination.hasMore` is `true` and `pagination.nextCursor` holds
an opaque cursor. Pass it back as `cursor` with every other parameter
unchanged:

```bash
curl -G https://api.agentpaid.io/api/v2/signals/summary \
  -H "Authorization: Bearer $PAID_API_KEY" \
  -d startDate=2026-08-01 -d endDate=2026-08-31 -d interval=day \
  -d groupBy=signalName \
  -d cursor=WyIyMDI2LTA4LTAyVDAwOjAwOjAwWiIsInRpY2tldF9yZXNvbHZlZCJd
```

## CSV

Add `format=csv` for the same points as a `text/csv` table with one header row:

```
period_start,period_end,group_key,signal_count,quantity_total
2026-08-01T00:00:00.000Z,2026-08-01T23:59:59.999Z,ticket_resolved,4213,5102
```

A CSV has nowhere to carry a cursor, and a response header is not readable by
cross-origin browser code or by the generated CLI and MCP clients. So a CSV is
returned whole or refused. A result that does not fit one page returns
`400 CSV_RESULT_TRUNCATED` rather than a file quietly missing rows.

Four ways past it: narrow the window, raise `limit` (5000 at most), request
`format=json` and page through the result, or start a batch export for a file.

Passing `cursor` with `format=csv` returns `400 CSV_CURSOR_UNSUPPORTED` for the
same reason.

Group truncation is the other case, and it behaves differently. It never
refuses a CSV. The `other` row is written into the file like any other row, so
nothing is hidden and the periods still total correctly. Only paging is
refused, because only paging could hand you a file that looks whole and is
not.

## Window alignment

`startDate` and `endDate` must fall on whole UTC hours, because the summary
reads an hourly rollup. A window starting at 06:30 has no answer: its first
bucket covers 06:00 to 06:59, so counting it would include signals before the
window and dropping it would lose signals inside one. A mid-hour bound returns
`400 WINDOW_NOT_HOUR_ALIGNED` rather than a total that silently describes a
different window.

A date-only value always satisfies this: `2026-08-01` is midnight, and as an
`endDate` it means through `23:59:59.999`, the final instant of that day's last
hour.

## Comparing two periods

There is no `compareToPrevious` parameter. Call the endpoint twice with the two
windows. It is cheap enough that a second call costs less than the ambiguity
of one endpoint answering about two periods at once.

## Errors

| Code                         | Meaning                                                           |
| ---------------------------- | ----------------------------------------------------------------- |
| `DIMENSION_NOT_DECLARED`     | `dimensionKey` is not declared for grouping in your org           |
| `DIMENSION_KEY_REQUIRED`     | `groupBy=dimension` was sent without a `dimensionKey`             |
| `WINDOW_TOO_LARGE`           | The window holds more than 250 periods at this `interval`         |
| `INVALID_CURSOR`             | The `cursor` could not be read                                    |
| `CONCURRENCY_LIMIT_EXCEEDED` | Too many summaries running at once for your organization          |
| `QUERY_TIMEOUT`              | The summary exceeded its budget. Narrow the window                |
| `CSV_RESULT_TRUNCATED`       | `format=csv` was asked for a result that needs more than one page |
| `CSV_CURSOR_UNSUPPORTED`     | `cursor` was sent with `format=csv`                               |