# Building queries

Shape an Analytics request: choose a timeframe (predefined or custom), set the timezone and output currency, break results down with group_by, narrow them with bounded filters, and attach a previous-period comparison.

**Language:** en
**Audience:** developer
**Search keywords:** timeframe, interval, timezone, currency, group_by, group by, breakdown, breakdowns, filter, filters, filter channel, filter country, channel, country, language, gender, segment, communication_type, communication type, sender, country code, ISO 3166, alpha-2, uppercase, case-insensitive, unknown country, no other bucket, other bucket, overflow, top 20, top buckets, account overview, campaign report, automation report, transactional rollup, support matrix, supported group_by, supported filter, per report, cross-cut, cross cut, countries within a channel, channels within a country, single dimension, one dimension, compare, previous period, query parameters, flow report, filter version, flow version
**Related pages:** /developers/product-api/analytics/statistics, /developers/product-api/analytics/reading-results
**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/building-queries/ (HTML) · https://docs.instasent.com/developers/product-api/analytics/building-queries.md (Markdown)

Every Analytics report shares the same request grammar. Beyond the required `statistics` list ([Statistics catalog](/developers/product-api/analytics/statistics)), a handful of optional parameters control the window, the units, the breakdown and the comparison. They behave identically across families, so once you know them you can read any report.

## Timeframe

Pick a window in one of two ways. Use a **predefined key** for the common cases, or **`start` + `end`** for an arbitrary range.

### Predefined keys

Pass `timeframe` with one of:

`today`, `yesterday`, `this_week`, `last_week`, `last_7_days`, `this_month`, `last_month`, `last_30_days`, `last_90_days`, `last_3_months`, `last_12_months`, `this_year`, `last_year`.

If you omit `timeframe`, each family applies a sensible default that matches its fastest path (the account overview defaults to the last 3 months; campaigns to their lifetime; automations, flows and the transactional rollup to the last 30 days).

#### url

```text
GET /project/{project}/analytics/overview
  ?statistics=delivered,click_rate
  &timeframe=last_30_days
```

### Custom range

Provide both `start` and `end` as ISO 8601 timestamps. When both are present they define the window and override `timeframe`.

#### url

```text
GET /project/{project}/analytics/overview
  ?statistics=delivered,click_rate
  &start=2026-05-01T00:00:00+02:00
  &end=2026-05-30T23:59:59+02:00
```

Windows are snapped to whole-hour boundaries in the resolved timezone (start snapped down, end snapped up). When snapping changes the bounds you asked for, the response carries a warning and echoes the actual window in its `timeframe` block.

> **Note**: The maximum span depends on your plan. Over-long requests are **clamped** to the allowed maximum and flagged with a warning rather than rejected, so you always get a report back.

## Interval (series only)

Series reports bucket the window over time. Set `interval` to one of `hour`, `day`, `week`, `month`. The default is `day` regardless of how long the window is.

If your window and interval would produce too many buckets, the server automatically coarsens to the next grain and tells you via a warning; the report always echoes the **effective** interval it used. If even the coarsest grain does not fit, the request is rejected with a clear error asking you to narrow the window.

## Timezone

Set `timezone` to any IANA zone (for example `Europe/Madrid`). It governs day, week and month boundaries and bucketing. The default is your **project's timezone** — never UTC and never the caller's local time. The resolved timezone is echoed in every response.

## Currency

Set `currency` to an ISO 4217 code to receive every monetary statistic in that currency. The response echoes the output `currency` and the `source_currency` the data was stored in. Dimensionless statistics (`roas`, `epm`) are never converted.

## Breakdowns with `group_by`

`group_by` splits a report into buckets along **exactly one dimension**. It is strictly single-dimension: multi-dimensional, nested or two-dimensional `group_by` is **not supported and never will be**. A request that passes more than one dimension is rejected with `too_many_group_by_dimensions`. There is no nested keying, no second dimension stacked under the first, and no per-outer roll-up — pass a single dimension.

