# Multi-page financial report
Source: https://docs.file.ai/examples/financial-report

> Pull the reporting period, headline figures, segment tables and chart readings out of a 60-page annual report.

Examples

# Multi-page financial report

Copy pageCopy page

Pull the reporting period, headline figures, segment tables and chart readings out of a 60-page annual report.

Copy pageCopy page

## 

[​

](#the-job)

The job

You receive a 60-page annual report as a PDF. You want the reporting period, total revenue and the auditor’s opinion as fields, the revenue-by-segment table as rows, and a reading of the key chart, without anyone paging through the file.

## 

[​

](#sample-input)

Sample input

`annual-report-2025.pdf`, 60 pages:

*   **Pages 1–4:** cover with the company logo, contents, and the chair’s letter with a signature.
*   **Pages 5–18:** narrative sections with photos of sites and products, and a bar chart of revenue by quarter.
*   **Pages 19–44:** financial statements. Dense tables with multi-level column headers (current year, prior year, and change, under each segment), subtotals, and footnote markers that point to notes at the foot of the page.
*   **Pages 45–56:** notes to the accounts, mostly paragraphs and small tables.
*   **Pages 57–60:** the independent auditor’s report, scanned from paper, with a signature and the firm’s stamp.

Every page carries a running header with the company name and a footer with the page number. Download the file to run this example yourself:

## annual-report-2025.pdf

60 pages, 1.5 MB. The company, people and figures are made up.

Your output might be a little different from the [Result](#result) below. Field names and the number of fields depend on the AI Schema in your workspace, but values such as the dates, totals and table rows should be close.

## 

[​

](#how-filescout-processes-it)

How fileScout processes it

Illustrative: the element breakdown is internal; the API returns the structured values.

fileScout reads the file as a set of elements and handles each kind on its own path, all within the same file:

Element type

Example in this report

How fileScout handles it

`heading`

”Consolidated income statement”

Gives structure to the content beneath it.

`paragraph`

The chair’s letter, the notes to the accounts

Read as text; labelled values in it become fields.

`table`

Revenue by segment, with multi-level headers and subtotals

Read as a table and returned as rows of named columns.

`list`

Key risks in a bulleted list

Read as text; each labelled value becomes a field.

`form_field`

”Reporting period: 1 January to 31 December 2025”

A label and its value, returned as a field.

`graph`

Bar chart of revenue by quarter

A visual element: described as one field (chart type, axes, series). If the same figures also appear as a printed table, both are kept.

`image`

Site and product photos

A visual element: described as one field.

`logo`

Company logo on the cover

A visual element: described as one field.

`signature`

Chair’s and auditor’s signatures

A visual element: who signed, and whether it is dated.

`handwriting`

A handwritten initial on a scanned page

A visual element: the transcribed text.

`stamp`

The audit firm’s stamp

A visual element: described as one field.

`qr_code`

A QR code linking to the online report

A visual element: described as one field.

`other`

Anything that fits none of the above

Read as text.

Running headers and footers, such as the company name and page numbers on every page, carry no data. fileScout skips them, so they cost no tokens. Long files like this one are read in chunks; spreadsheets take a separate path of their own. The fields you get back are the ones your [AI Schema](/extract/ai-schemas) defines. The breakdown above is how fileScout finds them; it is not part of the response.

## 

[​

](#steps)

Steps

The cURL commands use two environment variables:

```
export FILEAI_API_KEY="your-api-key"
export FILEAI_BASE_URL="https://api.orion.file.ai/prod/v1"   # or your regional host
```

See [Regions](/authentication#regions) for regional hosts.

1

Request an upload URL

Send the file name, its MIME type in `fileType`, and `enabledProducts: ["FILE_SCOUT"]`. fileScout needs normal processing, so leave `storageOnly` out. See [Choose products](/ingest/uploads#choose-products) for the rules.

cURL

Python

Node.js

```
curl -X POST "$FILEAI_BASE_URL/files/upload" \
  -H "x-api-key: $FILEAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fileName": "annual-report-2025.pdf",
    "fileType": "application/pdf",
    "enabledProducts": ["FILE_SCOUT"]
  }'
```

```
import os, time, requests

BASE = "https://api.orion.file.ai/prod/v1"
HEADERS = {"x-api-key": os.environ["FILEAI_API_KEY"]}

upload = requests.post(
    f"{BASE}/files/upload",
    headers=HEADERS,
    json={
        "fileName": "annual-report-2025.pdf",
        "fileType": "application/pdf",
        "enabledProducts": ["FILE_SCOUT"],
    },
)
upload.raise_for_status()
upload = upload.json()
```

```
import { readFile } from "node:fs/promises";

const BASE = "https://api.orion.file.ai/prod/v1";
const headers = { "x-api-key": process.env.FILEAI_API_KEY };

const upload = await fetch(`${BASE}/files/upload`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    fileName: "annual-report-2025.pdf",
    fileType: "application/pdf",
    enabledProducts: ["FILE_SCOUT"],
  }),
}).then((r) => r.json());
```

Keep `uploadId` and `presignedUploadURL` from the response. `enabledProducts` works only on this single upload route and in workspaces enabled for it; otherwise the request returns `422`.

2

PUT the file

The URL is valid for **300 seconds**. Send the same `Content-Type` you gave as `fileType`.

cURL

Python

Node.js

```
curl -X PUT "<presignedUploadURL>" \
  -H "Content-Type: application/pdf" \
  --data-binary @annual-report-2025.pdf
```

```
with open("annual-report-2025.pdf", "rb") as f:
    requests.put(
        upload["presignedUploadURL"],
        data=f,
        headers={"Content-Type": "application/pdf"},
    ).raise_for_status()
```

```
await fetch(upload.presignedUploadURL, {
  method: "PUT",
  headers: { "Content-Type": "application/pdf" },
  body: await readFile("annual-report-2025.pdf"),
});
```

3

Wait for processing

Poll `GET /v1/uploads/{uploadId}` until every entry in `files` is `processed` or `action_required`, or wait for a [processing callback](/deliver/callbacks). A 60-page file takes longer than a one-page invoice, so poll patiently: in our test run, the sample file took about 7 minutes.

cURL

Python

Node.js

```
curl "$FILEAI_BASE_URL/uploads/<uploadId>" \
  -H "x-api-key: $FILEAI_API_KEY"
```

```
while True:
    status = requests.get(f"{BASE}/uploads/{upload['uploadId']}", headers=HEADERS).json()
    if any(s["step"] == "processing_failed" for s in status.get("statuses", [])):
        raise RuntimeError("processing failed")
    files = status.get("files", [])
    if files and all(f["status"] in ("processed", "action_required") for f in files):
        break
    time.sleep(10)

file_id = files[0]["fileId"]
```

```
let status;
while (true) {
  status = await fetch(`${BASE}/uploads/${upload.uploadId}`, { headers }).then((r) => r.json());
  if ((status.statuses ?? []).some((s) => s.step === "processing_failed")) throw new Error("processing failed");
  const files = status.files ?? [];
  if (files.length && files.every((f) => ["processed", "action_required"].includes(f.status))) break;
  await new Promise((r) => setTimeout(r, 10000));
}

const fileId = status.files[0].fileId;
```

The report is one document, so `files` has one entry. If you upload with `isSplit: true`, one file can become several documents; iterate over `files` in that case.

4

Read the values, with every table row

Pass `includeFullTableData=true` so tables over 100 rows come back whole.

cURL

Python

Node.js

```
curl "$FILEAI_BASE_URL/files/<fileId>/values?includeFullTableData=true" \
  -H "x-api-key: $FILEAI_API_KEY"
```

```
values = requests.get(
    f"{BASE}/files/{file_id}/values",
    headers=HEADERS,
    params={"includeFullTableData": "true"},
).json()

form = values["formValues"][0]
fields = {f["title"]: f for f in form["fields"]}
segments = [
    {cell["title"]: cell["value"] for cell in row}
    for row in fields["revenue_by_segment"]["valueArray"]
]
```

```
const values = await fetch(
  `${BASE}/files/${fileId}/values?includeFullTableData=true`,
  { headers },
).then((r) => r.json());

const form = values.formValues[0];
const fields = Object.fromEntries(form.fields.map((f) => [f.title, f]));
const segments = fields["revenue_by_segment"].valueArray.map((row) =>
  Object.fromEntries(row.map((cell) => [cell.title, cell.value])),
);
```

## 

[​

](#result)

Result

Captured from a real run of the sample file in October 2026, with ids anonymised. The response had 106 fields; five are shown here. Your output might be a little different: the fields come from the AI Schema in your workspace, and extracted values can vary slightly between runs.

GET /v1/files/{fileId}/values?includeFullTableData=true

```
{
  "formValues": [
    {
      "id": "3cd4e2e5142b5c1153877479",
      "createdAt": "2026-10-22T04:47:29.627Z",
      "updatedAt": "2026-10-22T04:47:29.903Z",
      "schemaRelationshipId": "ed73bec713f370ad401c8e31",
      "storageType": "default",
      "fileId": "9b2e4c71-0d3a-4f5e-8c21-6a7f1e0b9d42",
      "fileName": "annual_report_2025.pdf",
      "fileType": "application/pdf",
      "schemaName": null,
      "fields": [
        {
          "id": "c4a516abfc737c90f5c3f70c",
          "title": "reporting_period_start",
          "description": "Start date of the reporting period covered by the report",
          "type": "string",
          "valueArray": [],
          "value": "2025-01-01",
          "valueS3Uri": null,
          "items": null,
          "enumValues": [],
          "format": "date-time",
          "internet_context": null,
          "fieldReferenceId": null,
          "fieldReference": null
        },
        {
          "id": "2536f92f7809d732a8a0de28",
          "title": "reporting_period_end",
          "description": "End date of the reporting period covered by the report",
          "type": "string",
          "valueArray": [],
          "value": "2025-12-31",
          "valueS3Uri": null,
          "items": null,
          "enumValues": [],
          "format": "date-time",
          "internet_context": null,
          "fieldReferenceId": null,
          "fieldReference": null
        },
        {
          "id": "3082b7d16f6e4b64489b99ac",
          "title": "group_revenue",
          "description": "Group revenue for the year (consolidated)",
          "type": "number",
          "valueArray": [],
          "value": 412850000,
          "valueS3Uri": null,
          "items": null,
          "enumValues": [],
          "format": null,
          "internet_context": null,
          "fieldReferenceId": null,
          "fieldReference": null
        },
        {
          "id": "3c95eb562acc17f5cab075db",
          "title": "audit_opinion_type",
          "description": "Type of audit opinion expressed",
          "type": "enum",
          "valueArray": [],
          "value": "unqualified",
          "valueS3Uri": null,
          "items": null,
          "enumValues": ["unqualified", "qualified", "adverse", "disclaimer"],
          "format": null,
          "internet_context": null,
          "fieldReferenceId": null,
          "fieldReference": null
        },
        {
          "id": "58285cd525563dcb1da492cf",
          "title": "revenue_by_segment",
          "description": "Revenue and operating profit by segment for 2025 and 2024 including percentage changes",
          "type": "list",
          "valueArray": [
            [
              {"title": "segment", "value": "retail", "type": "enum"},
              {"title": "revenue_2025", "value": 238400000, "type": "number"},
              {"title": "revenue_2024", "value": 221900000, "type": "number"},
              {"title": "revenue_change_percent", "value": 7.4, "type": "number"},
              {"title": "operating_profit_2025", "value": 33104000, "type": "number"},
              {"title": "operating_profit_2024", "value": 27480000, "type": "number"},
              {"title": "operating_profit_change_percent", "value": 20.5, "type": "number"}
            ],
            [
              {"title": "segment", "value": "wholesale", "type": "enum"},
              {"title": "revenue_2025", "value": 174450000, "type": "number"},
              {"title": "revenue_2024", "value": 168300000, "type": "number"},
              {"title": "revenue_change_percent", "value": 3.7, "type": "number"},
              {"title": "operating_profit_2025", "value": 23179500, "type": "number"},
              {"title": "operating_profit_2024", "value": 20283000, "type": "number"},
              {"title": "operating_profit_change_percent", "value": 14.3, "type": "number"}
            ],
            [
              {"title": "segment", "value": "total", "type": "enum"},
              {"title": "revenue_2025", "value": 412850000, "type": "number"},
              {"title": "revenue_2024", "value": 390200000, "type": "number"},
              {"title": "revenue_change_percent", "value": 5.8, "type": "number"},
              {"title": "operating_profit_2025", "value": 56283500, "type": "number"},
              {"title": "operating_profit_2024", "value": 47763000, "type": "number"},
              {"title": "operating_profit_change_percent", "value": 17.8, "type": "number"}
            ]
          ],
          "value": null,
          "valueS3Uri": "s3://.../forms/revenue_by_segment.csv",
          "items": [
            {"id": "141c14529a000209b7b15782", "title": "segment", "description": "Business segment name", "type": "enum", "format": null, "internet_context": null, "enumValues": ["retail", "wholesale", "total"]},
            {"id": "d7f25c077b0cbc6c3ef3ba26", "title": "revenue_2025", "description": "Segment revenue for 2025", "type": "number", "format": null, "internet_context": null, "enumValues": []},
            {"id": "9896d83e5e8e405572653276", "title": "revenue_2024", "description": "Segment revenue for 2024", "type": "number", "format": null, "internet_context": null, "enumValues": []},
            {"id": "655095a435e1ed7ad2f4d9e2", "title": "revenue_change_percent", "description": "Percentage change in revenue", "type": "number", "format": null, "internet_context": null, "enumValues": []},
            {"id": "854b3bad300b22be57a871c3", "title": "operating_profit_2025", "description": "Segment operating profit for 2025", "type": "number", "format": null, "internet_context": null, "enumValues": []},
            {"id": "6b718ab0da611cd052b6b329", "title": "operating_profit_2024", "description": "Segment operating profit for 2024", "type": "number", "format": null, "internet_context": null, "enumValues": []},
            {"id": "e9a184e2e09e243d7d921d20", "title": "operating_profit_change_percent", "description": "Percentage change in operating profit", "type": "number", "format": null, "internet_context": null, "enumValues": []}
          ],
          "enumValues": [],
          "format": null,
          "internet_context": null,
          "fieldReferenceId": null,
          "fieldReference": null
        }
      ]
    }
  ],
  "count": 1,
  "currentPage": 1
}
```

*   **Field titles come from the schema.** This workspace had no schema for annual reports, so fileAI generated one from the file, with snake\_case titles such as `group_revenue` and a `description` for each field. Your own [AI Schema](/extract/ai-schemas) decides the titles you get back, so look fields up by the titles you defined.
*   **Scalar fields** carry their answer in `value`. `group_revenue` is a JSON number: the printed `412,850,000` is read as `412850000`. Reports often print figures in thousands or millions, so say in the field’s description which unit you want. See [Extraction prompts](/extract/extraction-prompts).
*   **`audit_opinion_type` is an `enum` field.** Its `value` is always one of `enumValues`, here `unqualified`.
*   **`revenue_by_segment` is a `list` field.** Its `value` is `null`, its columns are described in `items`, and its rows are in `valueArray`, each row an array of cells with their own `title`, `type` and `value`. The two-level header (Revenue, then 2025, 2024 and Change) comes back as flat columns such as `revenue_2025` and `revenue_change_percent`. `valueS3Uri` points to fileAI’s stored copy of the table; read the rows from `valueArray`. See [Tables](/extract/data-types#tables).
*   **Printed totals come back as rows.** The table’s Total line is the third row, with `segment` set to `total`. Drop it before you add up the segments.
*   **Printed formatting is cleaned up.** Footnote markers such as `Retail (1)` come back as `retail`, and `+7.4%` comes back as the number `7.4`. Amounts printed in brackets come back negative: the balance sheet’s `(186,130,000)` total liabilities is `-186130000`.
*   **Empty cells come back as `null`**, and so does a number that is `0`, in fields and in table cells. Treat `null` in a number field as zero or missing according to your own rules. See [Data types](/extract/data-types).
*   **No field describes the bar chart**, because the schema didn’t ask for one. The quarterly figures came back from the printed table on page 29 instead, as `segment_revenue_by_quarter`. If you need a reading of a chart, add a field for it to your schema.
*   **`fileName` can differ from the name you uploaded.** The file was uploaded as `annual-report-2025.pdf` and came back as `annual_report_2025.pdf`. Match results to uploads by `fileId`.

## 

[​

](#what-it-shows)

What it shows

*   One upload handles a long, mixed file: tables, text, charts, photos and scanned pages are each read on their own path, so you don’t pre-sort pages.
*   Page furniture is skipped, so running headers and footers never turn into fields.
*   Long tables need `includeFullTableData=true`. Without it a table over 100 rows returns its first 100 and no error.
*   A chart is only described when your schema has a field for it. When you need exact figures, take them from a printed table.

## 

[​

](#next-steps)

Next steps

## AI Schemas

Define the fields and tables fileScout fills.

## Data types

Tables, `valueArray` and how values are typed.

## Bank statement extraction

Long tables and a totals check.

## Uploads

Every upload option, including `enabledProducts`.

[Previous](/deliver/incremental-loading)[

Bank statements

Turn a multi-page bank statement into a transactions table and balances, including statements with more than 100 rows.

Next

](/examples/bank-statement-extraction)

⌘I

## On this page

Back to top

*   [The job](#the-job)
*   [Sample input](#sample-input)
*   [How fileScout processes it](#how-filescout-processes-it)
*   [Steps](#steps)
*   [Result](#result)
*   [What it shows](#what-it-shows)
*   [Next steps](#next-steps)
