Pulling Reports for Large Companies
Use the Report Run API to generate payroll reports asynchronously for companies with large amounts of payroll data
Check's standard report endpoints, like Get payroll journal and Get payroll summary, generate reports synchronously: the report is built while the HTTP request is open, and the data is returned in the response. This works well for most companies, but for large companies — those with thousands of employees, or long payroll histories spanning many paydays — synchronous generation can take longer than a single HTTP request should be held open, leading to slow responses or timeouts.
The Report Run API solves this by generating reports asynchronously. Instead of waiting on a single long-lived request, you create a report run, let Check generate the report in the background, and download the finished artifact when it's ready. This makes report runs the recommended way to pull reports for large companies.
The Report Run API currently supports two report types:
payroll_journal— lists employees' earnings, deductions, and taxes by payroll for a payday range.payroll_summary— lists employees' earnings, deductions, and taxes aggregated over a payday range.
The report run lifecycle
A report run is a report generation job that moves through a simple lifecycle:
generating— the run was created and Check is building the report.completed— the report finished generating and can be downloaded fromresult.url.failed— the report could not be generated.
Report generation may take up to 20 minutes, though most reports complete much faster. Note that the data report runs rely on may be up to 10 minutes out of date, so a payroll approved moments ago may not appear in a report run created immediately afterward.
Step 1: Create a report run
Create a report run by POSTing to /report_runs with the report type and its parameters. For large companies, scope the run to the company and the payday range you need:
curl -X POST https://api.checkhq.com/report_runs \
-H "Authorization: Bearer {api_key}" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: 9f2b8c1e-payroll-journal-2025-q1" \
-d '{
"report": "payroll_journal",
"company": "com_sx3svU6K8c5ZkSFlOh5p",
"parameters": {
"payday_from": "2025-01-01",
"payday_to": "2025-03-31",
"contractors": true,
"additional_columns": ["employee.id", "payroll.id"]
}
}'The response is a report run in the generating status:
{
"id": "run_Bw6EkMDmxCPeVQ2eYzLu",
"report": "payroll_journal",
"status": "generating",
"company": "com_sx3svU6K8c5ZkSFlOh5p",
"created_at": "2026-06-24T18:26:56.848920Z",
"completed_at": null,
"parameters": {
"payday_from": "2025-01-01",
"payday_to": "2025-03-31"
},
"result": null,
"metadata": {}
}A few tips for creating report runs at scale:
- Always supply an
X-Idempotency-Keyheader. If your create request fails with a network error, you can safely retry with the same key without starting a duplicate generation job. - Include
additional_columnslikeemployee.idorpayroll.idif you plan to join the report output against records in your own system. For large companies, matching rows by employee name is error-prone; matching by ID is not. You can also includeemployee.metadata.<key>columns to pull through metadata you've stored on Check employee records, such as your own internal employee identifiers. - Use
metadatato tag runs with identifiers from your own system (for example, the internal request or customer ID that triggered the report) so you can correlate runs when listing them later.
Respect the rate limits
Report run creation is rate limited to 1 request per second per partner, with a maximum of 20 report runs generating at once. If you generate reports for many large companies on a schedule — for example, quarter-end journals for every customer — queue the create requests on your side and submit them serially rather than fanning out in parallel. A run leaves the concurrency pool when it reaches completed or failed, so a simple worker that tops the pool back up as runs finish will keep throughput at the maximum without hitting limits.
Step 2: Wait for the run to complete
Poll Get a report run until status is completed or failed:
curl https://api.checkhq.com/report_runs/run_Bw6EkMDmxCPeVQ2eYzLu \
-H "Authorization: Bearer {api_key}"Because generation for a large company can take several minutes, poll at a relaxed interval — every 30 to 60 seconds is plenty — and use a backoff rather than a tight loop. You can also listen for report_run webhooks to be notified when the run's status changes, which avoids polling entirely.
If the run reaches the failed status, create a new run to retry. For very large payday ranges, consider splitting the range into smaller windows (see below) before retrying.
Step 3: Download the report
Once the run is completed, the run's result.url points to the download endpoint. Calling it returns a short-lived presigned URL for the generated artifact:
curl https://api.checkhq.com/report_runs/run_Bw6EkMDmxCPeVQ2eYzLu/download \
-H "Authorization: Bearer {api_key}"{
"download_url": "https://...",
"extension": "zip",
"content_type": "application/zip"
}Download the file from download_url promptly — presigned URLs are short-lived, though you can always call the download endpoint again to get a fresh one. Report artifacts themselves also expire eventually: runs whose artifacts have expired are omitted from list results and can no longer be downloaded. If you need to retain reports long-term, store the downloaded artifact in your own system rather than relying on re-downloading from Check.
Strategies for very large reports
For most companies, a single report run per reporting period is all you need. For the largest companies, a few additional strategies help keep report generation fast and the output manageable:
Split long date ranges into smaller windows
Rather than requesting a full year of payroll journal data in one run, create one run per month or per quarter. Smaller windows generate faster, produce smaller artifacts, and let you retry a single window if a run fails, instead of regenerating the entire year. Since paydays are the boundary (payday_from and payday_to are both inclusive), make sure consecutive windows don't overlap — for example, 2025-01-01 to 2025-03-31 followed by 2025-04-01 to 2025-06-30.
You can create runs for multiple windows concurrently (up to the 20-run concurrency limit) and download each as it completes.
Scope runs to a single payroll when you can
If you only need data for one payroll — for example, to reconcile a specific pay period — pass the payroll parameter instead of a wide payday range. This keeps generation time and artifact size to a minimum.
Track your runs with list filters
List report runs supports filtering by company, report_type, and status. When orchestrating many runs across many companies, filter by status=generating to see what's still in flight, or by company to find the latest completed run for a given customer.
When to use report runs vs. synchronous reports
Both the synchronous report endpoints and the Report Run API produce the same underlying reports. As a rule of thumb:
- Use the synchronous endpoints for small companies and interactive use cases, where a user is waiting on the response and the data volume is modest.
- Use report runs for large companies, wide date ranges, scheduled or bulk report generation, and any workflow where a request timeout would be disruptive.
If you're unsure, report runs are the safer default: they never time out on data volume, they're retryable via idempotency keys, and the artifact can be re-downloaded until it expires.
Updated about 11 hours ago

