---
title: "Fetch Workspace Stats"
method: GET
path: "/workspaces/{workspace_id}/stats"
tags: ["Workspace"]
---

# Fetch Workspace Stats

`GET /workspaces/{workspace_id}/stats`

## Request

- Base URL: `https://myworkspace.myclickfunnels.com/api/v2`
- URL: `https://myworkspace.myclickfunnels.com/api/v2/workspaces/{workspace_id}/stats`
- Auth: one of:
  - HTTP bearer
  - OAuth 2
    - Authorization code: `https://accounts.myclickfunnels.com/oauth/authorize`
    - Token URL: `https://accounts.myclickfunnels.com/oauth/token`
- TypeScript: <https://skmtc.dev/myclickfunnels/apis/clickfunnels-api/docs/workspaces/:workspace_id/stats/get.ts> (a function with typed inputs; its response is checked against the schema)

Aggregate statistics over a **filtered set of records, without listing the records**. Use this for any
"how many", "what's the breakdown", or "what's the total" question: it is dramatically cheaper than paging
a collection and counting client-side.

One workspace endpoint serves every analyzable resource. Name the resource and request the stats you want
as query parameters:

```http
GET /api/v2/workspaces/{workspace_id}/stats?resource=contact&count=true&count_by[property]=is_active&filter[tag_ids]=42
```

A flag of `true` computes the stat; an object-style query supplies a stat's parameters. Omit a stat (or pass
`false`) to skip it. Calling with only `resource` returns a 422 that lists the stats and `property` values the
named resource supports, so the endpoint is self-describing.

### Narrowing the set

The same selectors the resource's own index accepts, composed with AND:

- `filter` - simple field matches, e.g. `filter[tag_ids]=42` for contacts
- `stable_id` - a one-off Refine filter token, e.g. from `POST /workspaces/{workspace_id}/contacts/filters`
- `stored_filter_id` - a saved Refine filter id

Omit all three to report on every record of that resource in the workspace.

Beyond the index's own keys, `filter` takes a `created_at` window on every resource
(`created_at_after` inclusive, `created_at_before` exclusive, ISO 8601 only, with an offset's `+`
sent as `%2B`), and on `order` the two attribution keys a sales dashboard needs: `funnel_id`
(orders attributed to that funnel) and `product_id` (orders containing that product, from any
channel). Every id filter (`id`, `contact_id`, `page_id`, `tag_ids`, `funnel_id`, `product_id`)
takes a numeric `id` or a `public_id`, comma-separated for several; an id that is not this
workspace's is a 422 naming it, never a silent zero:

```http
GET /api/v2/workspaces/{workspace_id}/stats?resource=order&count=true&sum[property]=total_collected&filter[funnel_id]=41&filter[created_at_after]=2026-10-01T00:00:00Z
```

### Large sets

Every stat is computed over the whole matching set, bounded in time rather than in rows: each query has a
statement timeout, and the request has an overall time budget. A stat that cannot finish in time is not
returned; it is named in `unavailable`, with `unavailable_reason` saying whether to narrow the filter or to
ask for it on its own, rather than computed over part of the set.

`count` has one fallback of its own. Past 100,000 matches, if counting the whole set cannot finish in time,
it returns 100,000 with `count_capped_at` beside it - the real total is higher, so narrow the filter.

When no requested stat can be computed in time, the response is a 422, not a 503: retrying it unchanged
fails the same way, so narrow it instead.

### Access

Computing a statistic persists nothing, so a token with **read-only** access may call it. Authorization
follows the category of the resource being analyzed: `contact` requires read access to Contacts, `order`
requires read access to Store, `form_submission` requires read access to Forms.

### Form submissions per page

`form_submission` reports on the same rows `GET /workspaces/{workspace_id}/form_submissions` lists: one per
form post on a ClickFunnels page. `count_by[property]=page_id` returns the submission count of every page in
one call, so counting a funnel's submissions no longer means paging the whole log:

```http
GET /api/v2/workspaces/{workspace_id}/stats?resource=form_submission&count_by[property]=page_id&filter[page_id]=41,42
```

These are form posts, not opt-ins: checkout submits and repeat posts by the same contact count too. For
opt-ins as the funnel dashboard counts them, read `optins` from `GET /funnels/{funnel_id}/stats` or
`GET /pages/{page_id}/stats`.

It has no Refine filter class, so `stable_id` and `stored_filter_id` are refused with a 422 for it.

## Path parameters

- `workspace_id` string, required

