Chart signal volume
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 instead.
Prerequisites
- Have your API key ready. The
endpoint needs the
read:analyticsscope.
A first call
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.
Grouping
groupBy=signalName cuts each period by the signal’s own name:
groupBy=dimension cuts each period by a field in your signal payload. Name
the field in dimensionKey:
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 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:
groups.truncated is 0 when every group kept its own figure. Read it before
you trust a per-group number.
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.
A payload value that is itself literally other is counted in the same bucket
as the remainder.
Filtering
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:
CSV
Add format=csv for the same points as a text/csv table with one header row:
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.