---
title: "Fetch Workflow Step Stats"
method: GET
path: "/workflows/steps/{step_id}/stats"
tags: ["Workflows::Step"]
---

# Fetch Workflow Step Stats

`GET /workflows/steps/{step_id}/stats`

## Request

- Base URL: `https://myworkspace.myclickfunnels.com/api/v2`
- URL: `https://myworkspace.myclickfunnels.com/api/v2/workflows/steps/{step_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/workflows/steps/:step_id/stats/get.ts> (a function with typed inputs; its response is checked against the schema)

Stats for one workflow step — the numbers the workflow builder shows under it. Every step
type reports `contacts`: how many are at the step right now and how many left it inside
the window. A `send_email_step` also reports `email`: engagement and attributed revenue
for the email it sends. For every other step type `email` is `null`, so a delay or tag
step never reads as an email that performed badly.

`contacts.are_here` is current occupancy — runs whose current step is this one — and is
the one number that does not follow `timerange`. `contacts.were_here` counts contacts who
left the step inside the window, once per pass through it.

Unlike a broadcast, a workflow step has no audience and no send window: it keeps
sending for as long as the workflow runs and contacts keep reaching it. So there is
nothing to report as queued or in progress, and the numbers are always for a window
you choose. Defaults to a 30-day window ending now. Override with `timerange_start`
and `timerange_end`. Values are clamped: a future end date is clamped to now, a window
over 90 days is clamped to 90 days, and a start after the end resets to 30 days ending
at `timerange_end`. Read `timerange` back from the response for the window actually used.

`sends` counts a contact once per pass through the step, so a looping workflow makes it
larger than `recipients`. Rates are whole percentages rounded to the nearest integer — a
share under 0.5% reads 0 next to a non-zero count, and the counts are authoritative.
`open_rate`, `click_rate`, `bounce_rate` and `unsubscribe_rate` divide by `recipients`;
`click_to_open_rate` divides unique clicks by unique opens. The workflow builder's
percentages under a step divide by `sends` instead, so for a step that sent to a contact
more than once the app shows a lower rate than this endpoint does for the same window;
the counts are the same on both. Numerator and denominator are
each counted inside the window on their own: an open inside the window counts even when
the send it answers happened before the window. So a rate can exceed 100 when few new
sends meet many opens of older emails, and it is 0 whenever its denominator is 0, however
many opens there were. A step whose email last sent before the window reports zero
`sends` and `recipients` while opens and clicks inside it still count. To compare like
with like, widen the window to cover the sends you care about.

Revenue is attributed when the buyer's last tracked source within 30 days was a click on
one of the email's links. Each attributed order contributes its **first** paid invoice,
and only when that invoice was issued inside the window: renewals never count, and an
order whose first payment predates the window contributes nothing even if a renewal was
paid inside it. Only live-mode orders paid in the workspace currency are counted. Note
the engagement counts do include test-mode sends, so the revenue-per-X figures divide
live-only money by counts that may include test traffic.

The broadcast equivalent is `GET /emails/broadcasts/{broadcast_id}/stats`. Its
`performance` and `revenue` blocks share this vocabulary at the top level, with
`delivered` in place of `sends`/`recipients`/`failed`, and it takes no timerange — a
broadcast reports on its own send window.

## Path parameters

- `step_id` string, required

## Query parameters

- `timerange_start` string, date-time
- `timerange_end` string, date-time

## Response `200`

OK

- WorkflowsStepStatsAttributes: Stats for one workflow step over the requested timerange. Every step type reports `contacts`; only a `send_email_step` reports `email`, which is `null` for every other step type. Counts are unique contacts unless noted; rates are whole percentages; money is a decimal string in `currency`.
  - `step` object: The workflow step these stats belong to
    - `id` integer: Step ID
    - `public_id` string, nullable: Step public ID
    - `name` string, nullable: Step name
    - `step_type` string: The step's type, e.g. `send_email_step` or `delay_step`. `email` is `null` unless this is `send_email_step`.
  - `workflow` object: The workflow the step belongs to
    - `id` integer: Workflow ID
    - `public_id` string, nullable: Workflow public ID
    - `name` string, nullable: Workflow name
  - `currency` string, nullable: ISO 4217 currency code for all monetary values (e.g. USD, EUR). `email.revenue` counts only orders paid in this currency. `null` when the workspace has no currency set, in which case there is nothing to filter on and revenue sums every currency its orders were paid in.
  - `timerange` object: The window the stats cover, after clamping. Read it back rather than assuming the window you asked for was used. Every number follows it except `contacts.are_here`, which is a live count.
    - `from` string, date-time: Start of the window
    - `to` string, date-time: End of the window
  - `contacts` object: Where contacts stand in relation to this step. Every step type reports these; they are the two numbers the workflow builder shows under each step.
    - `are_here` integer: Contacts at this step right now — runs whose current step is this one. A live count; it does not follow `timerange`.
    - `were_here` integer: Contacts who left this step inside the window, counting a contact once per pass through it.
  - `email` object, nullable: Engagement and attributed revenue for the email this step sends. `null` for every step type except `send_email_step` — a step that sends nothing has nothing to report, which is not the same as performing badly.
    - `performance` object: Engagement inside the window. Rates are rounded to whole percentages, so a share under 0.5% reads 0 next to a non-zero count; the counts are authoritative. Numerator and denominator are each counted inside the window on their own, so a rate is not a share of one fixed group of contacts — see `open_rate`. These counts include test-mode sends, while `revenue` counts live-mode orders only, so the revenue-per-X figures divide live-only money by counts that may include test traffic.
      - `sends` integer: Emails this step sent inside the window, counting a contact once per pass through the step. A workflow that loops, or a contact re-entered into it, makes this larger than `recipients`.
      - `recipients` integer: Distinct contacts the step sent to inside the window, including any reached in test mode. Every rate below except `click_to_open_rate` is a share of this, not of `sends`.
      - `failed` integer: Contacts whose send failed, either refused outright or timed out in the queue. A contact that failed both ways counts once.
      - `bounced` integer: Contacts whose address bounced
      - `opened` integer: Total opens inside the window, counting repeat opens by the same contact
      - `unique_opens` integer: Contacts who opened at least once inside the window
      - `clicked` integer: Total clicks inside the window, counting repeat clicks by the same contact
      - `unique_clicks` integer: Contacts who clicked at least once inside the window
      - `unsubscribed` integer: Contacts who unsubscribed from this email
      - `spam_complaints` integer: Contacts who reported this email as spam
      - `open_rate` integer: Unique opens as a whole percentage of `recipients`. Both are counted inside the window on their own, so an open of an email sent before the window counts while its send does not; the figure can exceed 100 when few new sends meet many opens of older emails, and it is 0 whenever `recipients` is 0. Widen the window to cover the sends you care about.
      - `click_rate` integer: Unique clicks as a whole percentage of `recipients`. Counted the same way as `open_rate`, so it can exceed 100 and is 0 when `recipients` is 0.
      - `click_to_open_rate` integer: Unique clicks as a whole percentage of unique opens; 0 when there are no opens.
      - `bounce_rate` integer: Bounces as a whole percentage of `recipients`; 0 when `recipients` is 0.
      - `unsubscribe_rate` integer: Unsubscribes as a whole percentage of `recipients`; 0 when `recipients` is 0.
    - `revenue` object: Orders attributed to this step's email: the buyer's last tracked source within 30 days was a click on one of its links, so a later touch from elsewhere supersedes it. Each order contributes its FIRST paid invoice, and only when that invoice was issued inside the window - renewals never count, and an order whose first payment predates the window contributes nothing even if a renewal was paid inside it. Only live-mode orders paid in the workspace `currency` are counted.
      - `total` string: Revenue attributed to the step, as a decimal string in `currency`
      - `orders_count` integer: Attributed orders whose FIRST paid invoice was issued inside the window
      - `per_recipient` string: Revenue divided by `recipients`, as a decimal string in `currency`
      - `per_unique_open` string: Revenue divided by unique opens, as a decimal string in `currency`
      - `per_unique_click` string: Revenue divided by unique clicks, as a decimal string in `currency`

## Other responses

- `401`: Unauthorized
- `403`: Forbidden — the token's scopes don't grant read access to workflows. Not retryable.
- `404`: Not Found — step not found
- `422`: Unprocessable Entity — a timerange parameter is not a real date or time. Unparseable values and calendar dates that do not exist (2026-02-31) are refused rather than silently answered with a different window.
- `503`: Service Unavailable — a stats query exceeded the database timeout, or a dependency was briefly unavailable. Retryable; narrowing the timerange makes the query cheaper.

## Changes

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

[Operation history](https://skmtc.dev/myclickfunnels/apis/clickfunnels-api/changes/workflows/steps/:step_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)
