> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formo.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Get Flow

> Session-scoped user-flow transitions for Sankey-style charts.

Returns the same Sankey transitions the dashboard renders on the Flows chart. For each session that fires the start step inside `[dateFrom, dateTo]`, the pipe rebuilds the ordered sequence of subsequent events within the conversion window, normalises them into flow-node labels, and emits one row per `(step, source, target)` transition with counts and per-step percentages.

When `end_step` is set, only sessions that reached the end event within the conversion window are kept (converter-only mode), and the matched event is suffixed with `__END_MATCH__`.

## Defining start and end steps

Both `start_step` and `end_step` are JSON-encoded objects of shape `{type, event, resolved_event, filters?: [...], status_type?, status_value?}`:

* **`type`** - event type (`event`, `track`, `transaction`, `signature`, `decoded_log`).
* **`event`** - the event name.
* **`resolved_event`** - the value matched against the events table. Use the sentinel `"__ALL_PAGE_VIEWS__"` to match any page view, or the page path / event name otherwise.
* **`filters`** - optional `[{operand, operator, value, values?}]`. Use `values` for the `in` / `notIn` operators.

Pass `global_filters` as a JSON-encoded array to apply additional `{operand, operator, value, values?}` constraints to both the start-session scan and the relevant-events scan.

## Conversion window and depth

`window_seconds` (default `1209600`, i.e. 14 days) bounds how long after the start event the path is allowed to extend. `max_steps` (default `4`, clamped to `2..10`) caps the Sankey depth.

## Example

```bash theme={null}
curl -G "https://api.formo.so/v0/flow" \
  -H "Authorization: Bearer $FORMO_API_KEY" \
  --data-urlencode "dateFrom=2026-04-01" \
  --data-urlencode "dateTo=2026-04-30" \
  --data-urlencode "window_seconds=1209600" \
  --data-urlencode "max_steps=4" \
  --data-urlencode 'start_step={"type":"event","event":"page","resolved_event":"__ALL_PAGE_VIEWS__","filters":[]}' \
  --data-urlencode 'end_step={"type":"track","event":"swap","resolved_event":"swap","filters":[]}'
```


## OpenAPI

