---
title: "Query message statistics"
method: POST
path: "/message-stats"
tags: ["metrics"]
---

# Query message statistics

`POST /message-stats`

## Request

- Base URL: `https://api.pscrpt.com/v1`
- URL: `https://api.pscrpt.com/v1/message-stats`
- Auth: one of:
  - HTTP bearer
  - OAuth 2
    - Authorization code: `https://api.pscrpt.com/oauth/authorize`
    - Token URL: `https://api.pscrpt.com/oauth/token` scopes: `metrics:read`

Sends, engagement and attributed revenue for merchant campaigns and flows, by send cohort: a click or order counts toward the hour its message was sent, bucketed in the property's timezone. Shopper sends (campaigns, Autos, Responses) are excluded from every row and total, so `source_type: flow` means merchant automations; a property with more than 1,000 Shopper campaigns and automations answers 503 `service_unavailable` rather than totals that include them. Attribution-dependent statistics come back once per attribution model the property has; `data.attribution_models` flags the default. Billing figures are not served here. A query is not a mutation and takes no `Idempotency-Key`. At most 3 dimensions and 500 rows; past 500, `data.truncated` is true and `rows` holds the first 500 — narrow with `filter` rather than paging. `filter.message_id` requires `message` in `group_by` (400 otherwise) and narrows `rows` only; `totals` cover the other filter clauses. Only these groupings are served in the property's timezone: none, `source_type`, `interval` alone, `campaign` or `flow`, and `interval: day` with `campaign`/`flow` and `message`; every other grouping is served in UTC days and says so in `timezone_reason`.

## Headers

- `Postscript-Version` string, date

## Request body

- MessageStatsQuery: A query, not a mutation — no `Idempotency-Key`. Covers merchant campaigns and flows only: Shopper sends (campaigns, Autos, Responses) are excluded, and a Shopper campaign id in `filter` matches nothing. At most 3 dimensions and 500 rows (`truncated` past that); narrow with `filter`. Every attribution-dependent statistic is returned once per attribution model the property has, and `data.attribution_models` says which one is the property's default.
  - `attribution_models` string[]: Restrict attribution-dependent statistics to these models. Default — every model the property has.
  - `filter` object: All clauses are ANDed; ids not belonging to the property are ignored, never 404.
    - `campaign_id` string[]
    - `channel` string[]
    - `flow_id` string[]
    - `message_id` string[]: Requires `message` in `group_by`. Narrows `rows` only; `totals` ignore it.
    - `source_type` string[]
  - `group_by` MessageStatsDimension[]
  - `interval` 'none' | 'day' | 'week' | 'month': `none` returns one row per dimension combination; otherwise rows also carry `bucket_start` (local day/Monday-week/month start as a UTC instant).
  - `statistics` MessageStatistic[], required
  - `timeframe` object: Default when omitted — the last 30 days. Max span 366 days (`timeframe_too_large`). Bounds are read in `timezone` and snapped outward to whole days; when `timezone` is omitted they snap to their UTC calendar dates, which are then served as local days in the property's timezone. The served window is echoed in `data.timeframe`.
    - `gte` string, date-time, required
    - `lte` string, date-time, required
  - `timezone` string: IANA name. Default — the property's timezone as configured in Shopify. The response echoes the timezone actually served and why (`timezone_reason`).

## Response `200`

Statistics per requested dimension combination, plus independent totals.

