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

# Reports API overview

> Query your payroll data with a single JSON report definition — explore datasets, run reports synchronously, export large result sets, and schedule recurring deliveries.

<Note>
  The Reports API is in **beta**. The contract is stable enough to build against, but endpoints and datasets may still change in response to feedback before general availability. Tell us what you build — and what's missing — at [hello@flowpayroll.ai](mailto:hello@flowpayroll.ai).
</Note>

The Reports API lets you query your payroll data programmatically instead of downloading fixed reports. You describe the report you want as a single JSON **report definition** — which dataset, which columns, how to filter, group, aggregate, and sort — and the same document drives everything the API can do: synchronous runs, large asynchronous exports, saved definitions, and recurring schedules. The product's reporting screens are covered in the [Reports guide](/guides/reports/reports).

All endpoints sit under the `/v1/report` base path, relative to the environment base URLs documented on the [API reference introduction](/api-reference/introduction).

## What you can do

* **Discover datasets** — list the queryable datasets and inspect each one's field catalog before you write a definition.
* **Run reports synchronously** — post an inline definition (or run a saved one) and get columns and rows straight back, capped at 10,000 rows.
* **Save definitions** — store a definition under a name unique to your organisation, then re-run, export, or schedule it by ID.
* **Export large result sets** — queue an asynchronous export to CSV or JSONL for up to 5,000,000 rows and download it from a presigned URL.
* **Schedule recurring reports** — run a saved definition on a cron schedule and deliver the export by signed webhook or email.
* **Audit every run** — list run history, newest first, including who ran what, in which mode, and whether sensitive fields were touched.

## Key concepts

* **Dataset** — a queryable view of payroll data, such as payslips, payslip lines, or payroll runs, plus satellite datasets covering statutory payments, court orders, holiday, payments, recurring line items, pensions, journals, invoices, and clients. `GET /datasets` lists them.
* **Field catalog** — every dataset field with its type and capabilities: whether it is filterable, groupable, which aggregations it supports, and whether it is sensitive. Dotted field names (for example `employee.department`) reach related entities. Access to sensitive fields is recorded in the run audit trail.
* **Report definition** — the JSON document describing a report: the `dataset`, 1–50 `select` entries, an optional `filters` tree, `groupBy`, `orderBy`, and a `limit`. One contract drives sync runs, exports, saved definitions, and schedules.
* **Run** — one execution of a definition, in `SYNC`, `EXPORT`, or `SCHEDULED` mode. Every run lands in the history served by `GET /runs`.
* **Export** — an asynchronous run that writes the result set to a file (CSV or JSONL) and hands you a time-limited download URL.
* **Schedule** — a cron expression attached to a saved definition, with a delivery target (a webhook and/or email recipients — `emails` as the To line, plus optional `cc` and `bcc`) and an output format.

## API structure

### Datasets

Start here: `GET /datasets` returns the dataset summaries, and `GET /datasets/{name}` returns the full field catalog for one dataset — the names, types, and capabilities your report definition is validated against.

### Run reports

`POST /reports/run` executes an inline report definition synchronously and returns the result set: typed columns, rows, a row count, and the execution time. Synchronous runs are capped at 10,000 rows — the response's `truncated` flag tells you the cap was hit, and the fix is an export. A definition that takes too long returns `408`; narrow the filters or use an export instead. `POST /report-definitions/{id}/run` does the same for a saved definition.

### Report definitions

Save a definition with `POST /report-definitions` (the name must be unique within your organisation), list them with `GET /report-definitions`, and retrieve, replace, or delete one with `GET`, `PUT`, and `DELETE` on `/report-definitions/{id}`. Deleting refuses with `409` while schedules still reference the definition — delete the schedules first.

### Exports

`POST /exports` queues an asynchronous export of either a saved definition (`definitionId`) or an inline `definition` — exactly one of the two — in `CSV` or `JSONL` format, with a row cap of 5,000,000. The request is accepted with status `QUEUED`; poll `GET /exports/{id}` until the status reaches `COMPLETED`, then follow the presigned `downloadUrl`. Download URLs expire after 15 minutes — refetch the export to re-issue one.

