# Document integrity checks
Source: https://docs.file.ai/examples/document-integrity

> Screen an identity document or statement for editing and AI generation during onboarding, and route it on the result.

fileForensics

# Document integrity checks

Copy pageCopy page

Screen an identity document or statement for editing and AI generation during onboarding, and route it on the result.

Copy pageCopy page

## 

[​

](#the-job)

The job

A new customer uploads a photo of their driving licence and a PDF bank statement during onboarding. Before you accept them, you want to know whether either was edited, generated by AI, or is not what its file type claims, and send anything suspicious to a reviewer. This example shows the two decisions that are yours to make: **which checks to run for each document**, and **how to route on the result**. For the request and polling basics, see the [Forensics quickstart](/quickstart/forensics).

## 

[​

](#sample-input)

Sample input

*   `licence-front.jpg`, a 3 MB JPEG photo of the front of a driving licence taken on a phone.
*   `statement-2026-09.pdf`, a 4-page PDF bank statement used as proof of address.

Both are already stored in your workspace. If you only need integrity checks, upload them with `storageOnly: true` so they skip OCR and extraction; see [Upload options](/ingest/uploads#upload-options). To run all 19 checks as the file arrives instead, see [Two ways to run it](/forensics/overview#two-ways-to-run-it).

## 

[​

](#steps)

Steps

1

Choose checks for each document

A check that doesn’t fit the file reports `skip`, so pick the ones that can find something in this kind of document:

Document

`spec_filter`

Why

Photo of an ID

`copy_move`, `ai_gen_detection`, `web_presence`, `magic_byte`

Cloned regions, a generated image, a photo lifted from the web, or a file that isn’t really a JPEG.

PDF statement

`ai_pdf_detection`, `producer_divergence`, `revision_history`, `magic_byte`

A generated PDF, pages from different producers, or later edits.

See [Detectors](/verify/forensics#detectors) for every key.

Python

```
import os, time, requests

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

CHECKS = {
    "image/jpeg": ["copy_move", "ai_gen_detection", "web_presence", "magic_byte"],
    "application/pdf": ["ai_pdf_detection", "producer_divergence", "revision_history", "magic_byte"],
}
```

2

Start a run and wait for it

Python

```
def check(file_id, mime_type):
    run = requests.post(
        f"{BASE}/forensics",
        headers=HEADERS,
        json={"file_id": file_id, "spec_filter": CHECKS[mime_type]},
    )
    run.raise_for_status()
    run_id = run.json()["run_id"]

    while True:
        result = requests.get(f"{BASE}/forensics/runs/{run_id}", headers=HEADERS).json()
        if result["status"] in ("complete", "error"):
            return result
        time.sleep(5)
```

`web_presence` can take up to about 180 seconds. A run still `queued` or `running` after about 6 minutes is stuck; see [Retry a stuck run](/verify/forensics#how-it-works).

3

Route on each check's status

Python

```
def route(file_id, result):
    statuses = {key: r["status"] for key, r in result.get("forensics", {}).items()}

    if result["status"] == "error" or "error" in statuses.values():
        retry_later(file_id)          # a check or the run failed
    elif "issue" in statuses.values():
        send_to_reviewer(file_id, [k for k, s in statuses.items() if s == "issue"])
    else:
        accept(file_id)               # every check reported none or skip

for file_id, mime_type in [(licence_id, "image/jpeg"), (statement_id, "application/pdf")]:
    route(file_id, check(file_id, mime_type))
```

## 

[​

](#result)

Result

Illustrative: shape from the API reference. Fields beyond `status` and `value` vary by check.

GET /v1/forensics/runs/{run\_id}

```
{
  "run_id": "eebb50fee5b1",
  "file_id": "665f1c2e8a1b4c0012ab9911",
  "spec_filter": ["copy_move", "ai_gen_detection", "web_presence", "magic_byte"],
  "status": "complete",
  "created_at": "2026-09-25T02:14:00.000Z",
  "forensics": {
    "copy_move": { "status": "issue" },
    "ai_gen_detection": { "status": "none", "value": "AI-generated: false" },
    "web_presence": { "status": "none", "value": "Web presence: false", "web_presence": false },
    "magic_byte": { "status": "none" }
  }
}
```

Here `copy_move` found cloned regions, so the licence goes to a reviewer even though the other checks are clear.

## 

[​

](#what-it-shows)

What it shows

*   **Choose checks per document type.** An image and a PDF need different checks; `spec_filter` is required, so a check added later never changes what your integration runs.
*   **Route on each check’s `status`, not the run’s.** A `complete` run can still contain an `issue`.
*   For `web_presence`, a missing `web_presence` boolean means no conclusion, not “not found”.

Delete the run with `DELETE /v1/forensics/runs/{run_id}` once you have stored the outcome. The file is not affected.

## 

[​

](#next-steps)

Next steps

## Detectors and results

All nineteen checks, statuses and errors.

## Forensics quickstart

The shortest path to a first run, in cURL, Python and Node.

## Run fileForensics from your assistant

`run_file_forensics` and related MCP tools.

## Uploads

Upload options, including `storageOnly`.

[Previous](/verify/forensics)[

Financial report

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

Next

](/examples/financial-report)

⌘I

## On this page

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