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

# Report Exports

> Generate financial reports as PDF, CSV, or Excel files, then poll and download them

Use [Create export](/docs/api-reference/reports/create-export) to generate an income
statement, balance sheet or cash flow file. The request returns an `export_` ID
immediately; [Retrieve export](/docs/api-reference/reports/retrieve-export) tells you
when it is ready and returns a signed download URL.

Both operations require **API access: Manage** and **Reports: View**. The key
selects the company. Any currently authorized teammate may retrieve an API
export in that company. Generating an export does not edit your books.

## Choose the report

All four examples below use `POST /api/v1/reports/exports`. Send a different
`Idempotency-Key` for each new job.

| Report | `report_type` | `group_by.rows` |
| - | - | - |
| Income statement | `income_statement` | `category` |
| Balance sheet | `balance_sheet` | `category` |
| GAAP department P\&L | `income_statement` | `department` |
| Cash flow | `cash_flow` | `category` |

Income statement:

```json theme={null}
{"report_type":"income_statement","start_date":"2026-01-01","end_date":"2026-09-30","format":"pdf","group_by":{"period":"month","rows":"category"}}
```

Balance sheet:

```json theme={null}
{"report_type":"balance_sheet","start_date":"2026-01-01","end_date":"2026-09-30","format":"xlsx","group_by":{"period":"month","rows":"category"}}
```

GAAP department P\&L:

```json theme={null}
{"report_type":"income_statement","start_date":"2026-01-01","end_date":"2026-09-30","format":"csv","group_by":{"period":"quarter","rows":"department"}}
```

Cash flow:

```json theme={null}
{"report_type":"cash_flow","start_date":"2024-01-01","end_date":"2025-12-31","format":"pdf","group_by":{"period":"year","rows":"category"}}
```

### Fields and options

| Field | Options and meaning |
| - | - |
| `report_type` | Required. `income_statement` is profit and loss; `balance_sheet` compares cumulative balances; `cash_flow` shows operating, investing and financing activity. |
| `start_date`, `end_date` | Required inclusive `YYYY-MM-DD` dates. End must be on or after start. |
| `format` | `pdf` for a formatted document, `csv` for a comma-separated table, or `xlsx` for an Excel workbook. Defaults to `pdf`. |
| `group_by.period` | `month`, `quarter`, or `year` time columns. Defaults to `month`. |
| `group_by.rows` | `category` for accounting categories, or `department` for the GAAP department income statement. Defaults to `category`. |

`group_by` is an object; omitted members take their defaults. Quarter/year
columns follow the company's existing calendar/fiscal report settings. First
and last columns can be partial periods. There is no `date_type` override.

Balance sheets need a **range**, too. Each displayed period shows cumulative
balances through its cutoff, retaining activity before `start_date`.
Department rows work only for `income_statement` and require the departments
feature. Without it, the request returns `403 feature_disabled`.

Ranges are limited to **120 calendar months**, counting both boundary months,
regardless of format or grouping. PDF exports additionally support **13 displayed
periods**, using the company's calendar/fiscal report settings. For more columns,
use a shorter range, coarser grouping, CSV or XLSX. Oversized ranges return
`400 invalid_date_range` with `param: "end_date"` before creating or queuing a job.
Unknown fields are rejected, including
`email_on_completion`, `grouped_by`, a single `date`, or a company selector.
Company, actor and filename are inferred by Finta.

## Submit an export

```bash theme={null}
curl --fail-with-body https://app.finta.com/api/v1/reports/exports \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: income-statement-2026-q1-attempt-1' \
  -d '{"report_type":"income_statement","start_date":"2026-01-01","end_date":"2026-03-31","format":"pdf","group_by":{"period":"month","rows":"category"}}'
```

Creation and same-key replay return `201 Created`, with `Location` pointing to
the retrieval path and `Cache-Control: no-store`.

```json theme={null}
{
  "id": "export_2b2dde33-f70d-4db2-ad61-03060e982fa7",
  "object": "report_export",
  "report_type": "income_statement",
  "start_date": "2026-01-01",
  "end_date": "2026-03-31",
  "format": "pdf",
  "group_by": {"period": "month", "rows": "category"},
  "status": "pending",
  "created": 1792022400,
  "started_at": null,
  "completed_at": null,
  "generation_duration_ms": null,
  "file": null,
  "error": null
}
```

A fast worker or a replay can return a later state immediately. Inspect
`status`; HTTP 201 does not guarantee that the job is new or still pending.

## Poll for completion