- MessageStatsEnvelope
  - `data` MessageStats, required: Snapshot result. `meta.as_of` (mandatory) is `data_as_of` when open days were served, else the version cutoff the read was pinned to, and never later than the request time. Days are served from published rollups. When the window reaches past the last closed day, the open days are served as of `data_as_of`, the rollup's ingest cutoff. Days after `data_through` are not served. In both cases the window's rows and totals are `partial`.
    - `attribution_models` object[], required
      - `is_default` boolean, required
      - `model` string, required
    - `data_as_of` string, date-time, nullable, required: Instant the open days (after the last closed day) are served as of, the rollup's ingest cutoff; null when no open days were served.
    - `data_available_from` string, date, nullable, required: Day rollup coverage starts, in `timezone`; set only when the whole window precedes it, which returns no rows.
    - `data_through` string, date, nullable, required: Last day served, in `timezone`, when the rollup watermark cut the window short; null when the window was served to its end.
    - `group_by` MessageStatsDimension[], required
    - `interval` 'none' | 'day' | 'week' | 'month', required
    - `object` 'message_stats', required
    - `rows` MessageStatsRow[], required
      - `coverage` MessageStatsCoverage, required: Whether every source behind the row's numbers covered the whole window.
        - `data_available_from` string, date-time, nullable: Earliest instant the row's sources cover; before it the numbers are undercounts.
        - `reason` string, nullable: Set when `partial`, e.g. `data_available_from` when the rollups begin inside the window, `data_through` when the window was served only through `data.data_through`, `data_as_of` when the window reaches days after the last closed day, which are served as of `data.data_as_of`, or a metric's own unavailability reason such as `delivery_receipts_not_rolled_up`.
        - `status` 'complete' | 'partial', required
      - `currency` string, nullable: ISO 4217 code of every monetary statistic in the row; null when the row mixes currencies (then monetary values are null).
      - `dimensions` object, required: One key per requested dimension, plus `bucket_start` when `interval` is not `none`.
        - `bucket_start` string, date-time
        - `campaign_id` string, nullable
        - `channel` 'sms' | 'mms' | 'rcs' | 'email'
        - `flow_id` string, nullable
        - `message_id` string, nullable
        - `source_type` 'campaign' | 'flow'
      - `statistics` object, required: Keyed by the requested statistic. Attribution-independent statistics are a number (null when the denominator is zero). Attribution-dependent ones are an object keyed by attribution model, each value a number or null.
    - `timeframe` object, required
      - `gte` string, date-time, required
      - `lte` string, date-time, required
    - `timezone` string, required: IANA name actually used for day boundaries.
    - `timezone_reason` 'property' | 'override' | 'unresolved' | 'non_hour_offset' | 'hourly_disabled' | 'hourly_incomplete', required: `property` — the property's configured timezone; `override` — the request's `timezone`; `unresolved` — the property has none, or the grouping has no timezone-aware rollup, so UTC days were served; `non_hour_offset` — the zone's UTC offset is not a whole hour (India, Newfoundland, Nepal…), so UTC days were served instead; `hourly_disabled` — timezone-aware rollups are off in this environment, UTC days served; `hourly_incomplete` — the timezone-aware rollups do not yet cover the window, UTC days served.
    - `totals` MessageStatsRow, required
      - `coverage` MessageStatsCoverage, required: Whether every source behind the row's numbers covered the whole window.
        - `data_available_from` string, date-time, nullable: Earliest instant the row's sources cover; before it the numbers are undercounts.
        - `reason` string, nullable: Set when `partial`, e.g. `data_available_from` when the rollups begin inside the window, `data_through` when the window was served only through `data.data_through`, `data_as_of` when the window reaches days after the last closed day, which are served as of `data.data_as_of`, or a metric's own unavailability reason such as `delivery_receipts_not_rolled_up`.
        - `status` 'complete' | 'partial', required
      - `currency` string, nullable: ISO 4217 code of every monetary statistic in the row; null when the row mixes currencies (then monetary values are null).
      - `dimensions` object, required: One key per requested dimension, plus `bucket_start` when `interval` is not `none`.
        - `bucket_start` string, date-time
        - `campaign_id` string, nullable
        - `channel` 'sms' | 'mms' | 'rcs' | 'email'
        - `flow_id` string, nullable
        - `message_id` string, nullable
        - `source_type` 'campaign' | 'flow'
      - `statistics` object, required: Keyed by the requested statistic. Attribution-independent statistics are a number (null when the denominator is zero). Attribution-dependent ones are an object keyed by attribution model, each value a number or null.
    - `truncated` boolean, required: More than 500 rows matched; `rows` holds the first 500.
  - `meta` Meta, required
    - `as_of` string, date-time
    - `livemode` boolean, required
    - `page` PageMeta
      - `limit` integer, required
      - `next_cursor` string, nullable, required: Pass as `page[after]`. Null on the last page.
    - `request_id` string, required
    - `resolved_from` string: The merged-away profile ID the request addressed; `data` holds the survivor.
    - `revision` string, date, required

## Other responses

- `400`: Malformed input, an unknown parameter, an unsupported filter, sort, or expand, or a mistyped ID.
- `401`: Missing or invalid API key.
- `403`: The key lacks the required scope.
- `429`: Rate limit exceeded.
- `500`: Internal error.
- `502`: Upstream provider failure.
- `503`: Temporarily unavailable.

## Changes

- **2026-10-28** `f6f425236291`: 3 info
  - added the new `opens` enum value to the request property `statistics/items/`
  - added the new `spam_complaints` enum value to the request property `statistics/items/`
  - added the new `unique_opens` enum value to the request property `statistics/items/`
- **2026-10-15** `18d62041a6aa`: 1 info
  - added the required property `data/data_as_of` to the response with the `200` status
- **2026-10-02** `c0c9a9f0ac5c`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/pscrpt/apis/postscript-native-api-beta/changes/message-stats/post.md)

---

[API](https://skmtc.dev/pscrpt/apis/postscript-native-api-beta.md) - [All operations](https://skmtc.dev/pscrpt/apis/postscript-native-api-beta/llms.txt) - [OpenAPI document](https://skmtc.dev/pscrpt/apis/postscript-native-api-beta/revisions/882458ee1f36?raw)
