# 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.

**Language:** en
**Audience:** developer
**Search keywords:** response, values, series, date_times, grouped, grouped values, group_by, totals_per_group, group_order, single dimension, cross-cut, compare, freshness, warnings, errors, comparison
**Related pages:** /developers/product-api/analytics/building-queries, /developers/product-api/analytics/statistics
**Docs index (every page):** https://docs.instasent.com/llms.txt
**This zone's index:** https://docs.instasent.com/developers/product-api/llms-full.txt
**This page:** https://docs.instasent.com/developers/product-api/analytics/reading-results/ (HTML) · https://docs.instasent.com/developers/product-api/analytics/reading-results.md (Markdown)

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.

> **Note**: Like every Product API endpoint, Analytics wraps its payload in a top-level `entity` key. The report itself — the `timeframe`, `statistics`, `series`, `comparison`, `freshness` and so on — always lives under `entity`. The examples below show the full response, `entity` wrapper included; when this page refers to a field such as `statistics` or `freshness`, it means `entity.statistics`, `entity.freshness`, and so on.

## 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`.

```json
{
  "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.

```json
{
  "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](/developers/product-api/analytics/building-queries#cross-cutting-two-dimensions). For which `group_by` each report accepts, see the [support matrix](/developers/product-api/analytics/building-queries#which-report-supports-what).

## 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`.

```json
{
  "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.

```json
{
  "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](/developers/product-api/analytics/building-queries#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](/developers/product-api/analytics/building-queries#which-report-supports-what).

## 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`.

```json
"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.

```json
{
  "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):

#### json

```json
{
  "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.

```json
{
  "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. |

> **Warning**: Because the API is fail-closed, you never have to guess whether a `0` means "no activity" or "something broke". Zeros are real zeros; failures are errors. Only `null` carries a special meaning: a revenue scope the project's conversion configuration does not include — flagged by the `conversion_scope_unconfigured` warning. A configured scope with no conversions or no value (such as `profit` with no margin data) returns `0`, not `null`.

## Next

- [Statistics catalog](/developers/product-api/analytics/statistics) - Every statistic and which family exposes it.
- [Building queries](/developers/product-api/analytics/building-queries) - Timeframes, breakdowns, filters and comparisons.

---

This is one page of the Instasent documentation. For the complete machine-readable index of every guide and API reference, fetch https://docs.instasent.com/llms.txt — start there for full context.
