> For the complete documentation index, see [llms.txt](https://docs.graspil.com/en/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.graspil.com/en/api/reports.md).

# Report & dashboard builder

Methods for building analytical reports — the same reports you see in the dashboard under [Report Builder](/en/app/reports.md), but through the API. Useful if you want charts and tables based on your data inside your own system (internal analytics, a dashboard, a reporting bot, etc.) without opening the dashboard.

{% hint style="info" %}
Report builder methods are only available on the **Premium** plan.
{% endhint %}

### How it works in a nutshell

1. Reports are built from the events you send to Graspil (see [Event reference](/en/app/reports/events.md)). To find out which events and fields are available for your bot, use the `events`, `event-fields`, and `utm-labels` methods — they tell you what you can plug into filters and the report itself.
2. The report itself is described by a single JSON object, `report_config` — it specifies what to count (which event, which aggregation), how to filter, and how to group. Its format is described in detail below — it's the same format used by the visual report builder in the dashboard.
3. Before saving a report, you can run it as a dry run with the `preview` method — see the first rows of the result and make sure everything is calculated the way you expect.
4. Once the report is ready, save it with the `save` method. Saved reports can be listed via `list`.

### Authentication

If your API key is tied to a single bot (a regular key from the "My Bots" section — see [Authorization](/en/api/auth.md)), you don't need to specify anything else — all methods apply to that bot.

If you're using a key from the [API Keys](https://app.graspil.com/api-keys) section that's been issued for several bots or all of your bots, every request must explicitly state which bot it's for — via the `Resource-Key` header, or the `resource_key` field (for `POST`) / query parameter (for `GET`). The value is the bot's public key: the part before the colon in the bot's API key, which you can see in the "My Bots" section next to the key icon (format `bot_key:api_key`).

{% hint style="info" %}
The `save` and `list` methods only work with a personal key from the "API Keys" section — a plain single-bot key won't work for them.
{% endhint %}

### Response format

Successful response:

{% code overflow="wrap" %}

```json
{ "ok": true, "data": { ... } }
```

{% endcode %}

Error response:

{% code overflow="wrap" %}

```json
{ "ok": false, "error": "...", "error_code": 400 }
```

{% endcode %}

***

## Report configuration (`report_config`)

This is the main object that describes a report. It's passed to the `preview` and `save` methods, and it's also what saved reports return. It has two parts: `source_request` (what to count and how) and `settings` (the date range):

{% code overflow="wrap" %}

```json
{
  "source_request": {
    "type": "trends",
    "type_chart": "line",
    "requests": [ ... ],
    "filters": null,
    "separators": [ ... ]
  },
  "settings": {
    "params": {
      ":date_from": "2026-01-01 00:00:00",
      ":date_to": "2026-01-31 23:59:59"
    }
  }
}
```

{% endcode %}

| Field             | Description                                                                                                                                           |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source_request`  | What to count and how — report type (`type` / `type_chart`), events, filters, groupings. Described below                                              |
| `settings.params` | The date range the report is built for. The resource (bot) is filled in automatically from your key — you don't need to pass `:resource_key` yourself |

| `settings.params` parameter | Type   | Required | Description                                       |
| --------------------------- | ------ | -------- | ------------------------------------------------- |
| `:date_from`                | string | yes      | Start of the period, format `YYYY-MM-DD HH:MM:SS` |
| `:date_to`                  | string | yes      | End of the period, format `YYYY-MM-DD HH:MM:SS`   |

### Report types (`source_request.type` / `type_chart`)

| `type`      | `type_chart` | What it shows                                                                                                | When to use it                                                                                 |
| ----------- | ------------ | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| `trends`    | `line`       | A metric's dynamics over days — an "events/users over time" chart                                            | The most common type: "how many events/users were there each day"                              |
| `trends`    | `table`      | Same as above, but as a table instead of a chart. Note: table uses a shorter set of `agr` values — see below | When you need exact daily numbers rather than a picture                                        |
| `list`      | `list`       | A list of raw rows from the events table (or another table), without aggregation                             | When you need a list of specific events rather than a summarized number — e.g. an activity log |
| `funnels`   | `funnels`    | A funnel: how many users went through a chain of events step by step                                         | "How many of the people who opened a product page made it to checkout"                         |
| `retention` | `retention`  | Retention: did users come back and repeat an action after N days                                             | "How many users who paid once paid again a week later"                                         |

Depending on `type`, different fields of `source_request` are filled in — they're described in the sections below.

***

## Trends and tables (`type: "trends"`)

| Field        | Type        | Required | Description                                                                               |
| ------------ | ----------- | -------- | ----------------------------------------------------------------------------------------- |
| `requests`   | array       | yes      | One or more metrics on the same chart/table — see "Metric (`requests`)" below             |
| `filters`    | array\|null | no       | A shared filter, applied to all metrics at once — see "Filters" below                     |
| `separators` | array       | no       | Splits a metric into several lines/rows by a field — see "Breakdown (`separators`)" below |

### Metric (`requests`)

Each item in the `requests` array is one metric (one line on the chart / one row in the table):

{% code overflow="wrap" %}

```json
{
  "type": "trend",
  "event_id": 5,
  "event_name": "purchase",
  "label": "Purchases",
  "agr": "total",
  "filters": null
}
```

{% endcode %}

| Field        | Type        | Required | Description                                                                                     |
| ------------ | ----------- | -------- | ----------------------------------------------------------------------------------------------- |
| `type`       | string      | yes      | Always `"trend"`                                                                                |
| `event_id`   | int\|null   | no       | ID of the event being counted (see the `events` method below). `null` means counting all events |
| `event_name` | string      | yes      | Event name — just for readability, doesn't affect the calculation                               |
| `label`      | string      | no       | A label for the metric that shows up in the result (e.g. the chart line's name)                 |
| `agr`        | string      | yes      | How to aggregate — see the table below                                                          |
| `filters`    | array\|null | no       | A filter for this metric only (on top of the shared `source_request.filters`)                   |

#### Aggregations (`agr`)

The allowed values depend on `type_chart`.

**`type_chart: "table"`:**

| Value        | What it counts                           |
| ------------ | ---------------------------------------- |
| `total`      | Total number of events                   |
| `total_uniq` | Number of unique users who had the event |
| `avg`        | Average number of events per user        |
| `min`        | Minimum number of events per user        |
| `max`        | Maximum number of events per user        |
| `median`     | Median number of events per user         |

**`type_chart: "line"`, `"bar"`, `"area"`, `"pie"`, etc. (any chart):**

| Value                       | What it counts                                                |
| --------------------------- | ------------------------------------------------------------- |
| `total`                     | Total number of events                                        |
| `total_uniq_user_id`        | Number of unique users who had the event                      |
| `total_uniq_session_id`     | Number of unique sessions with this event                     |
| `total_uniq_app_session_id` | Number of unique Mini App sessions with this event            |
| `avg_user_id`               | Average number of events per user                             |
| `min_user_id`               | Minimum number of events per user                             |
| `max_user_id`               | Maximum number of events per user                             |
| `median_user_id`            | Median number of events per user                              |
| `property_sum`              | Sum of a numeric event field (`agr_field`) — e.g. sales total |
| `property_avg`              | Average value of a numeric event field                        |
| `property_min`              | Minimum value of a numeric event field                        |
| `property_max`              | Maximum value of a numeric event field                        |
| `property_median`           | Median value of a numeric event field                         |

{% hint style="warning" %}
Don't mix the two sets. A request with `type_chart: "table"` and `agr: "total_uniq_user_id"` fails with `Unsupported aggregation`. Use the bare form (`total_uniq`) for tables.
{% endhint %}

For `property_*` aggregations, the `agr_field` parameter is also required — which field to aggregate. Two values are available:

* `events.value_int` — the "raw" value exactly as it was sent in `value_num`, without converting it to normal units and without currency conversion. For monetary events this is the smallest unit of the currency (e.g. kopecks for RUB)
* `events.value_amount` — the same value, converted to normal units (rubles instead of kopecks) and to the exchange rate of the currency set in the key owner's [profile](https://app.graspil.com/profile)

{% hint style="warning" %}
If you're totalling money (sales, revenue, etc.), use `agr_field: "events.value_amount"`. With `events.value_int`, the total for monetary events will usually look 100x too large (or more, if the event's unit isn't kopecks/cents).
{% endhint %}

**Example:** sales total by day, in the profile's currency:

{% code overflow="wrap" %}

```json
{ "type": "trend", "event_id": 5, "event_name": "purchase", "label": "Revenue", "agr": "property_sum", "agr_field": "events.value_amount" }
```

{% endcode %}

### Filters

Filters narrow down which events are included in the calculation. They're written as a nested array `[operator, ...arguments]` — the same style as the audience filters in the [broadcast API](/en/api/broadcasts.md), except fields come from the events table (use the `event-fields` method below to see which ones are available).

* Logic: `["and", condition1, condition2, ...]`, `["or", ...]`
* Comparison: `["=", "field", value]`, `["!=", ...]`, `[">", ...]`, `["<", ...]`, `[">=", ...]`, `["<=", ...]`
* Value sets: `["in", "field", [value1, value2, ...]]`
* Range: `["between", "field", from, to]`

**Example:** only purchases of 1000 or more:

{% code overflow="wrap" %}

```json
[">=", "events.value_int", 1000]
```

{% endcode %}

{% hint style="warning" %}
A single condition is a **flat** array `[operator, field, value]`. Do not wrap it in an extra array (`[[">=", "events.value_int", 1000]]` is invalid and the API rejects it with "expected a single condition, got a nested array"). For several conditions, combine them explicitly with `["and", ...]` / `["or", ...]` — a bare list of conditions (without `"and"`/`"or"`) is also invalid.
{% endhint %}

{% hint style="info" %}
The date range (`:date_from`/`:date_to`) and your bot are added to the filter automatically — you don't need to add them yourself.
{% endhint %}

### Breakdown (`separators`)

A breakdown shows the same metric split out separately for each value of a chosen field — for example, a separate line on the chart for every custom event property value.

{% code overflow="wrap" %}

```json
"separators": [
  { "property": "events.properties.plan_name" }
]
```

{% endcode %}

You can specify several `separators` at once — the breakdown then runs across the combination of all the listed fields.

{% hint style="warning" %}
`separators` items must be objects with a whitelisted `property` (a fixed set of `events.*` columns, or anything prefixed `events.properties.*` / `user.*` / `user.custom_fields.*`). A bare string like `"utm_source"` is invalid and fails with `Wrong separator property`.

To break a report down **by UTM source/medium/campaign**, don't use `separators` — the UTM values aren't exposed as plain `events.properties.*` fields, they live behind internal encoded id columns (`events.f_param_id_*`) used by the dedicated UTM report machinery. Use `type_chart: "utm_trends"` / `"utm_table"` instead, see "UTM breakdown" below — that's what the visual report builder itself uses for UTM breakdowns.
{% endhint %}

***

## UTM breakdown (`type_chart: "utm_trends"` / `"utm_table"`)

A dedicated report type for splitting a metric out by UTM tag (source/medium/campaign) without picking specific tag values up front — for example, "sales per day, split by UTM tag".

| `type_chart` | What it shows                                                                             |
| ------------ | ----------------------------------------------------------------------------------------- |
| `utm_trends` | A chart over time, one line per UTM combination (analogous to `line`)                     |
| `utm_table`  | A flat summary table, one row per UTM combination, no time buckets (analogous to `table`) |

{% code overflow="wrap" %}

```json
{
  "source_request": {
    "type": "trends",
    "type_chart": "utm_trends",
    "requests": [
      { "type": "trend", "event_id": 28, "event_name": "Sale", "label": "",
        "agr": "property_sum", "filters": [] }
    ],
    "filters": null,
    "separators": [],
    "granularity": "day",
    "breakdown_limit": 25,
    "row_limit": 100
  },
  "settings": { "params": { ":date_from": "2026-06-01 00:00:00", ":date_to": "2026-06-29 23:59:59" } }
}
```

{% endcode %}

| Field             | Type   | Required | Description                                                                                                                                                                                |
| ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `requests`        | array  | yes      | Same format as regular trends — but `agr: "property_sum"` sums the event's monetary value field automatically (with currency conversion); an `agr_field` is not needed and is ignored here |
| `breakdown_limit` | int    | no       | Max number of distinct UTM combinations returned (5–500, default `25`) — which combinations to break down by is chosen automatically, you don't specify them                               |
| `row_limit`       | int    | no       | Max rows returned (1–5000, default `100`)                                                                                                                                                  |
| `granularity`     | string | no       | Bucket size for `utm_trends`, `day` or others — ignored for `utm_table`                                                                                                                    |

{% hint style="info" %}
If you want to filter for ONE specific known UTM value (e.g. "sales from utm\_source=vk") rather than break down by all of them, that's a regular `filters` condition on a `trends`/`line` report instead — use `utm-labels` to confirm the exact value first.
{% endhint %}

***

## Event list (`type: "list"`)

Returns raw rows without aggregation — for example, a log of the latest events.

| Field     | Type        | Required | Description                                                                              |
| --------- | ----------- | -------- | ---------------------------------------------------------------------------------------- |
| `table`   | string      | yes      | Source table, usually `"events"`                                                         |
| `columns` | array       | yes      | Which fields to return, e.g. `["events.date", "events.user_id", "events.event_type_id"]` |
| `filters` | array\|null | no       | Filter — same format as in "Filters" above                                               |
| `order`   | object      | no       | Sorting, e.g. `{ "events.date": "desc" }`                                                |
| `limit`   | int         | no       | Number of rows to return, default `50`, max `1000`                                       |
| `offset`  | int         | no       | Offset for paginated retrieval                                                           |

***

## Funnel (`type: "funnels"`)

Shows how many users made it from the first funnel event to the last.

| Field                | Type   | Required | Description                                                                                                                     |
| -------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `requests`           | array  | yes      | Funnel steps in order — each item uses the same format as "Metric" above (the `agr` field doesn't matter much, usually `total`) |
| `funnel_window`      | int    | no       | How much time a user is given to complete the whole funnel                                                                      |
| `funnel_window_unit` | string | no       | Unit for `funnel_window`: `second`, `minute`, `hour`, `day`, `week`, `month`. Defaults to 14 days                               |
| `funnel_order_type`  | string | no       | How strictly the step order is checked — see the table below                                                                    |
| `separators`         | array  | no       | Breaks down the funnel by a field, same as in trends                                                                            |

| `funnel_order_type` | Behavior                                                                                 |
| ------------------- | ---------------------------------------------------------------------------------------- |
| `ordered`           | Steps must occur in the given order, with any other actions allowed in between           |
| `strict`            | The next step must happen right after the previous one, with no other actions in between |
| `unordered`         | Step order doesn't matter                                                                |

**Example** (this shows the `source_request` contents; wrap it in a `report_config` together with `settings` — see "Report configuration"), a "viewed product → paid" funnel with a 30-day window:

{% code overflow="wrap" %}

```json
{
  "type": "funnels",
  "type_chart": "funnels",
  "requests": [
    { "type": "trend", "event_id": 1, "event_name": "view_item", "agr": "total" },
    { "type": "trend", "event_id": 5, "event_name": "purchase", "agr": "total" }
  ],
  "funnel_window": 30,
  "funnel_window_unit": "day",
  "funnel_order_type": "ordered"
}
```

{% endcode %}

***

## Retention (`type: "retention"`)

Shows what share of users who performed a starting action came back and performed it (or another action) again after a given number of days/weeks.

| Field                       | Type   | Required | Description                                                                                                                   |
| --------------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `target_entity`             | object | yes      | The starting event — who counts as "entering the cohort". Format: `{ "type": "event", "event_id": ..., "event_name": "..." }` |
| `returning_entity`          | object | yes      | The event that counts as "coming back". Same format as `target_entity`                                                        |
| `retention_total_intervals` | int    | no       | How many intervals to show, default `8`                                                                                       |
| `granularity`               | string | no       | Interval step — `day` or `week`                                                                                               |
| `retention_type`            | string | no       | The logic used to build the cohort — see the table below                                                                      |
| `separators`                | array  | no       | Breaks down retention by a field                                                                                              |

| `retention_type`            | Logic                                                                                                         |
| --------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `retention_first_time`      | The cohort includes the first matching action within the period (default)                                     |
| `retention_first_time_ever` | A user only joins the cohort if their very first action ever (regardless of the period) matches the condition |
| `retention_recurring`       | A user can belong to several cohorts — one for each interval in which they performed the starting action      |

**Example** (this shows the `source_request` contents; wrap it in a full `report_config` — see "Report configuration"), did users who opened the bot come back to open it again 8 days later:

{% code overflow="wrap" %}

```json
{
  "type": "retention",
  "type_chart": "retention",
  "granularity": "day",
  "retention_total_intervals": 8,
  "target_entity": { "type": "event", "event_id": 1, "event_name": "open_bot" },
  "returning_entity": { "type": "event", "event_id": 1, "event_name": "open_bot" }
}
```

{% endcode %}

***

## Methods

### GET /v1/reports/resources

Returns the bot (or bots) your key has access to. Useful for keys from the "API Keys" section issued for several bots — to find out which `resource_key` values are available and which to use in the other methods.

You don't need to pass `Resource-Key` for this method.

#### Example request

{% code overflow="wrap" %}

```
GET /v1/reports/resources
Api-Key: YOUR_API_KEY
```

{% endcode %}

#### Response

{% code overflow="wrap" %}

```json
{
  "ok": true,
  "data": [
    { "key": "abc123...", "name": "My bot", "type": 1 }
  ]
}
```

{% endcode %}

***

### GET /v1/reports/events

Returns the list of events that can be used in a report for a specific bot (the `event_id` field in metrics).

#### Parameters

| Parameter      | Type   | Required                                  | Description                |
| -------------- | ------ | ----------------------------------------- | -------------------------- |
| `resource_key` | string | only for keys with access to several bots | See "Authentication" above |

#### Example request

{% code overflow="wrap" %}

```
GET /v1/reports/events
Api-Key: YOUR_API_KEY
```

{% endcode %}

#### Response

{% code overflow="wrap" %}

```json
{
  "ok": true,
  "data": [
    { "id": 5, "name": "purchase", "category": "Purchases", "is_global": false }
  ]
}
```

{% endcode %}

***

### GET /v1/reports/event-fields

Returns the list of event fields that can be used in `filters`. The set of fields is fixed and doesn't depend on the bot or a specific event.

#### Example request

{% code overflow="wrap" %}

```
GET /v1/reports/event-fields
Api-Key: YOUR_API_KEY
```

{% endcode %}

#### Response

{% code overflow="wrap" %}

```json
{
  "ok": true,
  "data": [
    { "field": "events.value_int", "type": "number", "label": "Numeric value" }
  ]
}
```

{% endcode %}

`type` is one of: `number`, `string`, `date`.

***

### GET /v1/reports/utm-labels

Returns the UTM tags (sources, mediums, campaigns) actually seen for your bot — handy for offering the user a pick list in a filter instead of guessing values.

#### Parameters

| Parameter      | Type   | Required                                  | Description                |
| -------------- | ------ | ----------------------------------------- | -------------------------- |
| `resource_key` | string | only for keys with access to several bots | See "Authentication" above |

#### Example request

{% code overflow="wrap" %}

```
GET /v1/reports/utm-labels
Api-Key: YOUR_API_KEY
```

{% endcode %}

#### Response

{% code overflow="wrap" %}

```json
{
  "ok": true,
  "data": {
    "sources": ["telegram", "vk"],
    "mediums": ["cpc", "social"],
    "campaigns": ["spring_sale"]
  }
}
```

{% endcode %}

***

### POST /v1/reports/preview

Runs a report without saving it and returns the first 10 rows of the result — useful for checking that the report is set up correctly before saving it.

#### Parameters

| Parameter       | Type   | Required                                  | Description                                      |
| --------------- | ------ | ----------------------------------------- | ------------------------------------------------ |
| `resource_key`  | string | only for keys with access to several bots | See "Authentication" above                       |
| `report_config` | object | yes                                       | The report configuration — see the section above |

#### Example request

{% code overflow="wrap" %}

```json
POST /v1/reports/preview
Api-Key: YOUR_API_KEY
Content-Type: application/json

{
  "report_config": {
    "source_request": {
      "type": "trends",
      "type_chart": "line",
      "requests": [
        { "type": "trend", "event_id": 5, "event_name": "purchase", "label": "Purchases", "agr": "total" }
      ]
    },
    "settings": {
      "params": { ":date_from": "2026-06-01 00:00:00", ":date_to": "2026-06-29 23:59:59" }
    }
  }
}
```

{% endcode %}

#### Response

{% code overflow="wrap" %}

```json
{
  "ok": true,
  "data": {
    "rows": [
      { "date": ["2026-06-01", "2026-06-02"], "total": [12, 18] }
    ],
    "total_estimate": 2
  }
}
```

{% endcode %}

{% hint style="info" %}
Cohort filters (`in_cohort`/`not_in_cohort`) aren't supported in preview yet.
{% endhint %}

***

### POST /v1/reports/save

Saves a report — after that it shows up in the dashboard under "Report Builder" and is available via the `list` method.

{% hint style="info" %}
Only works with a personal key from the "API Keys" section — a saved report needs an owner.
{% endhint %}

#### Parameters

| Parameter       | Type   | Required                                  | Description                                      |
| --------------- | ------ | ----------------------------------------- | ------------------------------------------------ |
| `resource_key`  | string | only for keys with access to several bots | See "Authentication" above                       |
| `name`          | string | yes                                       | Report name                                      |
| `report_config` | object | yes                                       | The report configuration — see the section above |

#### Example request

{% code overflow="wrap" %}

```json
POST /v1/reports/save
Api-Key: YOUR_API_KEY
Content-Type: application/json

{
  "name": "Purchases by day",
  "report_config": {
    "source_request": {
      "type": "trends",
      "type_chart": "line",
      "requests": [
        { "type": "trend", "event_id": 5, "event_name": "purchase", "label": "Purchases", "agr": "total" }
      ]
    },
    "settings": {
      "params": { ":date_from": "2026-06-01 00:00:00", ":date_to": "2026-06-29 23:59:59" }
    }
  }
}
```

{% endcode %}

#### Response

{% code overflow="wrap" %}

```json
{ "ok": true, "data": { "uuid": "f1b2c3d4-..." } }
```

{% endcode %}

`uuid` is the saved report's identifier — use it to find the report in `list`, or open it in the dashboard.

***

### GET /v1/reports/list

Returns previously saved reports.

{% hint style="info" %}
Only works with a personal key from the "API Keys" section.
{% endhint %}

#### Parameters

| Parameter      | Type   | Required | Description                                                                                                      |
| -------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `resource_key` | string | no       | If set, only that bot's reports are returned. If omitted, reports for all bots available to the key are returned |

#### Example request

{% code overflow="wrap" %}

```
GET /v1/reports/list
Api-Key: YOUR_API_KEY
```

{% endcode %}

#### Response

{% code overflow="wrap" %}

```json
{
  "ok": true,
  "data": [
    { "uuid": "f1b2c3d4-...", "name": "Purchases by day", "created_at": "2026-06-29 12:00:00", "resource_key": "abc123..." }
  ]
}
```

{% endcode %}

***

## Possible errors

| Code          | When it occurs                                                                                                                                              |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`         | A required parameter is missing (e.g. `report_config` or `:date_from`/`:date_to`), or a parameter has an invalid format                                     |
| `401`         | The API key is missing or invalid                                                                                                                           |
| `403`         | The method isn't available on your plan (Premium required), the resource isn't accessible to this key, or `save`/`list` were called with a non-personal key |
| `404`         | No bot was found for the given `resource_key`                                                                                                               |
| `500` / `502` | Internal server error — retry the request later                                                                                                             |

**Example error:**

{% code overflow="wrap" %}

```json
{ "ok": false, "error": { "errors": "Premium tariff required" }, "error_code": 403 }
```

{% endcode %}
