Batch reads
POST /api/v1/metrics/batch resolves up to 50 metrics in one request. Use it for dashboards, reports, or anywhere you'd otherwise fan out 10+ parallel GET calls.
Why batch
Two reasons matter beyond the obvious "fewer round trips":
- One consistent
asOf— every query in the batch shares one reference date, so the numbers describe one moment in time. - Cross-query memoization — the registry caches resolved values per request. When two queries depend on the same helper (e.g.
revenue.totalandexpenses.totalboth use the P&L), it computes once.
Request
json
POST /api/v1/metrics/batch
{
"asOf": "2024-12-01",
"queries": [
{ "key": "rev", "id": "revenue.total", "period": "ytd" },
{ "key": "cash", "id": "cash.balance", "period": "today" },
{ "key": "people", "id": "headcount.active", "period": "today" },
{ "key": "paygap", "id": "paygap.gender_median_gap", "period": "today" }
]
}| Field | Required | Notes |
|---|---|---|
queries | Yes | Array of 1–50 { key, id, period } items |
queries[].key | Yes | Caller-supplied identifier. Must be unique within the batch |
queries[].id | Yes | Metric id from the reference |
queries[].period | Yes | Period string — same syntax as ?period= |
asOf | No | ISO date or today. Pins all relative periods |
Response
json
{
"results": {
"rev": { "ok": true, "data": { "kind": "scalar", "value": 49912.40, "currency": "NZD" }, "meta": { "metric": "revenue.total", "period": { … } } },
"cash": { "ok": true, "data": { "kind": "scalar", "value": 515612.89, "currency": "NZD" }, "meta": { … } },
"people": { "ok": true, "data": { "kind": "scalar", "value": 18 }, "meta": { … } },
"paygap": { "ok": false, "error": { "code": "FORBIDDEN_CAPABILITY", "message": "…", "detail": { "capability": "people.view_paygap" } } }
},
"meta": {
"asOf": "2024-12-01",
"baseCurrency": "NZD",
"computedAt": "2026-05-07T03:14:22Z",
"version": 1
}
}A failing query produces { ok: false, error }. Sibling queries are unaffected — the batch never short-circuits.
Limits
| Field | Limit |
|---|---|
| Max queries per batch | 50 |
| Max body size | 100 KB (default Next.js handler limit) |
| Per-batch timeout | None enforced server-side; client should set its own |
For batches larger than 50, split client-side and merge.
Worked example: pinning a dashboard to a past date
js
const res = await fetch("/api/v1/metrics/batch", {
method: "POST",
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${KEY}` },
body: JSON.stringify({
asOf: "2024-12-01",
queries: [
{ key: "rev", id: "revenue.total", period: "mtd" },
{ key: "expenses", id: "expenses.total", period: "mtd" },
{ key: "cash", id: "cash.balance", period: "today" },
{ key: "ar", id: "ar.outstanding_total", period: "today" },
{ key: "ap", id: "ap.outstanding_total", period: "today" },
],
}),
});
const { results } = await res.json();All five numbers describe the business as it stood on 1 December 2024 — mtd resolves to the November 2024 month-to-date window, snapshots resolve at end-of-day on the pinned date, and the cross-query cache means the underlying P&L computes exactly once.