Skip to main content
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.
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. All endpoints sit under the /v1/report base path, relative to the environment base URLs documented on the 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.
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.

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.