Aggregate Overview
Count documents, failures, app decisions, deliveries, model calls and cost per document in one request. Numbers only, filtered, grouped by field or by day.
POST /v1/aggregate answers several questions about your workspace in one request and returns numbers, never document contents. A typical dashboard asks: how many documents arrived in the last 12 hours, how many of them failed, how they split by document type, and what processing a document cost on average. Each of those is one query in the same request, and the answer comes back keyed by the id you gave each query.
Every request names a time window with from and to. Both are ISO-8601 timestamps that carry an offset, such as 2026-11-05T12:00:00Z; a timestamp without one is refused, because it would be read in an unknown time zone. The window includes from and excludes to, so two back-to-back windows never count the same row twice. A window can be at most 31 days long.
Each query picks a dataset (what you are counting, such as documents or deliveries), a measure (count, sum, avg, median, a percentile like p90, and a few more) and optionally where filters and group_by fields. GET /v1/aggregate/catalog lists every dataset with a sentence on what one row is, every field with its type and the filters it takes, and every limit. Read it first: it is generated from the same definitions the endpoint checks requests against, so it is always current.
Most datasets count things created in the window and report their state now. A document that arrived yesterday and failed this morning counts as failed in yesterday's window. This makes repeated reports for the same window change slightly while work is still in progress, and settle once it is done. as_of in the answer says when the numbers were read; all queries of one request read the same moment.
scope narrows every query of the request to some documents: by id, by tag, by source, by document type, by the batch_id you sent at upload, or to top-level documents or attachments only. Datasets that are not about documents refuse a scope with a 400, so a filter never silently does nothing; the catalog marks them with document_scope: false. Rows tied to a document the owner of the API key may not see are left out. Rows tied to no document at all, such as a model call outside any document, still count unless a scope is set.
429 with a Retry-After header: wait that many seconds and send it again. Schedule dashboards one after the other rather than in parallel. (On a deployment that does not share this limit across its API processes, the limit is per process; the catalog says which in limits.concurrency.per.)cost_usd, credit_cost) and grouping by model need cost visibility for your organization. Without it they answer 403 cost_not_visible; every other field still works. GET /v1/aggregate/catalog says whether your workspace has it (cost_visible) and marks each field that needs it (cost: true)./v1/aggregateRequest body
from.document_ids, tags ({ any: [...] } or { all: [...] }), system_tags, source_connection_ids, document_types (ids or names), client_batch_ids, roots_only, attachments_only._); the answer is keyed by it.GET /v1/aggregate/catalog.count, rows, count_distinct, sum, avg, min, max, median or p1–p99. A list answers several at once.count / rows is computed over.{ field, op, value }. Operators: eq, neq, in, not_in, is_null, not_null, gt, gte, lt, lte, contains_any, contains_all; the catalog lists which ones each field takes.hour / day (UTC).Request — the last 12 hours
curl -s https://api.talonic.com/v1/aggregate \
-H "Authorization: Bearer $TALONIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "2026-11-05T12:00:00Z",
"to": "2026-11-06T00:00:00Z",
"queries": [
{ "id": "received", "dataset": "documents", "measure": "count",
"where": [{ "field": "duplicate", "op": "eq", "value": false }] },
{ "id": "failed", "dataset": "documents", "measure": "count",
"where": [{ "field": "failed", "op": "eq", "value": true }] },
{ "id": "by_type", "dataset": "documents", "measure": "count",
"group_by": ["document_type"] },
{ "id": "cost", "dataset": "document_cost",
"measure": ["avg", "median", "p90"], "of": "cost_usd" }
]
}'Response
{
"window": { "from": "2026-11-05T12:00:00.000Z", "to": "2026-11-06T00:00:00.000Z" },
"as_of": "2026-11-06T00:00:03.120Z",
"results": {
"received": { "dataset": "documents", "kind": "entity", "value": 412 },
"failed": { "dataset": "documents", "kind": "entity", "value": 3 },
"by_type": {
"dataset": "documents", "kind": "entity",
"groups": [
{ "key": { "document_type": "Invoice" }, "value": 301 },
{ "key": { "document_type": "Delivery Note" }, "value": 97 },
{ "key": { "document_type": null }, "value": 14 }
],
"groups_total": 3
},
"cost": { "dataset": "document_cost", "kind": "entity",
"values": { "avg": 0.0108, "median": 0.0104, "p90": 0.0124, "n": 412 } }
}
}Reading the answer
A query with one measure answers value; a query with a list of measures answers values, one entry per measure, plus n, the number of rows they were computed over. A grouped query answers groups, each with the key it groups by and its value; a null key collects the rows without a value for that field (above: documents not yet classified). Groups come largest first, or in time order when grouped by hour or day.
At most 200 groups come back per query. groups_total says how many existed, so when it is larger than the number of groups returned, narrow the query with a where filter or a shorter window. A computed measure such as avg or sum is null when there were no rows to compute it over; counts are 0.
Request — failures per day, by source
{
"from": "2026-10-07T00:00:00Z",
"to": "2026-11-06T00:00:00Z",
"queries": [
{ "id": "failed_daily", "dataset": "documents", "measure": "count",
"where": [{ "field": "failed", "op": "eq", "value": true }],
"group_by": ["day", "source_connection_id"] }
]
}Errors
400 invalid_aggregate: a name, operator, value or limit is wrong. The message says which, anddetailslists the names allowed there (allowed_datasets,allowed_fields,allowed_ops,allowed_keys).403 cost_not_visible: the request asks for a cost field or groups bymodel, and cost visibility is off for your organization.422 aggregate_timeout: one statement ran longer thanlimits.statement_timeout_msand the request was stopped;details.datasetnames the dataset. Ask for a shorter window, fewer groups or fewer queries.429withRetry-After: another aggregate request of your workspace is still running. Retry after that many seconds.429withX-RateLimit-Reset: the daily rate limit of your workspace is used up until that time.
Frequently asked questions
Why does a number for a past window change when I ask again?+
Why do I get 400 when I add a scope?+
document_scope: false; send its queries in a separate request without a scope.Can I count Accounts Payable bills waiting for a person?+
What does document_cost measure?+
cost_usd field needs cost visibility for your organization; counting documents and reading pages (OCR pages) work without it.