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

# Query subscriber statistics

`POST /subscriber-stats`

## Request

- Base URL: `https://api.pscrpt.com/v1`
- URL: `https://api.pscrpt.com/v1/subscriber-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`

Aggregate list growth for SMS and email: subscribe and unsubscribe events per bucket, net change, the active subscriber count at each bucket's end, and optionally the acquisition method (and raw consent source) behind the subscribes. The same readers as the dashboard's List Growth page. Buckets are UTC days, Monday weeks or calendar months; `timezone` is always `UTC`. Without `channel` in `group_by`, two channels are read as one combined list (`channel: all`). A query is not a mutation and takes no `Idempotency-Key`.

## Headers

- `Postscript-Version` string, date

## Request body

- SubscriberStatsQuery: A query, not a mutation — no `Idempotency-Key`. Every field is optional; `{}` returns the last 30 days by day for SMS and email combined.
  - `channels` SubscriberStatsChannel[]: Default — both.
  - `group_by` SubscriberStatsDimension[]
  - `interval` 'day' | 'week' | 'month': UTC calendar buckets; weeks start Monday. The first bucket may open before `timeframe.gte`.
  - `timeframe` object: Default when omitted — the last 30 days. Max span 366 days (`timeframe_too_large`). Bounds are snapped outward to whole UTC days and the served window is echoed in `data.timeframe`.
    - `gte` string, date-time, required
    - `lte` string, date-time, required

## Response `200`

Per-bucket subscriber flows and active counts, optional source breakdown, and window totals.

- SubscriberStatsEnvelope
  - `data` SubscriberStats, required: Snapshot result (`meta.as_of` — the earliest watermark of the readers served).
    - `channels` SubscriberStatsChannel[], required
    - `group_by` SubscriberStatsDimension[], required
    - `interval` 'day' | 'week' | 'month', required
    - `object` 'subscriber_stats', required
    - `series` SubscriberStatsSeriesRow[], required
      - `active_end_of_period` integer, nullable, required: Subscribed profiles at the bucket's end, from the daily subscriber snapshot (`subscriber_stock.observation`): the last snapshot on or before the bucket's last day. Sampled, never summed; it does not reconcile exactly with the running sum of `net_change`. Null before snapshots exist.
      - `bucket_start` string, date-time, required
      - `channel` 'sms' | 'email' | 'all', required: `all` is both channels read as one list: a profile subscribed on SMS and email counts once in `active_*`, while `new_subscribers`/`unsubscribes` count each channel's event.
      - `is_partial` boolean, required: `active_end_of_period` comes from a UTC day that has not closed and will change.
      - `net_change` integer, nullable, required: `new_subscribers - unsubscribes` (`subscribers.net_opt_events`).
      - `new_subscribers` integer, nullable, required: Subscribe events in the bucket (catalog `subscribers.opt_ins`). Events, not people: a profile that unsubscribes and resubscribes counts twice. Null when the channel scope has no subscribe/unsubscribe events on record in the whole window — not measured, rather than zero.
      - `unsubscribes` integer, nullable, required: Unsubscribe events in the bucket (`subscribers.opt_outs`), a positive count. Null as for `new_subscribers`.
    - `sources` SubscriberStatsSourceRow[], required: Empty unless `group_by` has `source` or `source_detail`.
      - `bucket_start` string, date-time, required
      - `channel` 'sms' | 'email' | 'all', required: `all` is both channels read as one list: a profile subscribed on SMS and email counts once in `active_*`, while `new_subscribers`/`unsubscribes` count each channel's event.
      - `method` string, required: Governed acquisition method. Unclassified sources land in `other`.
      - `method_detail` string, nullable: Present only with `source_detail`: the raw consent source recorded on the subscribe event (e.g. `Keyword - JOIN`, `klaviyo_sync`); null when none was recorded.
      - `method_label` string, required: Display name for `method`.
      - `new_subscribers` integer, required
      - `unsubscribes` integer, nullable, required: Unsubscribes attributed to the method of the profile's most recent prior subscribe (not the unsubscribe mechanism). Null on `source_detail` rows.
    - `timeframe` object, required
      - `gte` string, date-time, required
      - `lte` string, date-time, required
    - `timezone` 'UTC', required: Always `UTC`: the subscriber readers bucket by UTC day.
    - `timezone_reason` 'unresolved', required: `unresolved` — no property-timezone reader exists for subscriber statistics yet, so UTC days are served.
    - `totals` SubscriberStatsTotals[], required: One row per channel scope, computed over the whole window.
      - `active_end` integer, nullable, required: Subscribed profiles at the window's last day.
      - `active_start` integer, nullable, required: Subscribed profiles at the latest snapshot on or before `timeframe.gte`.
      - `channel` 'sms' | 'email' | 'all', required: `all` is both channels read as one list: a profile subscribed on SMS and email counts once in `active_*`, while `new_subscribers`/`unsubscribes` count each channel's event.
      - `coverage` object, required
        - `active_from` string, date, nullable, required: First day of subscriber snapshots; `active_*` is null before it.
        - `includes_partial_period` boolean, required: The window reaches a UTC day that has not closed.
        - `new_subscribers_from` string, date, nullable, required: First UTC day with subscribe/unsubscribe events in the window; null when none.
        - `source_complete_from` string, date, nullable, required: Source identity is complete from this day; earlier SMS subscribes may fall in `other`.
      - `net_change` integer, nullable, required
      - `new_subscribers` integer, nullable, required
      - `unsubscribes` integer, nullable, required
    - `truncated` boolean, required: More than 2000 `sources` rows matched; `sources` holds the first 2000.
  - `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-02** `c0c9a9f0ac5c`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/pscrpt/apis/postscript-native-api-beta/changes/subscriber-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)