## Query parameters

- `resource` 'contact' | 'order' | 'form_submission', required
- `count` boolean
- `count_by` object
  - `property` string, required: Allow-listed per resource - `contact`: `is_active`, `anonymous`, `email_suppression_reason`, `time_zone`; `order`: `billing_status`, `service_status`, `order_type`, `live_mode`; `form_submission`: `page_id`. A 422 lists them.
- `created_at_range` boolean
- `sum` object
  - `property` string, required: Allow-listed per resource - `order`: `total_amount`, `tax_amount`, `total_collected`. A 422 lists them.
- `filter` object
- `stable_id` string
- `stored_filter_id` string

## Response `200`

OK

- object
  - `resource` string: The resource reported on.
  - `workspace_id` integer
  - `stats` object: One key per requested stat.
    - `count` integer: Matching records. Exact unless `count_capped_at` is present, in which case it equals that value and the true total is higher.
    - `count_capped_at` integer: Present only when more than this many records match AND counting the whole set could not finish in time. `count` is this value, and the real total is higher - narrow the filter. Absent means `count` is exact.
    - `count_by` object
      - `property` string
      - `values` object: Count per distinct value. A NULL value is keyed as the string `null`.
      - `capped` boolean: Always `false`; kept for compatibility. The breakdown always covers the whole set, and one that cannot finish in time is listed in `unavailable` instead.
    - `created_at_range` object
      - `min` string, date-time, nullable: Earliest `created_at` in the matching set, or `null` when the set is empty.
      - `max` string, date-time, nullable: Latest `created_at` in the matching set, or `null` when the set is empty.
    - `sum` object
      - `property` string
      - `value` string, nullable: Decimal total serialized as a string to preserve precision. Money renders at the currency's minor units, the same scale the resource's own record serializer uses (`"100.00"`, not `"100.0"`). `null` means the matching set was EMPTY - a non-empty set whose rows are all NULL in that column totals zero, matching what those records serialize as, so `null` and a zero total are never ambiguous.
  - `unavailable` string[]: Requested stats that were NOT computed because they could not finish in time. Present only when something was skipped; the other requested stats are still returned.
  - `unavailable_reason` string: Why the stats in `unavailable` were skipped, and what to do - narrow the filter when a query took too long, or request the stat on its own when the request's time budget ran out first.

## Other responses

- `400`: Bad Request — the required `resource` query parameter is missing
- `401`: Unauthorized
- `403`: Forbidden — the token's scopes don't grant read access to the analyzed resource's category. Not retryable.
- `404`: Not Found — workspace not found
- `422`: Unprocessable Entity — an unknown `resource`, an unknown or unsupported stat, a missing/disallowed `property`, an unknown `filter` key, an id filter value that does not identify a record in this workspace, a `created_at_after`/`created_at_before` that is not ISO 8601, a `stable_id`/`stored_filter_id` on a resource without a Refine filter (`form_submission`), or no stats requested. The message enumerates the valid values, so an empty request is a cheap way to discover a resource's capabilities. Also returned when the stats took longer than the statement timeout to compute: the set is too large, so retrying unchanged fails the same way - narrow it with `filter[...]` (for example a `created_at` window) or request fewer stats.
- `429`: Too Many Requests — two filtered reads are already active for this token and workspace
- `503`: Service Unavailable — retryable. Two conditions land here: * **Transient dependency** — a database connection, the model behind filter generation, or another dependency was briefly unavailable. * **Request budget exhausted** — the request's own wall clock ran out before the filter could be given a fair slice, so no records were read. The time went elsewhere in the same request: a slow generation call, or earlier pages of the same projection. The second case is the OPPOSITE of the 422 that says the filter "took longer than 10s to evaluate". That 422 is a statement timeout and is deterministic — the filter selects more than can be evaluated, and an unchanged retry fails identically. This 503 makes no such claim about the filter, so retrying is worth doing; if it recurs, narrow the filter or ask for fewer records per call.

## Changes

- **2026-11-03** `511d88b30aea`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/myclickfunnels/apis/clickfunnels-api/changes/workspaces/:workspace_id/stats/get.md)

---

[API](https://skmtc.dev/myclickfunnels/apis/clickfunnels-api.md) - [All operations](https://skmtc.dev/myclickfunnels/apis/clickfunnels-api/llms.txt) - [OpenAPI document](https://skmtc.dev/myclickfunnels/apis/clickfunnels-api/revisions/511d88b30aea?raw)
