Skip to main content

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.

One aggregate request runs at a time per workspace. A second request sent while the first is running gets 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 fields (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).
POST/v1/aggregate

Request body

fromstringStart of the window (included), ISO-8601 with an offset.
tostringEnd of the window (excluded), ISO-8601 with an offset. At most 31 days after from.
scopeobjectOptional. Restricts every query to some documents: document_ids, tags ({ any: [...] } or { all: [...] }), system_tags, source_connection_ids, document_types (ids or names), client_batch_ids, roots_only, attachments_only.
queries[].idstringYour name for the query (lowercase letters, digits, _); the answer is keyed by it.
queries[].datasetstringWhat to count; see GET /v1/aggregate/catalog.
queries[].measurestring | string[]count, rows, count_distinct, sum, avg, min, max, median or p1–p99. A list answers several at once.
queries[].ofstringThe field a measure other than count / rows is computed over.
queries[].wherearrayFilters: { 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.
queries[].group_bystring[]Up to three groupable fields, or 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, and details lists 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 by model, and cost visibility is off for your organization.
  • 422 aggregate_timeout: one statement ran longer than limits.statement_timeout_ms and the request was stopped; details.dataset names the dataset. Ask for a shorter window, fewer groups or fewer queries.
  • 429 with Retry-After: another aggregate request of your workspace is still running. Retry after that many seconds.
  • 429 with X-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?+
Most datasets count rows created in the window and report their state now. A document that arrived in the window and finishes processing later moves from processing to completed or failed. Once all work from the window is done, its numbers stop changing.
Why do I get 400 when I add a scope?+
Some datasets are not about documents, for example delivery attempts or app runs. A document scope cannot apply to them, so the request is refused instead of the scope being ignored. The catalog marks each such dataset with document_scope: false; send its queries in a separate request without a scope.
Can I count Accounts Payable bills waiting for a person?+
Not with this endpoint. Accounts Payable works out each bill and its status when you read it, so no stored list of bills exists to count. Read the totals from GET /v1/ap/overview instead. The Money Found ledger is the same: read it from GET /v1/money-found/levers.
What does document_cost measure?+
For each document created in the window, what its processing has cost so far: model calls and OCR, at list price. It is not what your workspace is charged in credits, and a document without any calls counts with 0. Its cost_usd field needs cost visibility for your organization; counting documents and reading pages (OCR pages) work without it.
How do I find out which fields a dataset has?+
Call GET /v1/aggregate/catalog. It lists every dataset with its fields, their types, whether they can be grouped and which filters they take. A request with a wrong name also answers 400 with the allowed names in details.