Skip to content

Guide

Reading data

POST /v1/query, and how to read the provenance that comes with every result.

POST /v1/query returns published daily sessions for one connection and a window of at most 31 days.

POST /v1/query
Authorization: Bearer <key>
Content-Type: application/json
{
"workspaceId": "your_workspace",
"connectionId": "your_connection",
"propertyId": "123456789",
"startDate": "2026-08-31",
"endDate": "2026-09-04",
"metric": "ga4.sessions",
"dimensions": ["date"]
}

metric and dimensions are fixed in this version — ga4.sessions and ["date"]. They are required rather than defaulted, so a request always states what it asked for, and a future version that adds metrics will not silently change what an old caller receives.

Unknown fields are rejected, not ignored. A typo in a field name is an error rather than a silently different query.

In the examples below the identifiers are placeholders; the daily values, the total and the provenance timestamps are from a run recorded on staging on 7 September 2026.

{
"metric": "ga4.sessions",
"rows": [
{ "date": "2026-08-31", "sessions": 27 },
{ "date": "2026-09-01", "sessions": 35 },
{ "date": "2026-09-02", "sessions": 18 },
{ "date": "2026-09-03", "sessions": 34 },
{ "date": "2026-09-04", "sessions": 15 }
],
"total": 129,
"dataState": "observed",
"provenance": { "…": "see below" }
}

rows has exactly one entry per requested date, ascending, with no gaps — if a date could not be answered, the whole request is a 409 rather than a shorter array. You do not need to reconcile the array against your requested window.

dataState is zero when the window totals zero and observed otherwise. Neither value ever represents missing or unavailable data; that is what the 409 codes are for.

What a zero does and does not tell you. A date the provider returned no row for is filled with zero — but only when the report passed its quality gate, so a zero is never covering for a thresholded, sampled or lossy extraction. What it cannot tell you is whether the property was collecting correctly: a tag that stopped firing produces a genuine, complete report of zero sessions. Treat a zero as “the provider reported nothing for this day”, not as “nothing happened”.

This is the part worth building against.

{
"workspaceId": "your_workspace",
"connectionId": "your_connection",
"propertyId": "123456789",
"timezone": "Europe/Oslo",
"metricDefinition": { "id": "ga4.sessions", "version": 1, "…": "…" },
"window": { "startDate": "2026-08-31", "endDate": "2026-09-04" },
"dataThrough": "2026-09-04",
"source": "ga4_data_api",
"completeness": "complete",
"reconciliation": "not_independently_reconciled",
"freshness": {
"oldestExtractedAt": "2026-09-07T04:20:09.494Z",
"newestExtractedAt": "2026-09-07T04:20:09.494Z",
"stale": false,
"thresholdHours": 30
},
"flags": ["provider_may_revise_recent_data", "not_independently_reconciled"],
"snapshots": [
{
"date": "2026-08-31",
"runId": "78d95c71-2f6e-4b35-8729-6d8d4b6ecc86",
"extractionSeq": 9,
"publishedAt": "2026-09-07T04:20:11.781Z",
"extractedAt": "2026-09-07T04:20:09.494Z"
}
]
}

metricDefinition is embedded, not referenced. A result stored in a warehouse or pasted into a report still carries the definition that produced it, so nobody has to guess later whether “sessions” meant the provider metric or a reconstructed event count. It is the provider metric — it will not match a session_start count.

timezone is the property’s own. Dates are that property’s calendar dates and are never shifted to UTC. A day here is the day your analytics team means. If a window’s published dates carry different timezones, the query fails before reading analytics storage with 409 timezone_conflict. Re-sync all requested dates using the property’s current timezone, then retry. That error does not add missingDates.

snapshots is how you establish that two outputs read the same data. Matching totals prove very little — two different snapshots of a stable week produce the same total. Matching runId and extractionSeq for a date proves the same publication. If you are checking that a dashboard, a spreadsheet and an AI answer agree, compare snapshots, not totals.

freshness.stale turns true when the oldest snapshot in the window is more than 30 hours old, and a stale_source_snapshot flag is added. Stale does not mean wrong; it means nothing has re-extracted this window recently.

flags should reach whoever reads the number. provider_may_revise_recent_data means a recent day can legitimately change value later. not_independently_reconciled means the product has not re-derived the number from a second source. Both are true of every result today.

completeness is always complete on a 200. You do not need to check it — an incomplete window is a 409. It is present so that a stored result is self-describing.

A window is answered only when every date in it has a published snapshot.

  • 409 data_not_ready — nothing in the window is published.
  • 409 data_incomplete — some dates are.

Both name the dates:

{
"error": {
"code": "data_incomplete",
"message": "Complete data is not available for every requested day.",
"missingDates": ["2026-09-03", "2026-09-04"]
}
}

Narrow the window to the published dates, or run a sync and retry. Both are expected states, not faults.

The closed interval must contain 1 to 31 days. Queries are not gated on UTC: publication decides what can be returned. Sync now checks completed dates in the property’s returned timezone, so a completed Oslo day is accepted immediately after local midnight even when UTC has not changed dates. The stage 2 correction is locally tested; new live automation acceptance remains pending.