```bash theme={null}
curl --fail-with-body \
  https://app.finta.com/api/v1/reports/exports/export_2b2dde33-f70d-4db2-ad61-03060e982fa7 \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

Retrieval returns `200 OK` for every recognized job state:

| Status | Meaning | Next step |
| - | - | - |
| `pending` | Waiting for generation. An accepted request may still need dispatch recovery. | Poll with backoff. |
| `processing` | Generation has begun, including retry waits. | Poll with backoff. |
| `succeeded` | Generation, final access checks and file attachment finished. | Download `file.download_url` if present; fresh links stop 30 days after completion. |
| `failed` | Terminal access or generation failure. `file` is null. | Inspect `error`, fix the cause, and use a new key for a new job. |

Stop polling at success or failure. Every poll consumes the ordinary
[API allowance](/docs/api-reference/usage-limits), so use bounded backoff rather than
a tight loop. There is no guaranteed generation time, completion webhook,
email notification or MCP event subscription in this flow.

The worker reads the books when it generates the file; POST does not freeze a
financial snapshot. Completed files are retained results. To regenerate against
updated books, submit a new job with a new key.

## Download the file

Only `succeeded` responses expose a `file` object:

```json theme={null}
{
  "filename": "Example Company - Income statement - 2026-03-31.pdf",
  "content_type": "application/pdf",
  "size": 24576,
  "download_url": "https://storage.example.com/report.pdf?signature=example",
  "expires_at": 1792627220
}
```

The URL is illustrative. In a real response, `size` is bytes and `expires_at`
is Unix seconds. CSV uses `text/csv`; XLSX uses
`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`.

The client or agent fetches the returned URL with its own HTTP/file tools:

```bash theme={null}
# Assign the real file.download_url from your response.
DOWNLOAD_URL='YOUR_RETURNED_DOWNLOAD_URL'
curl --fail --location "$DOWNLOAD_URL" --output report.pdf
```

The signed storage URL does not need a Finta bearer token. The API supplies
the file; the caller decides where to save it on their computer.

Fresh links are available for **30 days after `completed_at`** (2,592,000 seconds).
At or after that cutoff, GET and idempotent POST replays still return
`status: "succeeded"`, but `file` and `error` are null. The export record and stored
file are retained. To generate another report from the current books, submit a
new export with a new key.

Within that window, successful retrieval requests a fresh link with expiry **seven days from
retrieval**, not from job creation or when the link is opened. Signing credentials
or storage policies can invalidate a link sooner. If it expires,
retrieve the export again before its 30-day cutoff. Already-issued links keep
their original expiry and can remain valid beyond the regeneration cutoff.
A download started before expiry can finish afterward;
restarting or requesting additional byte ranges afterward needs a fresh link.

Links are bearer credentials: anyone with one can download until it expires,
even if API permissions change in the meantime. Retrieval itself checks current
access. Link expiry does not delete the file; export records and files have no
automatic age-based cleanup policy in the application.

## Idempotency and retries

POST requires a nonblank, case-sensitive `Idempotency-Key` of 1–255 characters.
It is scoped to the company, submitting user and operation, across that user's
API keys. Identical normalized inputs replay the same export and current state,
including failures. Different inputs with that key return
[`409 idempotency_key_conflict`](/docs/api-reference/errors#idempotency_key_conflict).
Keys remain reserved for the lifetime of the record.

* After a timeout or `503 report_export_failed`, retry the same body/key.
  A job may already be queued; same-key retry repairs an interrupted dispatch
  without creating another record. The retained job stays pending rather than
  becoming a terminal enqueue failure.
* After a terminal failed job, fix the cause and use a **new** key to retry.
  The old key continues to replay that failure.
* For intentionally new work, including refreshing the same dates after the
  books change, use a new key.

### Export capacity

A company may have at most **five unfinished API exports**, across all
submitters. New submissions above that limit return
[`429 report_export_limit_exceeded`](/docs/api-reference/errors#report_export_limit_exceeded)
without creating or enqueueing a job. Existing-key replays and GET polling
remain available, subject to ordinary API limits. A terminal success or failure
releases its slot.

This capacity error has no `Retry-After`: the finish time is unknown. Poll
existing jobs with backoff and retry the rejected submission after one finishes.
It is separate from burst/monthly quotas. For ordinary `rate_limit_exceeded`,
honor `Retry-After`; for `monthly_limit_exceeded`, stop short-term retries.

### Headers

| Header | Type | Meaning |
| - | - | - |
| `Idempotency-Key` | Request string | Required on POST; not required on GET. |
| `Location` | Response string | Retrieval path on successful POST creation/replay; GET does not return it. |
| `Cache-Control` | Response string | `no-store` on successful export responses. |
| `Retry-After` | Response integer seconds | Ordinary burst-limit errors only; absent on export-capacity 429 and dispatch 503. |

Standard `X-RateLimit-*` headers describe API requests, not export slots. Their
availability and reset behavior are documented under
[Usage Limits](/docs/api-reference/usage-limits#headers).

## Failures and metrics

An HTTP 200 with `status: "failed"` contains a job error with `code`/`message`,
not the HTTP error envelope's `type`/`doc_url` fields:

* `access_revoked`: the submitter lost required access before generation finished.
* `generation_failed`: generation could not finish after retries.
* `enqueue_failed`: a compatibility value in the declared job-error schema.
  New dispatch uncertainty returns HTTP 503 with a pending job, rather than
  recording this terminal code.

`started_at` is the first generation-attempt timestamp and can remain null when
access is revoked before generation. `completed_at` is terminal
completion, for success or failure. Both are Unix seconds and nullable before
their events. `generation_duration_ms` is nullable until an attempt finishes,
then accumulates generation/render/upload attempt time, including failed attempts
but excluding queue and retry waits. It is not total time since submission.

Finta records export ID, status and whether a download link was issued in its
API request logs. Pending/failed responses and HEAD probes do not count as link
issuance. Issuing a link does not prove its bytes were downloaded; actual S3
downloads and download duration are not tracked.

Unknown or other-company IDs, browser-created exports and raw database IDs
return `404 resource_missing`. Department exports also require the feature on
retrieval. There is no export list, cancel or delete operation.

Use recognized schema values and inspect status before file/error. Treat an
unknown future state as unrecognized, rather than assuming success. File exports
use the `export_` resource; they are separate from transaction `job_` resources.
The JSON income-statement endpoint keeps its scalar `group_by=category|department`;
this new export body uses `group_by: {period, rows}`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.