# Insurance claim evidence pack
Source: https://docs.file.ai/examples/claim-evidence-pack

> Check every file in a claim with fileForensics, extract the forms with fileScout, and combine the findings into one routing decision.

Examples

# Insurance claim evidence pack

Copy pageCopy page

Check every file in a claim with fileForensics, extract the forms with fileScout, and combine the findings into one routing decision.

Copy pageCopy page

## 

[​

](#the-job)

The job

A policyholder submits a motor claim with nine files. Before you pay, you want to know whether any of them was edited, generated by AI, or lifted from the web, and you want the claim form and repair estimate extracted at the same time. If anything looks wrong, the claim goes to an investigator with a list of what was found and where.

## 

[​

](#sample-input)

Sample input

File

What it is

`claim-form.pdf`

The signed claim form, a 3-page PDF.

`repair-estimate.pdf`

A garage’s repair estimate, a 2-page PDF.

`police-report.pdf`

A police report, scanned from paper to PDF.

`photo_1.jpg` … `photo_6.jpg`

Six JPEG photos of the damage, taken on a phone.

## 

[​

](#steps)

Steps

The examples use your API key from the environment:

```
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

Choose products for each file

Pick `enabledProducts` per file on `POST /v1/files/upload`. Both choices run fileForensics with **all 19 checks**, started once the file is stored:

Files

`enabledProducts`

`storageOnly`

Why

The photos

`["FILE_FORENSICS"]`

`true` (required)

You only need the integrity checks. The photos are stored, not read or extracted.

The three PDFs

`["FILE_SCOUT", "FILE_FORENSICS"]`

Left out (must not be `true`)

You want the forms’ values extracted as well as checked.

`enabledProducts` works only on the single upload route and in workspaces enabled for these products; otherwise the request returns `422`. See [Choose products](/ingest/uploads#choose-products).

2

Upload every file in the pack

Request an upload URL for each file and `PUT` the bytes to it within 300 seconds. Then poll `GET /v1/uploads/{uploadId}` until the file’s document is `processed` or `action_required`, and keep its `fileId`.

Python

Node.js

```
import os, time, requests
from concurrent.futures import ThreadPoolExecutor

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

PACK = [
    "claim-form.pdf", "repair-estimate.pdf", "police-report.pdf",
    "photo_1.jpg", "photo_2.jpg", "photo_3.jpg",
    "photo_4.jpg", "photo_5.jpg", "photo_6.jpg",
]

def upload(path):
    is_photo = path.endswith(".jpg")
    mime = "image/jpeg" if is_photo else "application/pdf"
    body = {"fileName": path, "fileType": mime}
    if is_photo:
        body |= {"enabledProducts": ["FILE_FORENSICS"], "storageOnly": True}
    else:
        body |= {"enabledProducts": ["FILE_SCOUT", "FILE_FORENSICS"]}

    res = requests.post(f"{BASE}/files/upload", headers=HEADERS, json=body)
    res.raise_for_status()
    up = res.json()
    with open(path, "rb") as f:
        requests.put(up["presignedUploadURL"], data=f,
                     headers={"Content-Type": mime}).raise_for_status()

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

```
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 sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const PACK = [
  "claim-form.pdf", "repair-estimate.pdf", "police-report.pdf",
  "photo_1.jpg", "photo_2.jpg", "photo_3.jpg",
  "photo_4.jpg", "photo_5.jpg", "photo_6.jpg",
];

async function upload(path) {
  const isPhoto = path.endsWith(".jpg");
  const mime = isPhoto ? "image/jpeg" : "application/pdf";
  const body = isPhoto
    ? { fileName: path, fileType: mime, enabledProducts: ["FILE_FORENSICS"], storageOnly: true }
    : { fileName: path, fileType: mime, enabledProducts: ["FILE_SCOUT", "FILE_FORENSICS"] };

  const res = await fetch(`${BASE}/files/upload`, {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${path}: upload request failed (${res.status})`);
  const up = await res.json();
  await fetch(up.presignedUploadURL, {
    method: "PUT",
    headers: { "Content-Type": mime },
    body: await readFile(path),
  });

  while (true) {
    const status = await fetch(`${BASE}/uploads/${up.uploadId}`, { headers }).then((r) => r.json());
    if ((status.statuses ?? []).some((s) => s.step === "processing_failed")) {
      throw new Error(`${path}: processing failed`);
    }
    const files = status.files ?? [];
    if (files.length && files.every((f) => ["processed", "action_required"].includes(f.status))) {
      return { path, fileId: files[0].fileId };
    }
    await sleep(5000);
  }
}
```

3

Wait for each file's run

Read each file’s run with `GET /v1/forensics/files/{file_id}`. It returns `404` until the run exists, then the run itself; poll until its `status` is `complete` or `error`. All the checks in a run execute at the same time, so a run takes about as long as its slowest check: `web_presence` can take up to about 180 seconds.

Python

Node.js

```
def forensics(path, file_id, timeout=600):
    deadline = time.time() + timeout
    while time.time() < deadline:
        res = requests.get(f"{BASE}/forensics/files/{file_id}", headers=HEADERS)
        if res.status_code != 404:            # 404: the run hasn't been created yet
            res.raise_for_status()
            run = res.json()
            if run["status"] in ("complete", "error"):
                return path, run
        time.sleep(10)
    return path, None                          # no finished run in time
```

```
async function forensics(path, fileId, timeoutMs = 600_000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const res = await fetch(`${BASE}/forensics/files/${fileId}`, { headers });
    if (res.status !== 404) {                  // 404: the run hasn't been created yet
      if (!res.ok) throw new Error(`${path}: ${res.status}`);
      const run = await res.json();
      if (["complete", "error"].includes(run.status)) return { path, run };
    }
    await sleep(10_000);
  }
  return { path, run: null };                  // no finished run in time
}
```

A run still `queued` or `running` after about 6 minutes is stuck; see [Retry if stuck](/verify/forensics#how-it-works).

4

Combine the results into one decision

Fan out over the pack, then decide. Any `issue` on any file sends the claim to an investigator, with the file and check named. A failed run or check holds the claim for a retry, because “no finding” from a check that didn’t run is not evidence.

Python

Node.js

```
with ThreadPoolExecutor(max_workers=len(PACK)) as pool:
    uploaded = list(pool.map(upload, PACK))
    runs = list(pool.map(lambda pf: forensics(*pf), uploaded))

