Reading results
The Analytics response shapes: values reports, time series, grouped series and compare reports, plus the freshness block, the comparison block, warnings and the error model.
Every Analytics report returns the same family of response shapes. Once you can read one, you can read them all: each carries the resolved timeframe, the output currency, a freshness block describing how fresh the numbers are, and a warnings array. This page walks through each shape.
Values report
A values report returns aggregate totals over the resolved window in a statistics object. Counts are integers, rates are fractions (4 decimals), monetary amounts have 2 decimals, and ratios like roas are dimensionless. Empty windows return explicit zeros rather than omitting keys. The only legitimate null is a revenue statistic for a scope the project's conversion configuration does not include (it always comes with a conversion_scope_unconfigured warning); a configured scope with no conversions or no value returns 0.
{
"entity": {
"resource": { "type": "overview" },
"timeframe": {
"key": "last_3_months",
"start": "2026-03-01T00:00:00+01:00",
"end": "2026-05-29T23:59:59+02:00",
"timezone": "Europe/Madrid"
},
"conversion_scope": "sales",
"currency": "EUR",
"source_currency": "EUR",
"statistics": {
"delivered": 48210,
"failed": 1203,
"delivery_rate": 0.9757,
"clicks_first": 5120,
"click_rate": 0.1062,
"conversions": 612,
"conversion_value": 38420.55,
"roas": 15.94,
"unsubscribes": 88,
"unsubscribe_rate": 0.0018
},
"freshness": {
"computed_at": "2026-05-29T10:00:00Z",
"source": "cache",
"stale": false,
"ttl_seconds_remaining": 1842
},
"warnings": []
}
}For per-entity reports (a single campaign, automation or flow), resource carries that entity's public metadata — id, name, status, channel, the fallback channel_chain, option count, audience size and the relevant timestamps, whichever of those apply to the entity — so you can describe the report without a second lookup.
Grouped values
When you add group_by, the values response switches to the grouped shape — and this works on every family that accepts a breakdown, the per-entity campaign and automation reports and the transactional rollup included, not just the account overview. group_by is strictly single-dimension: statistics becomes a map keyed by each group value, where each value is a full statistics object (the same fields the flat report returns). Alongside it, totals_per_group carries the cross-group roll-up — the same totals the flat report would return — group_order (an array of group keys in rank order) makes the ranking explicit, and group_by echoes back the one dimension as a single-element array.
{
"entity": {
"resource": { "type": "campaign", "id": "6627f1a2b3c4d5e6f7a8b9c0", "name": "Spring sale" },
"timeframe": {
"key": null,
"start": "2026-04-12T00:00:00+02:00",
"end": "2026-05-29T23:59:59+02:00",
"timezone": "Europe/Madrid"
},
"conversion_scope": "sales",
"currency": "EUR",
"source_currency": "EUR",
"statistics": {
"sms": { "delivered": 5, "conversions": 2, "conversion_value": 236.80 },
"rcs": { "delivered": 3, "conversions": 1, "conversion_value": 150.00 }
},
"totals_per_group": { "delivered": 8, "conversions": 3, "conversion_value": 386.80 },
"group_order": ["sms", "rcs"],
"group_by": ["channel"],
"freshness": { "computed_at": "2026-05-28T10:14:33Z", "source": "entity_stats", "stale": false, "ttl_seconds_remaining": null },
"warnings": []
}
}Open-cardinality dimensions (country, language, segment) return only their top 20 groups ordered by your first statistic, with no other overflow bucket — the same rule the grouped series follows. A request with two or more group_by dimensions is rejected with too_many_group_by_dimensions; to look at one dimension within another, pair a single group_by with a filter — see Cross-cutting two dimensions. For which group_by each report accepts, see the support matrix.
Time series
A series report returns a single continuous, zero-filled date_times axis plus one array per statistic, each the same length as date_times and index-aligned. There are no gaps to interpolate — empty buckets are explicit zeros. The report echoes the effective interval.
{
"entity": {
"resource": { "type": "campaign", "id": "6627f1a2b3c4d5e6f7a8b9c0", "name": "Spring sale" },
"timeframe": {
"key": "last_30_days",
"start": "2026-05-01T00:00:00+02:00",
"end": "2026-05-30T23:59:59+02:00",
"timezone": "Europe/Madrid"
},
"currency": "EUR",
"source_currency": "EUR",
"interval": "day",
"date_times": ["2026-05-01", "2026-05-02", "2026-05-03"],
"series": {
"delivered": [0, 8, 0],
"clicks_first": [0, 3, 0],
"conversions": [0, 3, 0],
"conversion_value": [0, 386.80, 0]
},
"freshness": { "computed_at": "2026-05-28T10:15:02Z", "source": "live", "stale": false, "ttl_seconds_remaining": null },
"warnings": []
}
}Grouped series
When you add group_by, the series response switches to the grouped shape. group_by is strictly single-dimension: series is keyed by each group value (a flat map, never nested under a second dimension), totals_per_group rolls them up across groups (the same totals the flat report returns), and group_order (an array of group keys in rank order) makes the ranking explicit. The group_by field echoes back the one dimension as a single-element array.
{
"entity": {
"timeframe": {
"key": "last_30_days",
"start": "2026-05-01T00:00:00+02:00",
"end": "2026-05-30T23:59:59+02:00",
"timezone": "Europe/Madrid"
},
"currency": "EUR",
"source_currency": "EUR",
"interval": "day",
"date_times": ["2026-05-01", "2026-05-02", "2026-05-03"],
"group_by": ["channel"],
"totals_per_group": {
"delivered": [0, 1200, 1800],
"clicks_first": [0, 210, 350]
},
"series": {
"sms": {
"delivered": [0, 800, 1200],
"clicks_first": [0, 120, 200]
},
"rcs": {
"delivered": [0, 400, 600],
"clicks_first": [0, 90, 150]
}
},
"group_order": ["sms", "rcs"],
"freshness": { "computed_at": "2026-05-29T10:15:02Z", "source": "live", "stale": false, "ttl_seconds_remaining": null },
"warnings": []
}
}A request with two or more group_by dimensions is rejected with too_many_group_by_dimensions — there is no nested keying and no per-outer roll-up, and multi-dimensional grouping will not be added. To look at one dimension within another, pair a single group_by with a filter — see Cross-cutting two dimensions. Open-cardinality dimensions (country, language, segment) return only their top 20 buckets ordered by your first statistic; there is no other overflow bucket — the tail is simply not keyed. For which group_by each report accepts, see the support matrix.
Comparison
When you request compare_to=previous_period on a values report, the response adds a comparison block alongside statistics under entity: the resolved previous window, the same statistics for it, and a delta object with the absolute change per statistic plus a _pct fractional change (null when the previous value was zero). The fragment below shows just that block — in a full response it sits inside entity, next to statistics.
"comparison": {
"timeframe": {
"start": "2026-03-31T00:00:00+02:00",
"end": "2026-04-29T23:59:59+02:00"
},
"statistics": { "conversion_value": 7420.10 },
"delta": { "conversion_value": 1490.20, "conversion_value_pct": 0.2008 }
}Compare reports
The campaign and automation compare endpoints return one aligned values report per entity over a shared timeframe, ordered descending by your first requested statistic, plus a summary that names the best and worst entity for each statistic.
{
"entity": {
"timeframe": {
"key": "last_90_days",
"start": "2026-03-01T00:00:00+01:00",
"end": "2026-05-29T23:59:59+02:00",
"timezone": "Europe/Madrid"
},
"conversion_scope": "sales",
"currency": "EUR",
"source_currency": "EUR",
"reports": [
{
"resource": { "type": "campaign", "id": "6627f1a2b3c4d5e6f7a8b9c1", "name": "Black Friday", "status": "sent" },
"statistics": { "delivery_rate": 0.9120, "click_rate": 0.4123, "conversion_value": 542.15, "roas": 980.45 }
},
{
"resource": { "type": "campaign", "id": "6627f1a2b3c4d5e6f7a8b9c0", "name": "Spring sale", "status": "sent" },
"statistics": { "delivery_rate": 0.8889, "click_rate": 0.3750, "conversion_value": 386.80, "roas": 1175.69 }
}
],
"summary": {
"best": { "delivery_rate": "6627f1a2b3c4d5e6f7a8b9c1", "roas": "6627f1a2b3c4d5e6f7a8b9c0" },
"worst": { "delivery_rate": "6627f1a2b3c4d5e6f7a8b9c0", "roas": "6627f1a2b3c4d5e6f7a8b9c1" }
},
"freshness": { "computed_at": "2026-05-29T11:02:14Z", "source": "live", "stale": false, "ttl_seconds_remaining": null },
"warnings": []
}
}The compare set is posted in the request body, alongside the statistics, timeframe and conversion scope (the request body is not wrapped — only responses carry the entity envelope):
{
"campaigns": [
"6627f1a2b3c4d5e6f7a8b9c0",
"6627f1a2b3c4d5e6f7a8b9c1",
"6627f1a2b3c4d5e6f7a8b9c2"
],
"statistics": ["delivery_rate", "click_rate", "conversion_value", "roas"],
"timeframe": "last_90_days",
"conversion_scope": "sales"
}The number of entities you can compare in one call scales with your plan.
Freshness
Every response carries a freshness block so you can render "data as of …" and decide whether to wait for a fresher value:
| Field | Meaning |
|---|---|
computed_at | When the numbers were produced (for a cached value, the time it was written). |
source | Where the numbers came from: cache, live (freshly computed), or entity_stats (a campaign's or automation's stored statistics). |
stale | true when the value is older than the family expects. |
ttl_seconds_remaining | Seconds until a cached value expires; null for non-cached sources. |
Warnings
The warnings array carries benign metadata only — never a swallowed failure. Analytics is fail-closed: any real problem aborts the whole call with an error (below). A warning means the report succeeded but something about your request was adjusted or is worth knowing. Each warning has a stable code and a human-readable message you can show to an end user.
| Code | What it tells you |
|---|---|
bounds_clamped | Your window exceeded the plan maximum and was clamped. |
interval_coarsened | The series interval was widened to keep the bucket count in range; the effective interval is in the response. |
timeframe_snapped_to_hour | Your window was snapped to whole-hour boundaries. |
timeframe_clamped_to_entity_lifetime | A per-entity window was trimmed to the entity's lifetime. |
conversion_scope_unconfigured | The requested scope is not part of the project's conversion configuration, so that scope's statistics are null. By default both sales and profit are configured, so this only fires for a custom configuration that omits the scope. A configured scope with no value (for example profit with no margin data) returns 0 and does not raise this. |
retention_truncated / partial_retention_window | Part of the requested window falls outside available history. |
cache_drift | A cached total and a freshly computed total differ slightly; the figures are being reconciled. |
series_hll_not_summable | A distinct-contact statistic in a series is an estimate per bucket and should not be summed across buckets to get a period total. |
Errors
Analytics is fail-closed: on any real failure you get an error envelope and no partial results. The envelope is verbose enough to explain the cause and how to fix the request, and never leaks internals.
{
"code": "unknown_statistic_for_family",
"message": "\"opens\" is not a valid statistic for a transactional report.",
"hint": "Transactional sends have no open event. Remove \"opens\" or use a campaign / automation report."
}| HTTP | Code | Cause |
|---|---|---|
| 400 | unknown_statistic_for_family | A statistic not supported by this family. |
| 400 | histogram_bucket_limit_exceeded | The series would produce too many buckets even at the coarsest interval. |
| 400 | bound_exceeded | A request bound was breached. |
| 400 | invalid_currency / invalid_timezone | A malformed currency code or timezone. |
| 400 | too_many_group_by_dimensions | More than one group_by dimension — group_by is single-dimension only. |
| 400 | dimension_not_yet_available | A breakdown dimension is reserved and not yet selectable. |
| 400 | compare_to_not_supported | previous_period requested for a standard campaign. |
| 401 | — | Missing or invalid token. |
| 403 | forbidden_resource | The token cannot read this resource, or the project is blocked. |
| 404 | entity_not_found | The campaign, automation or flow does not exist in this project. |
| 429 | rate_limit_exceeded | The endpoint limit for your plan was reached; the envelope includes a retry_after. |
| 500 | analytics_timeout | The query exceeded its time budget. Narrow the window or reduce the compare set and retry. |