Report
A table of rows and columns built from your event data — sales, tickets, attendees, and more.
Building a reporting integration
The reporting API lets you query tabular event data with configurable columns, filters, sorting, and aggregation.
What is a report?
A Report is a table of rows and columns built from your event data. Each column corresponds to a
data point (a metric), and each row is a ReportLine containing one ReportCell per column.
The available report types are determined by your account. Common types include "sales",
"tickets", and "events". Each type has its own set of metrics, which you can discover via
metrics.
At a glance
These are the root fields and mutations you use to interact with reports:
Query.reportQuery.reportViewsMutation.createReportViewMutation.modifyReportViewMutation.deleteReportView
1. Starting a report
To fetch a report you call Query.report. You must provide either:
- type — a report type key such as
"sales". This returns the default configuration for that type. - id — an opaque
idstring you previously received from the API. This restores an exact report configuration, including any filters, sorting, and column choices you applied.
Start with type when building a UI from scratch. Use id when you want to bookmark or share a specific view, or when you navigate between pages of the same report.
The available type keys (such as "sales", "tickets", or "events") depend on your account
configuration. The type of the report you are currently looking at is readable via type.
query GetReport($locale: Locale!) { report(type: "sales", locale: $locale) { id # Opaque string; pass this back as `id` to restore this exact configuration. numberOfLines columnsV2 { label typeHints } lines { cells { value typeHints label } } } }query GetReport($locale: Locale!) { report(type: "sales", locale: $locale) { id # Opaque string; pass this back as `id` to restore this exact configuration. numberOfLines columnsV2 { label typeHints } lines { cells { value typeHints label } } } }
2. Reading cells and type hints
ReportCell.value is always a plain string (or null). The ReportCell.typeHints list tells you how
to interpret it:
| Type hint | Meaning |
|---|---|
"money" | Integer amount in the smallest currency unit (e.g. cents). |
"datetime" | Unix timestamp (seconds since epoch). |
"comma_separated_list" | Multiple values joined by commas. |
"key_value" + "json_encoded" | JSON-encoded object with string values. |
"minmax" | Aggregated range formatted as "min/max" (e.g. "100/500"). |
"cardinality" | Number of distinct values in an aggregated group. |
"row_count" | Number of rows in an aggregated group. |
Use ReportCell.label whenever it is non-null — it contains a localized, human-readable string
ready to display. For example, when ReportCell.value is an event ID, ReportCell.label is the
event's name in the requested locale.
ReportColumn.typeHints reflects the same hints at the column level, so you can determine the
rendering strategy before iterating over rows.
3. Pagination
Use first and offset on Query.report to page through results. Use numberOfLines to
get the total row count for building page controls.
offset must be a multiple of first — pagination works in whole pages, and an offset that falls
between page boundaries is rounded down to the previous multiple of first.
query GetReportPage($first: Int!, $offset: Int!, $locale: Locale!) { report(type: "sales", first: $first, offset: $offset, locale: $locale) { numberOfLines # Total rows matching the current filters — use this to calculate page count. lines { cells { value label typeHints } } } }query GetReportPage($first: Int!, $offset: Int!, $locale: Locale!) { report(type: "sales", first: $first, offset: $offset, locale: $locale) { numberOfLines # Total rows matching the current filters — use this to calculate page count. lines { cells { value label typeHints } } } }
4. Filtering
Filters narrow the rows in the report. Pass them as arguments to Query.report:
rangeFilters— numeric min/max bounds (e.g. sale amount, number of guests).wildcardFilters— text search against a metric (e.g. order number, customer name).
Each filter entry requires a ReportMetric.key to identify which metric to filter. You can
discover available filters and their types by inspecting filters or
ReportColumn.filters.
query GetFilteredReport($locale: Locale!, $first: Int!, $offset: Int!) { report( type: "sales", first: $first, offset: $offset, locale: $locale, rangeFilters: [{ key: "amount", min: 1000, max: 5000 }], # Amount in cents wildcardFilters: [{ key: "order_number", query: "j9i5" }] ) { numberOfLines filters { key label(locale: $locale) typeHints valueLabel(locale: $locale) # Human-readable summary of the active filter value. ...on RangeReportFilter { currentMin currentMax } ...on WildcardReportFilter { query } ...on OptionsReportFilter { selectedOptions { label(locale: $locale) } } } lines { cells { value label typeHints } } } }query GetFilteredReport($locale: Locale!, $first: Int!, $offset: Int!) { report( type: "sales", first: $first, offset: $offset, locale: $locale, rangeFilters: [{ key: "amount", min: 1000, max: 5000 }], # Amount in cents wildcardFilters: [{ key: "order_number", query: "j9i5" }] ) { numberOfLines filters { key label(locale: $locale) typeHints valueLabel(locale: $locale) # Human-readable summary of the active filter value. ...on RangeReportFilter { currentMin currentMax } ...on WildcardReportFilter { query } ...on OptionsReportFilter { selectedOptions { label(locale: $locale) } } } lines { cells { value label typeHints } } } }
To clear a filter, traverse ReportFilter.cleared — it returns a new Report with that filter
removed. See the next section for how this pattern works.
5. The functional navigation pattern
This is the most important thing to understand about the reporting API: you do not call mutations
to change the report configuration. Instead, you read a modified Report by traversing special
fields on columns, filters, and aggregation options. Each such field resolves to a brand-new
Report that reflects the change.
This means a single query can fetch both the current report data and the "what would this look like if sorted by column X" state — without any extra round-trips.
query ReportWithNavigation($locale: Locale!) { report(type: "sales", locale: $locale) { id numberOfLines columnsV2 { label typeHints sortDirection # Current sort direction for this column, if any. sortedAscending { # Non-null when the column is sortable. id # Pass this id back to Query.report to apply the sort. } sortedDescending { id } removed { # The report without this column. id } } filters { key label(locale: $locale) cleared { # The report with this filter removed. id } } aggregationOptions { label(locale: $locale) isEnabled report { # The report grouped by this aggregation option. id } } withoutAggregations { # The report with all aggregations removed. id } } }query ReportWithNavigation($locale: Locale!) { report(type: "sales", locale: $locale) { id numberOfLines columnsV2 { label typeHints sortDirection # Current sort direction for this column, if any. sortedAscending { # Non-null when the column is sortable. id # Pass this id back to Query.report to apply the sort. } sortedDescending { id } removed { # The report without this column. id } } filters { key label(locale: $locale) cleared { # The report with this filter removed. id } } aggregationOptions { label(locale: $locale) isEnabled report { # The report grouped by this aggregation option. id } } withoutAggregations { # The report with all aggregations removed. id } } }
When you want to apply a navigation step (e.g. the user clicks a column header to sort), take the
id from the relevant nested Report and pass it back as the id argument to Query.report:
query ApplyNavigation($reportId: ID!, $locale: Locale!, $first: Int!, $offset: Int!) { report(id: $reportId, locale: $locale, first: $first, offset: $offset) { id numberOfLines sorting { key direction } lines { cells { value label typeHints } } } }query ApplyNavigation($reportId: ID!, $locale: Locale!, $first: Int!, $offset: Int!) { report(id: $reportId, locale: $locale, first: $first, offset: $offset) { id numberOfLines sorting { key direction } lines { cells { value label typeHints } } } }
6. Sorting via arguments
As an alternative to the functional navigation pattern, you can pass the sort argument directly to
Query.report. Each entry is a SortInput with a metric key and an optional direction (ASC or
DESC, defaulting to ASC).
Only sortable metrics can be used — check that ReportColumn.sortedAscending is non-null, or that
ReportMetric.isSortable is true, to determine which columns support sorting.
query GetSortedReport($locale: Locale!) { report( type: "sales", locale: $locale, sort: [{ metric: "amount", direction: DESC }] ) { id sorting { key direction } lines { cells { value label typeHints } } } }query GetSortedReport($locale: Locale!) { report( type: "sales", locale: $locale, sort: [{ metric: "amount", direction: DESC }] ) { id sorting { key direction } lines { cells { value label typeHints } } } }
7. Aggregation
Aggregation groups rows by one or more dimensions (e.g. by event or by date) and summarizes the
numeric columns. Use aggregationOptions to discover available groupings and to navigate to
aggregated variants of the current report.
When a report is aggregated, ReportColumn.isAggregated is true for the grouping columns, and
numeric summary cells may carry additional type hints such as "minmax", "cardinality", or
"row_count" depending on the summary mode of each metric.
query GetAggregationOptions($locale: Locale!) { report(type: "sales", locale: $locale) { aggregationOptions { label(locale: $locale) isEnabled report { id # Pass this to Query.report to view the aggregated report. } } withoutAggregations { id # Pass this to remove all groupings. } } }query GetAggregationOptions($locale: Locale!) { report(type: "sales", locale: $locale) { aggregationOptions { label(locale: $locale) isEnabled report { id # Pass this to Query.report to view the aggregated report. } } withoutAggregations { id # Pass this to remove all groupings. } } }
8. Saved views
A ReportView is a named snapshot of a report configuration — a label attached to a id.
Users (or your application) can save, rename, and delete views.
Use Query.reportViews to list all views for the current user:
query GetReportViews($locale: Locale!) { reportViews(locale: $locale) { id label report { id numberOfLines columnsV2 { label typeHints } lines { cells { value label typeHints } } } } }query GetReportViews($locale: Locale!) { reportViews(locale: $locale) { id label report { id numberOfLines columnsV2 { label typeHints } lines { cells { value label typeHints } } } } }
To create a view from the current report configuration, pass the id you want to save:
mutation SaveView($reportId: ID!, $label: String!) { createReportView(input: { reportId: $reportId, label: $label }) { view { id label } } }mutation SaveView($reportId: ID!, $label: String!) { createReportView(input: { reportId: $reportId, label: $label }) { view { id label } } }
To rename a view or point it at a new report configuration:
mutation UpdateView($viewId: ID!, $label: String, $reportId: ID) { modifyReportView(input: { viewId: $viewId, label: $label, reportId: $reportId }) { view { id label } } }mutation UpdateView($viewId: ID!, $label: String, $reportId: ID) { modifyReportView(input: { viewId: $viewId, label: $label, reportId: $reportId }) { view { id label } } }
To delete a view:
mutation RemoveView($viewId: ID!) { deleteReportView(input: { viewId: $viewId }) { success } }mutation RemoveView($viewId: ID!) { deleteReportView(input: { viewId: $viewId }) { success } }
9. Gotchas
Omitting locale silently falls back to English.
When you omit the locale argument on Query.report, ReportCell.label and ReportColumn.label
come back in English rather than the user's language. Note that fields like ReportFilter.label
take their own required locale argument instead. Always pass a locale when your goal is to display
the report to a user.
ReportCell.value is always a raw string — never display it directly.
A money cell's value is an integer in the smallest currency unit (e.g. "1250" means €12.50 in a
euro account). Check ReportCell.typeHints first and format accordingly. Use ReportCell.label instead when a
pre-formatted display string is sufficient.
Aggregated cells have different ReportCell.typeHints than plain cells.
When a report is grouped, a "money" column no longer holds a single amount — it may hold a
"minmax" range ("1000/5000") or a "cardinality" count. Read ReportCell.typeHints per-cell, not just
per-column, when working with aggregated data.
offset is rounded down to a multiple of first — and ignored without first.
Pagination works in whole pages: an offset that is not a multiple of first is silently rounded
down to the previous page boundary (e.g. offset: 150, first: 100 returns rows 100–199, not
150–249). Passing offset without first has no effect. Always page in steps of first, starting
at 0.
OptionsReportFilter cannot be applied via Query.report arguments.
The rangeFilters and wildcardFilters arguments only cover range and text filters. To apply a
categorical filter (e.g. filter by specific events), navigate to ReportFilterOption.report and
pass the resulting id back to Query.report. See the options filter recipe below.
Unknown or unusable keys in sort, rangeFilters, and wildcardFilters are silently ignored.
Entries whose key (or metric, for sort) does not match a metric of the report type produce no
error — the report simply comes back unfiltered or unsorted. The same goes for sort entries that
reference a non-sortable metric. A typo'd key gives you no signal, so validate keys against
metrics and check ReportMetric.isSortable before sorting.
id is not a stable identifier — it encodes the current configuration.
Every navigation step (sort, filter change, column toggle) produces a new id. Don't store
it as a permanent reference to a report type; store it only as a bookmark to restore a specific
configuration. For a named, persistent snapshot, use saved views instead.
System-provided ReportViews have a null ReportView.id.
Only user-created views have a ReportView.id and can be modified or deleted. Always check for null before
passing ReportView.id to modifyReportView or deleteReportView.
Deprecated fields ReportLine.values and ReportLine.labels omit type hints.
They exist for backwards compatibility. Use ReportLine.cells — each cell has a ReportCell.value,
ReportCell.typeHints, and an optional localized ReportCell.label.
10. Recipes
Export all rows as CSV
Paginate through all rows until you have fetched the full result set. Use numberOfLines to
know when to stop.
query ExportPage($type: String!, $locale: Locale!, $first: Int!, $offset: Int!) { report(type: $type, locale: $locale, first: $first, offset: $offset) { numberOfLines # Total rows — compare to offset + first to detect the last page. columnsV2 { label # Column header } lines { cells { value # Raw string value; format using typeHints. label # Pre-formatted display string; use this when available. typeHints # Tells you how to interpret value (e.g. "money", "datetime"). } } } }query ExportPage($type: String!, $locale: Locale!, $first: Int!, $offset: Int!) { report(type: $type, locale: $locale, first: $first, offset: $offset) { numberOfLines # Total rows — compare to offset + first to detect the last page. columnsV2 { label # Column header } lines { cells { value # Raw string value; format using typeHints. label # Pre-formatted display string; use this when available. typeHints # Tells you how to interpret value (e.g. "money", "datetime"). } } } }
page 1: offset=0, first=200
page 2: offset=200, first=200
page N: offset = (N-1) * 200
stop when: offset + 200 >= numberOfLines
Build a column selector
Use metricConnections to enumerate all available metrics and let users add or remove
columns. ReportMetricConnection.toggledColumn returns the Report you'd get after clicking
the toggle.
query GetColumnSelector($reportId: ID!, $locale: Locale!) { report(id: $reportId, locale: $locale) { metricConnections { metric { key label(locale: $locale) typeHints } isColumnEnabled # true when this metric is currently a visible column. toggledColumn { id # Pass this id back to Query.report to apply the toggle. } } } }query GetColumnSelector($reportId: ID!, $locale: Locale!) { report(id: $reportId, locale: $locale) { metricConnections { metric { key label(locale: $locale) typeHints } isColumnEnabled # true when this metric is currently a visible column. toggledColumn { id # Pass this id back to Query.report to apply the toggle. } } } }
When the user toggles a metric, take toggledColumn.id and issue a new Query.report(id: ...) call
to reload the report with that column added or removed.
Apply a categorical (options) filter
OptionsReportFilter options are navigated, not passed as arguments. Fetch the available choices,
then follow ReportFilterOption.report to get the filtered Report.
query GetFilterOptions($reportId: ID!, $filterKey: String!, $locale: Locale!) { report(id: $reportId, locale: $locale) { filter(key: $filterKey) { ... on OptionsReportFilter { label(locale: $locale) options { label(locale: $locale) # Display name for this choice. report { id # Pass this id to load the report filtered to this option. } } selectedOptions { label(locale: $locale) # Currently active selections. } } } } }query GetFilterOptions($reportId: ID!, $filterKey: String!, $locale: Locale!) { report(id: $reportId, locale: $locale) { filter(key: $filterKey) { ... on OptionsReportFilter { label(locale: $locale) options { label(locale: $locale) # Display name for this choice. report { id # Pass this id to load the report filtered to this option. } } selectedOptions { label(locale: $locale) # Currently active selections. } } } } }
Pass id from the chosen option to Query.report as the id argument. To select multiple
options, chain the navigations: take the id from the first option's report, load it, then follow
the next option's id from there. To clear the filter, use ReportFilter.cleared.
Fields
| Field | Description | Type |
|---|---|---|
aggregationOptions | The grouping dimensions available for this report. | [ ReportAggregationOption ] |
columns | The keys of the columns currently in the report. Deprecated — use `columnsV2` instead. | [ String ] |
columnsV2 | The columns currently included in the report, in display order. | [ ReportColumn ] |
filter | Returns the filter identified by `ReportFilter.key`, or null if no filter with that key exists for this report. | ReportFilter |
filters | All filters available for this report, whether or not they are currently active. | [ ReportFilter ] |
id | An opaque identifier that encodes the complete report configuration — type, columns, filters, sorting, and active aggregations. | ID! |
lines | The data rows for the current page. Each `ReportLine` contains one cell per column in the same order as `columnsV2`. | [ ReportLine ] |
metricConnections | All available metrics paired with their current column state. | [ ReportMetricConnection ] |
metrics | Flat list of all metrics available for this report type. | [ ReportMetric ] |
numberOfLines | The total number of rows matching the current filters, regardless of pagination. | Int |
sorting | The sort orders currently applied to the report. Each entry names a metric key and a direction. | [ ReportSorting ] |
summaries | One summary value per column, in the same order as `columnsV2`. | [ String ] |
type | The report type, such as `"sales"` or `"events"`. The type determines which metrics are available and how rows are defined. | ReportType |
withoutAggregations | The same report with all grouping removed, returning to individual row-level data. | Report |