The breakdown vocabulary is `channel`, `country`, `language`, `gender`, `segment` and `communication_type`. **Which of these a given report accepts varies by report** — see the [support matrix](#which-report-supports-what) below.

| Dimension            | Cardinality                               |
| -------------------- | ----------------------------------------- |
| `channel`            | Closed enum — every bucket returned       |
| `communication_type` | Closed enum — every bucket returned       |
| `gender`             | Closed enum — every bucket returned       |
| `country`            | Open — top 20 buckets, no overflow bucket |
| `language`           | Open — top 20 buckets, no overflow bucket |
| `segment`            | Open — top 20 buckets, no overflow bucket |

Closed-enum dimensions return **every** bucket. Open-cardinality dimensions return the **top 20** buckets ordered by your first requested statistic — there is **no synthetic `other` bucket**: the tail beyond the top 20 is simply not returned, never rolled up into an overflow bucket.

When you group by `country`, the bucket keys are the uppercase ISO 3166-1 alpha-2 code (`ES`, `FR`), plus an `unknown` bucket for recipients whose country could not be resolved.

To look at one dimension within another (countries within RCS, channels within Spain), don't stack two dimensions — pair a single `group_by` with a `filter`. See [Cross-cutting two dimensions](#cross-cutting-two-dimensions).

When you group, the response switches to the grouped shape: a values report keys `statistics` by group, a series report keys `series` by group, and both add a `totals_per_group` roll-up plus a `group_order` ranking — see [Grouped values](/developers/product-api/analytics/reading-results#grouped-values) and [Grouped series](/developers/product-api/analytics/reading-results#grouped-series).

#### url

```text
GET /project/{project}/analytics/overview/series
  ?statistics=delivered,clicks_first
  &timeframe=last_30_days
  &group_by=channel
```

## Filters

`filter` narrows a report to a subset before aggregating. There is **no raw query language** — only a closed set of keys, each validated against a fixed enum. Use bracket syntax:

```text
?filter[channel]=sms&filter[country]=ES
```

**The two general filter keys are `channel` and `country`.** Every other breakdown dimension — `language`, `gender`, `segment`, `communication_type` and `sender` — is **`group_by`-only and is never a filter**; passing it as a filter key returns a `400`, as does any value outside the closed enum. One report adds a third key of its own: the flow report accepts `version` (see [Flow reports](/developers/product-api/analytics/flows#totals-versus-one-version)), which no other family does and which is never a `group_by` dimension.

Which filters a given report honors **varies by report** — see the [support matrix](#which-report-supports-what). A report rejects any filter key it does not support.

Country filter values are the ISO 3166-1 alpha-2 code and are **case-insensitive** on input — `filter[country]=ES` and `filter[country]=es` are equivalent. Group-by `country` bucket *keys*, by contrast, are always returned uppercase (`ES`), with an `unknown` bucket for unresolved recipients.

### Cross-cutting two dimensions

Because `group_by` is single-dimension, you look at one dimension *within* another by **pairing a single `group_by` with a `filter` on the other dimension** — not by stacking two `group_by` values. This works **on any report that supports both that `group_by` and that `filter`** (see the [support matrix](#which-report-supports-what) — for example, the campaign and automation reports honor both `channel` and `country`):

| Goal                  | Request                                    |
| --------------------- | ------------------------------------------ |
| Countries within RCS  | `filter[channel]=rcs` + `group_by=country` |
| Channels within Spain | `filter[country]=ES` + `group_by=channel`  |
| RCS totals only       | `filter[channel]=rcs` (no `group_by`)      |
| Spain totals only     | `filter[country]=ES` (no `group_by`)       |

#### url

```text
GET /project/{project}/analytics/campaign/{campaign}
  ?statistics=delivered,clicks_first
  &filter[channel]=rcs
  &group_by=country
```

Filtering and grouping on the **same** dimension is redundant and rejected (`filter[channel]=rcs` + `group_by=channel`).

## Which report supports what

`group_by` and `filter` are expressed identically everywhere, but **each report accepts its own subset** — and each report rejects any dimension it does not support. This matrix is the authoritative reference; a report's own reference page repeats its row.

| Report                                          | `group_by`                                                                  | `filter`                        |
| ----------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------- |
| Account overview — values & series              | `channel`, `country`, `language`, `gender`, `segment`, `communication_type` | `country`                       |
| Campaign report — values                        | `channel`, `country`                                                        | `channel`, `country`            |
| Campaign report — series                        | `channel`, `country`                                                        | `channel`, `country`            |
| Automation report — values                      | `channel`, `country`                                                        | `channel`, `country`            |
| Automation report — series                      | `channel`, `country`                                                        | `channel`, `country`            |
| Flow report — series                            | `channel`, `country`                                                        | `channel`, `country`, `version` |
| Direct / transactional rollup — values & series | `channel`, `country`                                                        | `channel`, `country`            |

A few consequences worth calling out:

- The **account overview** is the only report that accepts the full breakdown catalog, but it filters by `country` only.
- The **campaign, automation and transactional rollup** reports all behave the same on both their **values and series**: they break down and filter by `channel` and `country`, so the cross-cut works on every one of them (e.g. `filter[country]=ES` + `group_by=channel`, or `filter[channel]=rcs` + `group_by=country`).
- The **flow report** has no values shape — it is a series only — and it is the one report with a third filter key, `version`. It combines with the others, so `filter[version]=…` + `group_by=channel` gives you one version's per-channel series.
- The transactional rollup's `group_by=channel` series is the per-leg series of a fallback chain.

## Comparison

On **values** reports, set `compare_to=previous_period` to attach a `comparison` block: the natural "before" window, the same statistics, and the per-statistic change (absolute and percentage).

- Accepted for the account overview, automations and the transactional rollup.
- **Rejected for standard campaigns** — a campaign is a one-shot send with no previous period. To compare campaigns, use the campaign **compare** endpoint instead.
- **Not applicable to flows** — the flow family is a series report only, and comparison is a values-report feature. To weigh one flow version against another, scope two series calls with `filter[version]` — see [Comparing two versions](/developers/product-api/analytics/flows#comparing-two-versions).

```text
?compare_to=previous_period
```

See [Reading results](/developers/product-api/analytics/reading-results#comparison) for the comparison shape, and the [campaign / automation compare reports](/developers/product-api/analytics/reading-results#compare-reports) for ranking several entities at once.

## Next

- [Reading results](/developers/product-api/analytics/reading-results) - Response shapes, freshness, warnings and errors.
- [Conversions & revenue](/developers/product-api/analytics/conversions) - How revenue statistics are scoped per project.

---

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.
