Chart signal volume

GET /v2/signals/summary counts your signals into period buckets and returns them as a series you can chart. It is built for a page that loads on every view: it reads a pre-aggregated hourly rollup rather than your raw signals, so a 90-day window answers in well under a second.

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 instead.

Prerequisites

  • Have your API key ready. The endpoint needs the read:analytics scope.

A first call

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
{
"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 a single signal reporting a batch of 100 adds 100. They are equal when every signal reported a single occurrence.

Buckets and time basis

Signals are bucketed on their effective timestamp in UTC — 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 grain available, because the rollup behind these figures is itself hourly. The first and last bucket’s published bounds are clipped to your requested window, so a window starting at noon reports a first bucket that starts at noon.

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

Grouping

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

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:

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.

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 returns the busiest dimensionLimit groups (10 by default, 100 at most) and folds every other group into a single series under the key other. Nothing is dropped: the counts in other are the counts of all the groups it stands for, so the periods still total correctly.

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

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

Filtering

ParameterEffect
customerId / externalCustomerIdNarrow to one customer. Give one, not both
signalNamesNarrow to these signal names. Repeat the parameter for more
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:

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 window whose result does not fit one page returns 400 CSV_RESULT_TRUNCATED rather than a file quietly missing rows. Narrow the window, lower dimensionLimit, raise limit, request format=json to page, or start a batch export for a file.

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

CodeMeaning
DIMENSION_NOT_DECLAREDdimensionKey is not declared for grouping in your org
DIMENSION_KEY_REQUIREDgroupBy=dimension was sent without a dimensionKey
WINDOW_TOO_LARGEThe window holds more than 2000 periods at this interval
INVALID_CURSORThe cursor could not be read
CONCURRENCY_LIMIT_EXCEEDEDToo many summaries running at once for your organization
QUERY_TIMEOUTThe summary exceeded its budget. Narrow the window