Skip to navigation

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

intervalLongest window
hour250 hours, about 10 days
day250 days, about 8 months
week250 weeks, about 4.8 years
month250 months, about 20 years

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 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:

{
"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.

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

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

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 250 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
CSV_RESULT_TRUNCATEDformat=csv was asked for a result that needs more than one page
CSV_CURSOR_UNSUPPORTEDcursor was sent with format=csv