---
title: "Run a saved funnel and return per-step conversions"
method: POST
path: "/funnels/{id}/report"
tags: ["Stats"]
---

# Run a saved funnel and return per-step conversions

`POST /funnels/{id}/report`

## Request

- Base URL: `https://statable.com/api/v1`
- URL: `https://statable.com/api/v1/funnels/{id}/report`
- Auth: HTTP bearer
- TypeScript: <https://skmtc.dev/statable/apis/statable-stats-api/docs/funnels/:id/report/post.ts> (a function with typed inputs; its response is checked against the schema)

Runs a saved funnel over a date range (+ optional segment filters) and returns an ORDERED per-step result with top-level scalars — a bespoke, non-tabular shape (funnel output is not rows). `steps` must not be sorted; `conversion_rate` is cumulative versus the first step (`entering`), not step-to-step. The intra-conversion window is a fixed 1 day, independent of `date_range` (which only bounds the sessions scanned).

## Path parameters

- `id` integer, required

## Request body

- FunnelReportRequest
  - `site_id` integer: Optional for a single-site key; required for an all-sites key.
  - `date_range` union, required: A scalar string — a preset (`7d`/`30d` = last N full days; `month` = current calendar month to date; `realtime` = last ~30 min) or a bare `Nd` for the last N days, N in 1–90 (e.g. `14d`) — OR a custom inclusive `[from, to]` pair of `YYYY-MM-DD` dates with `from <= to` and a span of at most 366 days. The scalar forms are exactly what `GET /sites` accepts; for a window over 90 days use a custom pair.
    - string
    - string[]
  - `filters` Filter[]: Segment/session filters only (country, browser, os, device, source, channel, referrer, utm_*, entry_page, exit_page, hostname). Event-level fields (event, page, code) return `invalid_filter`.
    - `field` 'browser' | 'browser_version' | 'os' | 'os_version' | 'device' | 'country' | 'city' | 'region' | 'hostname' | 'utm_source' | 'utm_medium' | 'utm_campaign' | 'utm_content' | 'utm_term' | 'entry_page' | 'exit_page' | 'source' | 'channel' | 'referrer' | 'page' | 'code' | 'event', required: Field name WITHOUT the visit:/event: prefix. Geo fields take codes, not names (country/region = ISO/subdivision codes, city = numeric geoname_id — the values geo breakdowns return). The `event` field filters by a custom event name (an `event:name` breakdown value) and restricts the whole query to that event's sessions; it accepts ONLY operator `is` with a single non-standard value (not `pageview` / `engagement`) — other forms return `invalid_filter`.
    - `operator` 'is' | 'is_not' | 'contains' | 'does_not_contain', required
    - `values` string[], required: OR-ed together. A value may contain commas.

## Response `200`

The funnel result.

- FunnelReportResponse
  - `funnel` object
    - `id` integer
    - `name` string
    - `scope` 'visitor' | 'session'
    - `strict_order` boolean
  - `entering` integer: Visitors that reached step 0 — the funnel's denominator.
  - `all_visitors` integer: Total visitors/visits in the period (funnel-independent).
  - `steps` FunnelReportStep[]
    - `index` integer: 0-based position in the funnel.
    - `name` string
    - `kind` 'page' | 'event' | 'goal' | 'scroll' | 'entry_page' | 'exit_page'
    - `visitors` integer: Visitors/visits that reached this step.
    - `conversion_rate` number: visitors / entering * 100 — cumulative vs the first step, NOT step-to-step.
    - `dropoff` integer: Visitors lost versus the previous step.
  - `query` object: The resolved request (site_id, funnel_id, normalized date_range, filters).

## Other responses

- `400`: Invalid request. Branch on the stable `code`: invalid_request, metrics_required, unknown_metric, too_many_dimensions, unknown_dimension, invalid_date_range, invalid_interval, metric_not_available, invalid_filter, event_filter_required, invalid_compare, compare_length_mismatch, site_id_required, limit_offset_misuse.
- `401`: Missing/malformed/invalid/revoked bearer token (`unauthorized`), or a key past its expiry (`key_expired`; the message names the expiry time).
- `403`: `insufficient_scope` (the key lacks the `read` scope every endpoint here requires), `key_not_scoped` (single-site key asked for a different site), `tracking_inactive` (the site's tracking has stopped) or `plan_expired` (the site owner's plan has expired: tracking may still run through the grace period, but reports are closed until the owner chooses a plan).
- `404`: `unknown_site` — the `site_id` does not exist OR the key's owner has no access. Deliberately indistinguishable (same status AND code) so a key cannot enumerate which site_ids exist. `unknown_funnel` (funnel endpoints only) — the funnel id is not found for the resolved site.
- `429`: Hourly account (default 2000/h) or per-key (default 600/h) limit hit (`rate_limited`). Retry after the `Retry-After` seconds. The X-RateLimit-* headers report the window that tripped (account on an account limit).
- `500`: Unexpected server error (`internal`). Report it with the `request_id` from the body or the X-Request-ID header: it is what lets the exact log entry be found, and without it a 500 can only be matched by guessing at a time window.

---

[API](https://skmtc.dev/statable/apis/statable-stats-api.md) - [All operations](https://skmtc.dev/statable/apis/statable-stats-api/llms.txt) - [OpenAPI document](https://skmtc.dev/statable/apis/statable-stats-api/revisions/352b6d47c186?raw)
