> ## 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 KPIs

> Time-series traffic KPIs (visitors, pageviews, bounce rate, session duration), with optional dimension breakdown and previous-period comparison.

Returns the same KPI series that powers the Formo dashboard's overview chart. Here `sessions` is the per-day session count (the overview's **Sessions** card), while `visitors` (and `visitors_current` / `visitors_previous`) are unique anonymous visitors (the overview's **Visitors** card), returned only when `include_previous_period=true` and no `group_by` is set.

Use `group_by` to break results down by `referrer`, `location`, `device`, `browser`, `os`, or any UTM dimension. Set `include_previous_period=true` to also receive the equivalent prior window for week-over-week comparisons.

## Filtering

Pass `filters` as a JSON-encoded array of `{ field, op, value }` objects. Multiple filters are combined with implicit AND.

For example, return KPIs only for traffic whose referrer contains `google`:

```bash theme={null}
curl -G "https://api.formo.so/v0/kpis" \
  -H "Authorization: Bearer $FORMO_API_KEY" \
  --data-urlencode "date_from=2026-07-01" \
  --data-urlencode "date_to=2026-07-28" \
  --data-urlencode 'filters=[{"field":"referrer","op":"contains","value":"google"}]'
```

To filter by page path, use the `page` field:

```bash theme={null}
curl -G "https://api.formo.so/v0/kpis" \
  -H "Authorization: Bearer $FORMO_API_KEY" \
  --data-urlencode "date_from=2026-07-01" \
  --data-urlencode "date_to=2026-07-28" \
  --data-urlencode 'filters=[{"field":"page","op":"eq","value":"/pricing"}]' \
  --data-urlencode "page_scope=page"
```

`page_scope=page` is the default. It counts `pageviews` only on the filtered page and calculates `bounce_rate` and `avg_session_sec` for sessions that landed there. Use `page_scope=session` for the legacy behavior, which includes all activity in any session that viewed the page. The scope only changes requests that include a `page` filter.

<Note>
  Every public filter leaf uses `{"field", "op", "value"}`, and every
  collection is named `filters`. Query endpoints and saved segments combine
  their arrays with implicit AND. Profile search adds a sibling `logic` field,
  so its body is `{"filters": [...], "logic": "and" | "or"}`.
</Note>


## OpenAPI

````yaml GET /v0/kpis
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.** Every endpoint on `api.formo.so` requires a workspace API key with
    the appropriate scopes (see `x-api-scopes`). The one exception is `POST
    /v0/raw_events`, which runs on `events.formo.so` and authenticates with the
    project SDK write key instead.


    **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.** Non-2xx responses from `api.formo.so` use the `Error` envelope,
    with two exceptions: some rate-limit rejections reply in plain text, and
    `POST /v0/raw_events` returns a plain `{ "error": "..." }` object. Treat the
    HTTP status as authoritative and parse defensively: `{ 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 write requests to
    alerts, boards, charts, contracts, segments, and import to make retries
    safe; the response is cached for 24 h and replayed on duplicate keys.
    Profile writes and `POST /v0/query` do not support it.
  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/kpis:
    get:
      tags:
        - Query
      summary: Get KPIs (sessions, pageviews, bounce rate, session duration)
      description: >-
        Time-series traffic KPIs with optional dimension breakdown. Returns
        `sessions` (session count), pageviews, bounce rate and average session
        length. When `include_previous_period=true` without `group_by`, the
        response also includes `visitors` (unique visitors), `visitors_current`
        and `visitors_previous`.
      operationId: getAnalyticsKpis
      parameters:
        - $ref: '#/components/parameters/AnalyticsDateFrom'
        - $ref: '#/components/parameters/AnalyticsDateTo'
        - $ref: '#/components/parameters/AnalyticsFilters'
        - $ref: '#/components/parameters/AnalyticsPageScope'
        - name: group_by
          in: query
          schema:
            type: string
            enum:
              - referrer
              - location
              - device
              - browser
              - os
              - utm_source
              - utm_medium
              - utm_campaign
          description: Dimension to break down by
        - name: limit
          in: query
          schema:
            type: integer
        - name: include_previous_period
          in: query
          schema:
            type: boolean
          description: Return current and previous period for WoW comparison
      responses:
        '200':
          description: KPI time-series
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyticsResponse'
              example:
                meta:
                  - name: date
                    type: Date
                  - name: project_id
                    type: String
                  - name: sessions
                    type: UInt64
                  - name: pageviews
                    type: UInt64
                  - name: bounce_rate
                    type: Float64
                  - name: avg_session_sec
                    type: Float64
                data:
                  - date: '2025-10-15'
                    project_id: proj_abc
                    sessions: 412
                    pageviews: 1187
                    bounce_rate: 0.31
                    avg_session_sec: 142.6
                  - date: '2025-10-16'
                    project_id: proj_abc
                    sessions: 487
                    pageviews: 1402
                    bounce_rate: 0.28
                    avg_session_sec: 156.2
                  - date: '2025-10-17'
                    project_id: proj_abc
                    sessions: 533
                    pageviews: 1610
                    bounce_rate: 0.26
                    avg_session_sec: 168.4
                rows: 3
                rows_before_limit_at_least: 3
                statistics:
                  elapsed: 0.041
                  rows_read: 12340
                  bytes_read: 482310
        '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:
  parameters:
    AnalyticsDateFrom:
      name: date_from
      in: query
      schema:
        type: string
        format: date
      description: Inclusive start date (YYYY-MM-DD). Defaults to 7 days before date_to.
    AnalyticsDateTo:
      name: date_to
      in: query
      schema:
        type: string
        format: date
      description: Inclusive end date (YYYY-MM-DD). Defaults to today.
    AnalyticsFilters:
      name: filters
      in: query
      description: >-
        Array of filter conditions, JSON-encoded in the query string. Entries
        use `{ field, op, value }` and are combined with implicit AND. For
        example, filter traffic referred by Google with
        `[{"field":"referrer","op":"contains","value":"google"}]`, or filter a
        page with `[{"field":"page","op":"eq","value":"/pricing"}]`. For `in` /
        `nin`, pass a non-empty array value such as `["chrome","firefox"]` or a
        pipe-delimited string such as `"chrome|firefox"`. Array string members
        cannot contain a literal `|`, which is reserved as the Tinybird
        membership separator. Optional one-level nested `filters` use the same
        canonical envelope and membership rule. On the user-aggregate endpoints
        (lifecycle, frequency) the same array also carries profile-family
        entries: profile metrics (net_worth_usd, volume, revenue, points),
        social identity fields, a lifecycle entry
        ({"field":"lifecycle","op":"in","value":"New|Power user"}), and resource
        entries using the stable fields chains.balance, apps.balance,
        tokens.balance, and labels.value with named qualifier properties
        (chain_id, app_id, token_address, scope, tag_id). The retired per-family
        parameters (socials, chain_filters, app_filters, token_filters,
        label_filters, profile_filters, lifecycle_filter) are rejected. Overview
        also accepts raw Data source filters with `field` set to `channel`, ops
        `eq`/`neq`/`in`/`nin`, and values `web`, `mobile`, `api`, `import`,
        `server`, `onchain`; this is distinct from acquisition Channel
        (`channel_type`) and the two can be combined.
      schema:
        type: string
      example: '[{"field":"referrer","op":"contains","value":"google"}]'
    AnalyticsPageScope:
      name: page_scope
      in: query
      schema:
        type: string
        enum:
          - page
          - session
        default: page
      description: >-
        Controls how a `page` filter is interpreted. `page` (default) scopes
        metrics to activity on the filtered page. On `/v0/kpis`, `pageviews`
        counts only views of the filtered page, while `bounce_rate` and
        `avg_session_sec` are entry-page metrics for sessions that landed there.
        `session` preserves the legacy session scope: metrics include all
        relevant activity in any session that viewed the page. `sessions` and
        `visitors` are unchanged between scopes. This parameter only affects
        requests that include a `page` filter. It is distinct from
        `/v0/top_pages` `mode`, which selects the all, entry, or exit page-flow
        view.
      example: page
  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.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.