### Schedules

`POST /report-definitions/{definitionId}/schedules` runs a saved definition on a 5-field cron expression (timezone defaults to `Europe/London`) and delivers the export by webhook, email, or both. Email delivery sends to the `emails` (To) recipients, with optional `cc` and `bcc`. Webhook deliveries are signed with HMAC-SHA256 in the `X-Flow-Signature` header; the per-schedule `webhookSecret` used to verify it is returned exactly once, in the creation response, so store it then. List a definition's schedules with `GET /report-definitions/{definitionId}/schedules` and remove one with `DELETE /schedules/{id}`.

### Run history

`GET /runs` returns the organisation's run history, most recent first — every synchronous run, export, and scheduled run, with its status, row count, requester, and timestamps. It doubles as the audit trail: runs that touched sensitive fields are recorded here.

## The report definition document

A definition selects from one dataset and combines five building blocks:

* **`select`** — 1–50 entries. Each entry is a plain field (`{"field": "employee.department"}`), an aggregation (`{"aggregate": "sum", "field": "amount", "as": "total"}` — `sum`, `avg`, `min`, `max`, or `count`, with optional `distinct` on a counted field), or a tag-value extraction from a tags field.
* **`filters`** — a tree of `and` / `or` / `not` branches (up to 3 levels deep) over field leaves. Operators: `eq`, `neq`, `in`, `gt`, `gte`, `lt`, `lte`, `between`, `contains` (case-insensitive text), `isNull`, and `inPeriod`. Values are type-checked against the field.
* **`groupBy`** — required as soon as any select entry aggregates; every plain select entry must then appear in it.
* **`orderBy`** — sort by any selected field or aggregation alias, ascending or descending.
* **`limit`** — clamped to 10,000 for synchronous runs and 5,000,000 for exports.

Invalid definitions come back as `400` with a JSON-pointer `path` for each problem, so you can point the user at the exact offending entry.

### Relative periods

`inPeriod` on a date field takes a relative-period token — `@currentWeek`, `@currentMonth`, or `@currentTaxYear` — and expands it to the full range at run time. Scheduled runs resolve the token against the schedule's fire date, so a saved "this month's payroll" report tracks the moving period without ever being edited.

### Payslip status scope

Unless a definition filters on `status` explicitly, an implicit `status = "FINAL"` filter is applied — draft (Preview) figures never leak into a report by accident. To report on an unlocked run's drafts, filter for `PREVIEW` (or `in` both). The response's `statusScope` echoes which scope applied.

## Typical workflow

1. Call `GET /datasets`, then `GET /datasets/{name}`, to find the dataset and fields you need.
2. Iterate on a definition with `POST /reports/run` until the columns and totals look right.
3. Save it with `POST /report-definitions` once it's stable.
4. For the full result set, queue `POST /exports` and poll `GET /exports/{id}` for the download URL.
5. To receive it recurrently, create a schedule with `POST /report-definitions/{definitionId}/schedules` and verify deliveries with the returned `webhookSecret`.
6. Review what ran — and who ran it — with `GET /runs`.

## Notes and best practices

* Decimal values arrive as strings in the response rows — parse them with a decimal-safe type, not a float.
* If a synchronous response has `truncated: true`, don't page around it — the export path exists precisely for that result set.
* The organisation is always taken from your credentials. `organisation_id` is not a selectable or filterable field, and it cannot be overridden in a definition.
* YTD fields can't be aggregated with `sum` or `avg` (only `min`/`max`) — summing running totals double-counts.
* Tags fields (arrays of `group:name:value` strings) only support `contains` and `isNull` filters, and aren't groupable — extract a tag's value into a column via a select entry to group by it.

<Tip>
  Build the definition once and reuse it everywhere: the exact JSON you refined with `POST /reports/run` is what you save, export, and schedule — no translation between "preview" and "production" shapes.
</Tip>

## Authentication

Every request must carry a valid token in the `X-Auth-Token` header, and credentials with access to more than one organisation must also send `X-Org-Id` — see [Authentication](/api-reference/authentication).