issues, errors = [], []
for path, run in runs:
    if run is None or run["status"] == "error":
        errors.append((path, "run"))
        continue
    for check, result in run["forensics"].items():
        if result["status"] == "issue":
            issues.append((path, check, result.get("value")))
        elif result["status"] == "error":
            errors.append((path, check))

if issues:
    decision = "investigate"        # route to an investigator with `issues`
elif errors:
    decision = "hold"               # retry the failed checks, then decide
else:
    decision = "straight_through"   # every check reported none or skip
```

```
const uploaded = await Promise.all(PACK.map(upload));
const runs = await Promise.all(uploaded.map(({ path, fileId }) => forensics(path, fileId)));

const issues = [];
const errors = [];
for (const { path, run } of runs) {
  if (!run || run.status === "error") {
    errors.push({ path, check: "run" });
    continue;
  }
  for (const [check, result] of Object.entries(run.forensics ?? {})) {
    if (result.status === "issue") issues.push({ path, check, value: result.value });
    else if (result.status === "error") errors.push({ path, check });
  }
}

const decision = issues.length
  ? "investigate"        // route to an investigator with `issues`
  : errors.length
    ? "hold"             // retry the failed checks, then decide
    : "straight_through"; // every check reported none or skip
```

For the PDFs, read the extracted values with `GET /v1/files/{fileId}/values` as in the [financial report example](/examples/financial-report#steps).

## 

[​

](#which-checks-matter-for-which-file)

Which checks matter for which file

Every file gets all 19 checks. A check that has nothing to look at in a file usually reports `skip`; a few report `none` with a note in `value` instead (for example, `producer_divergence` on a photo). Both mean “nothing found here”, so treat them the same.

Check

Photos (JPEG)

PDFs

`copy_move`

Looks for cloned regions, such as one dent copied across a panel.

Usually `skip`: forms and tables repeat labels and borders by design, so images of documents and PDF pages are not analyzed.

`ai_gen_detection`

Looks for an AI-generated image.

Checks an image embedded in the PDF, such as a scanned page; `skip` when there is none.

`web_presence`

Looks for the photo on the web.

Checks an image taken from the PDF: the largest embedded image, or else the first page.

`exact_duplicate`

Looks for a byte-identical copy of a file seen before.

Same.

`provenance_credentials`, `generation_toolchain`

Reads content credentials and the tools that made the file.

Same.

`ai_pdf_detection`, `revision_history`, `producer_divergence`

`skip` or `none`: not a PDF.

Look for a generated PDF, later edits, or pages typeset by a different producer.

`ai_text_detection`

Nothing to check: a photo has no text layer.

Looks for AI-generated text.

`digital_signature`, `attachments`, `active_content`, `encryption_permissions`

Mostly `skip`: a photo has no signature, attachments or permissions.

Apply to the PDF’s structure.

`ai_office_detection`

`skip`.

`skip`: applies to Word, PowerPoint and Excel files only.

`magic_byte`, `polyglot`, `trailing_data`, `entropy`

Apply to every file.

Apply to every file.

See [Detectors](/verify/forensics#detectors) for what each key returns.

## 

[​

](#result)

Result

Illustrative: shape from the API reference. Values vary by check.

Here is what the pack might return, with every check not listed reporting `none` or `skip`:

File

Check

`status`

What it found

`photo_4.jpg`

`copy_move`

`issue`

A region of the photo is cloned elsewhere in the same image.

`photo_6.jpg`

`web_presence`

`issue`

The image is found online.

`repair-estimate.pdf`

`producer_divergence`

`issue`

Page 2 is typeset with a different font set from page 1.

`claim-form.pdf`, `police-report.pdf`, `photo_1.jpg`–`photo_3.jpg`, `photo_5.jpg`

All

`none` or `skip`

Nothing found.

**Combined decision:** `investigate`. Three files carry an `issue`, so the claim goes to an investigator with the list above. The run for `photo_4.jpg`, shortened to four of its 19 checks:

GET /v1/forensics/files/{file\_id}

```
{
  "run_id": "a41c9e07d2b3",
  "file_id": "665f1c2e8a1b4c0012ab9a04",
  "spec_filter": ["web_presence", "ai_gen_detection", "copy_move", "magic_byte"],
  "status": "complete",
  "created_at": "2026-10-12T08:41:00.000Z",
  "forensics": {
    "copy_move": { "status": "issue", "value": "Copy-move: detected" },
    "ai_gen_detection": { "status": "none", "value": "AI-generated: false" },
    "web_presence": { "status": "none", "value": "Web presence: false", "web_presence": false },
    "magic_byte": { "status": "none" }
  }
}
```

An upload-time run lists all 19 keys in `spec_filter`, with one result for each under `forensics`.

## 

[​

](#how-the-findings-combine)

How the findings combine

The API has **no overall fraud score**. Each check on each file reports `none`, `issue`, `skip` or `error`, and your code decides what that means for the claim:

*   **Per file:** route on each check’s `status`, not the run’s. A `complete` run can still contain an `issue`.
*   **Per pack:** one `issue` anywhere is enough to stop straight-through payment here. You could instead weight checks differently, for example send a single `web_presence` hit to a quick manual look and a `copy_move` hit to a full investigation.
*   **Missing results:** a run in `error`, a check in `error`, or a run that never appears is not a clean result. Hold the claim and retry rather than pay.
*   **`web_presence`:** a missing `web_presence` boolean means the check reached no conclusion, not “not found on the web”.

## 

[​

](#what-it-shows)

What it shows

*   One upload per file both stores it and starts all 19 checks; photos skip extraction with `storageOnly: true`.
*   `GET /v1/forensics/files/{file_id}` finds each file’s run without tracking a `run_id`.
*   Checks that don’t fit a file type report `skip` (or `none` with a note), so you can run everything on everything and still read the results the same way.
*   The pack-level decision is yours: the API reports per file and per check, and your code combines them.

## 

[​

](#next-steps)

Next steps

## Detectors and results

All 19 checks, statuses and errors.

## Document integrity checks

Pick checks per file with `POST /v1/forensics` and route on the result.

## fileForensics

Two ways to run it, and how usage is counted.

## Uploads

`enabledProducts` and `storageOnly` rules.

[Previous](/examples/financial-report)[

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

*   [The job](#the-job)
*   [Sample input](#sample-input)
*   [Steps](#steps)
*   [Which checks matter for which file](#which-checks-matter-for-which-file)
*   [Result](#result)
*   [How the findings combine](#how-the-findings-combine)
*   [What it shows](#what-it-shows)
*   [Next steps](#next-steps)