````yaml GET /v0/flow
openapi: 3.1.0
info:
  title: Formo Public API
  description: >-
    REST API for managing Formo projects, analytics, alerts, boards, charts,
    contracts, segments, and AI chat.


    **Auth.** All endpoints require a workspace API key with the appropriate
    scopes (see `x-api-scopes`).


    **Response shape.** Successful responses return the resource directly (or `{
    data: [...], total, page, size, has_more }` for paginated lists). HTTP
    status carries success/failure; there is no envelope wrapping success
    bodies.


    **Errors.** Every non-2xx response uses the `Error` envelope: `{ error: {
    code, message, doc_url, param?, details? } }`. Branch on the
    machine-readable `code` (see `ErrorCode` enum) and follow `doc_url` to the
    matching section of the [errors
    reference](https://docs.formo.so/api/errors).


    **Idempotency.** Pass an `Idempotency-Key` header on POST/PUT/PATCH/DELETE
    to make retries safe; the response is cached for 24 h and replayed on
    duplicate keys.
  version: 0.1.0
  contact:
    name: Formo
    url: https://formo.so
servers:
  - url: https://api.formo.so
    description: API Server (boards, alerts, contracts, segments, profiles, query, import)
  - url: https://events.formo.so
    description: Events Server (event ingestion)
security:
  - WorkspaceApiKey: []
tags:
  - name: Alerts
    description: Manage project alerts and notifications
  - name: Boards
    description: Manage dashboard boards
  - name: Charts
    description: Manage charts within boards
  - name: Contracts
    description: Manage blockchain contract monitoring
  - name: Segments
    description: Manage user segments
  - name: Profiles
    description: Wallet profiles and import
  - name: Query
    description: >-
      Execute SQL queries and call pre-built analytics endpoints (KPIs, top
      pages, lifecycle, retention, revenue). Requires the query:read scope.
  - name: Events
    description: Event ingestion API (events.formo.so)
paths:
  /v0/flow:
    get:
      tags:
        - Query
      summary: Get session-scoped user-flow transitions (Sankey)
      description: >-
        Session-scoped user-flow transitions for Sankey-style charts. Given a
        required `start_step`, optional `end_step`, date window, conversion
        window, and max-step cap, returns one row per `(step, source, target)`
        transition with counts and per-step percentages.


        **How it works:** for each session that fires the start step inside
        `[date_from, date_to]`, the pipe rebuilds the ordered sequence of
        subsequent events within the conversion window (`window_seconds`),
        normalises each event into a flow-node label (page path for `page`,
        event name for `track`/`decoded_log`, prefixed for
        `signature`/`transaction`), truncates at the first end-step match (when
        `end_step` is set), and slices to `max_steps + 1` nodes. Adjacent nodes
        are then exploded into `(step, source, target)` edges with counts and
        percentages.


        **Converter-only mode:** when `end_step` is set, only sessions that
        actually reached the end event within the window are kept, and the
        matched event is suffixed with `__END_MATCH__`. This mirrors PostHog's
        `pathsFilter.endPoint` behaviour; every visible link is part of a
        converting path.


        **Step shape:** `start_step` and `end_step` are JSON objects of shape
        `{type, event, resolved_event, filters?: [...], status_type?,
        status_value?}`. `resolved_event` is the value used to match against the
        events table (event name, page path, or the sentinel
        `__ALL_PAGE_VIEWS__` for any page view). `filters` and `global_filters`
        accept `{operand, operator, value, values?}` (use `values` for
        `in`/`notIn`).
      operationId: getAnalyticsFlow
      parameters:
        - name: date_from
          in: query
          required: true
          schema:
            type: string
            format: date
          description: >-
            Inclusive ISO date for the start of the start-event window
            (YYYY-MM-DD).
          example: '2026-04-01'
        - name: date_to
          in: query
          required: true
          schema:
            type: string
            format: date
          description: >-
            Inclusive ISO date for the end of the start-event window
            (YYYY-MM-DD).
          example: '2026-04-30'
        - name: start_step
          in: query
          required: true
          schema:
            type: string
          description: >-
            JSON-encoded start-step spec of shape `{type, event, resolved_event,
            filters?: [...], status_type?, status_value?}`. Use
            `"resolved_event":"__ALL_PAGE_VIEWS__"` to match any page view; pass
            a page path for a specific landing page; or pass an event name for
            `track`/`decoded_log` start events. `filters` use `{operand,
            operator, value, values?}` (use `values` for `in`/`notIn`).


            **Filter operators:** `equals`, `notEquals`, `in`, `notIn`, `gt`,
            `lt`, `gte`, `lte`, `startsWith`, `endsWith`, `includes`. Standard
            columns (`device`, `browser`, `os`, `location`, `referrer`, `utm_*`,
            `page_path`, etc.) are read directly; everything else is read from
            `properties` via `JSONExtractString` (or `JSONExtractFloat` for
            numeric comparators).
          examples:
            anyPageView:
              summary: Match any page view as the start step
              value: >-
                {"type":"event","event":"page","resolved_event":"__ALL_PAGE_VIEWS__","filters":[]}
            specificPagePath:
              summary: Match a specific landing page
              value: >-
                {"type":"event","event":"page","resolved_event":"/swap","filters":[]}
            trackEvent:
              summary: Custom track event as the start step
              value: >-
                {"type":"track","event":"signup","resolved_event":"signup","filters":[]}
            withStandardFilter:
              summary: Step filter on a standard column (mobile sessions only)
              value: >-
                {"type":"event","event":"page","resolved_event":"__ALL_PAGE_VIEWS__","filters":[{"operand":"device","operator":"equals","value":"mobile"}]}
            withInValuesFilter:
              summary: Step filter using `in` with the `values` array
              value: >-
                {"type":"event","event":"page","resolved_event":"__ALL_PAGE_VIEWS__","filters":[{"operand":"utm_source","operator":"in","value":"twitter","values":["twitter","farcaster","telegram"]}]}
            withJsonPropertyFilter:
              summary: Step property filter (JSON property `provider_name=MetaMask`)
              value: >-
                {"type":"track","event":"connect","resolved_event":"connect","filters":[{"operand":"provider_name","operator":"equals","value":"MetaMask"}]}
            transactionWithStatus:
              summary: Onchain transaction matched via `status_type` + `status_value`
              value: >-
                {"type":"transaction","event":"swap","resolved_event":"swap","status_type":"transaction","status_value":"success","filters":[]}
        - name: end_step
          in: query
          schema:
            type: string
          description: >-
            Optional JSON-encoded end-step spec, same shape as `start_step`.
            When present, paths are truncated at the first end-event match
            (suffixed `__END_MATCH__`) and only converting sessions are
            returned.
          examples:
            trackEvent:
              summary: Custom track event as the conversion goal
              value: >-
                {"type":"track","event":"swap","resolved_event":"swap","filters":[]}
            endWithFilter:
              summary: Conversion goal with a JSON property filter (premium plan only)
              value: >-
                {"type":"track","event":"upgrade","resolved_event":"upgrade","filters":[{"operand":"plan","operator":"equals","value":"premium"}]}
            transactionEnd:
              summary: Successful onchain transaction as the end step
              value: >-
                {"type":"transaction","event":"swap","resolved_event":"swap","status_type":"transaction","status_value":"success","filters":[]}
        - name: global_filters
          in: query
          schema:
            type: string
          description: >-
            Optional JSON-encoded array of `{operand, operator, value, values?}`
            filters applied to both the start-session scan and the
            relevant-events scan. Useful for restricting the entire flow to a
            specific cohort (e.g. desktop visitors, a UTM source, a wallet
            provider).
          examples:
            deviceFilter:
              summary: Restrict to desktop sessions
              value: '[{"operand":"device","operator":"equals","value":"desktop"}]'
            utmCohort:
              summary: Restrict to paid UTM cohorts using `in`
              value: >-
                [{"operand":"utm_source","operator":"in","value":"google","values":["google","twitter","farcaster"]}]
            jsonProperty:
              summary: Filter by a JSON property (MetaMask wallet only)
              value: >-
                [{"operand":"provider_name","operator":"equals","value":"MetaMask"}]
            combined:
              summary: Combine standard column + JSON property filters
              value: >-
                [{"operand":"device","operator":"equals","value":"desktop"},{"operand":"chain_id","operator":"equals","value":"1"}]
        - name: window_seconds
          in: query
          schema:
            type: integer
            default: 1209600
            minimum: 1
          description: >-
            Conversion window length in seconds. Defaults to 1,209,600 (14
            days).
          example: 1209600
        - name: max_steps
          in: query
          schema:
            type: integer
            default: 4
            minimum: 2
            maximum: 10
          description: >-
            Maximum number of transitions per session (Sankey depth). Clamped to
            2..10.
          example: 4
      responses:
        '200':
          description: Per-step Sankey transitions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsResponse'
              example:
                meta:
                  - name: step
                    type: UInt32
                  - name: source
                    type: String
                  - name: target
                    type: String
                  - name: transitions
                    type: UInt64
                  - name: percentage
                    type: Float64
                data:
                  - step: 1
                    source: /
                    target: /swap
                    transitions: 412
                    percentage: 47.83
                  - step: 1
                    source: /
                    target: /earn
                    transitions: 248
                    percentage: 28.79
                  - step: 2
                    source: /swap
                    target: connect
                    transitions: 198
                    percentage: 38.6
                  - step: 3
                    source: connect
                    target: swap__END_MATCH__
                    transitions: 112
                    percentage: 56.57
                rows: 4
                rows_before_limit_at_least: 4
        '400':
          description: Invalid `start_step`, `end_step`, or other query parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RestApiError'
        '401':
          description: Invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RestApiError'
        '403':
          description: Insufficient permissions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RestApiError'
components:
  schemas:
    AnalyticsResponse:
      type: object
      description: >-
        Analytics endpoint response. The `data` array contains the rows; the
        exact row shape depends on the endpoint. `meta` carries column type
        information for rendering, `rows` is the row count, and `statistics`
        holds query timing metadata.
      properties:
        data:
          type: array
          items:
            type: object
            additionalProperties: true
        meta:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              type:
                type: string
        rows:
          type: integer
        rows_before_limit_at_least:
          type: integer
        statistics:
          type: object
          additionalProperties: true
    RestApiError:
      $ref: '#/components/schemas/Error'
      description: >-
        Deprecated alias for `Error`. Existing endpoint specs reference this
        name; new specs should reference `Error` directly.
    Error:
      type: object
      description: >-
        Standard error envelope returned by every public API endpoint for any
        non-2xx response. The HTTP status code carries success/failure; the body
        provides a machine-readable `code`, a human-readable `message`, and a
        `doc_url` pointing at the matching section of the docs so agents can
        fetch context on the fly.
      properties:
        error:
          type: object
          required:
            - code
            - message
            - doc_url
          properties:
            code:
              $ref: '#/components/schemas/ErrorCode'
            message:
              type: string
              description: >-
                Human-readable error description. Wording may change between
                releases, so branch on `code`, not `message`.
            doc_url:
              type: string
              format: uri
              description: >-
                Link to the matching section of the errors reference at
                https://docs.formo.so/api/errors.
            param:
              type: string
              description: >-
                When the error pertains to a specific request field, the dotted
                path to that field (e.g. `body.trigger_filters.0.value`).
            details:
              type: object
              additionalProperties: true
              description: >-
                Code-specific extra context. For `INVALID_VALIDATION_REQUEST`
                this is a `{ fieldPath: message }` map of every Zod validation
                failure.
      required:
        - error
    ErrorCode:
      type: string
      description: >-
        Stable, enumerated error codes. New codes may be added in any release;
        clients should treat unknown codes as the closest matching HTTP status
        family.
      enum:
        - INTERNAL_SERVER_ERROR
        - INVALID_VALIDATION_REQUEST
        - UNAUTHORIZED
        - BAD_REQUEST
        - FORBIDDEN
        - NOT_FOUND
        - CONFLICT
        - INVALID_CHAIN_ID
        - CONTEXT_LIMIT_EXCEEDED
        - SERVICE_UNAVAILABLE
        - TOO_MANY_REQUESTS
        - IDEMPOTENCY_IN_PROGRESS
        - INVALID_IDEMPOTENCY_KEY
  securitySchemes:
    WorkspaceApiKey:
      type: http
      scheme: bearer
      description: >-
        Workspace API key (e.g. `formo_xxx`). Create one in the Formo dashboard
        under Team Settings > API Keys.

````