# Holofin API Documentation > REST API for Holofin: send a financial or identity document to a workflow, get its data back as structured JSON with the validation and fraud checks run on it, and receive a webhook when processing finishes. Dossiers group a case's documents and check them against case rules written in HoloLang. ## Getting Started ### Introduction URL: https://app.holofin.ai/docs/api/v1.1/ API Documentation v1.1 ====================== For LLMs Machine-readable docs in the [llms.txt](https://llmstxt.org/) format. [llms.txtIndex of every page, for agents that fetch on demand.](https://app.holofin.ai/docs/api/llms.txt)[llms-full.txtThe whole documentation as one file, for context windows.](https://app.holofin.ai/docs/api/llms-full.txt)[openapi.jsonThe machine-readable API description, for SDK and client generation.](https://app.holofin.ai/docs/api/v1.1/openapi.json) [Download llms-full.txt](https://app.holofin.ai/docs/api/llms-full.txt?download=1) v1.1 Send a financial document, get its data back as structured JSON, and receive a webhook when processing finishes. What do you need to do? ----------------------- | You need to... | Start here | | --- | --- | | Send your first document and read the JSON back | [Quickstart](https://app.holofin.ai/docs/api/v1.1/quickstart/) | | Pull specific fields out of a financial document | [Extraction](https://app.holofin.ai/docs/api/v1.1/extraction/) | | Know whether a document has been tampered with | [Fraud analysis](https://app.holofin.ai/docs/api/v1.1/fraud-analysis/) | | Route an incoming file to the right process | [Classification](https://app.holofin.ai/docs/api/v1.1/classification/) | | Split a PDF that holds several documents | [Segmentation](https://app.holofin.ai/docs/api/v1.1/segmentation/) | | Check a printed address or company number against a reference | [Field enrichment](https://app.holofin.ai/docs/api/v1.1/enrichment/) | | Express your own business rules over extracted data | [HoloLang](https://app.holofin.ai/docs/api/v1.1/hololang/) | | Collect a whole file of documents against one dossier | [Dossiers](https://app.holofin.ai/docs/api/v1.1/cases/) | | Be told when processing finishes, without polling | [Webhooks](https://app.holofin.ai/docs/api/v1.1/webhooks/) | | Know what is safe to retry, and which limits stop a call | [Reliability and limits](https://app.holofin.ai/docs/api/v1.1/reliability/) | | Track consumption and processing volumes | [Usage](https://app.holofin.ai/docs/api/v1.1/usage/) and [Metrics](https://app.holofin.ai/docs/api/v1.1/metrics/) | | Run Holofin on behalf of client organizations | [Sub-organizations](https://app.holofin.ai/docs/api/v1.1/sub-organizations/) | Start building -------------- Three ways in. Pick the one that matches how you work. [### REST API One authenticated POST sends a document through the whole pipeline. No SDK required.](https://app.holofin.ai/docs/api/v1.1/quickstart/)[### Dashboard Compose the workflow visually, test it on real documents, then trigger the same one by API.](https://app.holofin.ai/workflows/)[### AI agents Point a coding agent at our MCP server, the installable skill, or the machine-readable docs index.](https://app.holofin.ai/docs/api/v1.1/ai-assistants/) Everything starts with an API key from your [developer settings](https://app.holofin.ai/settings/api-keys/). Keys are workspace-scoped and stay active even if the person who created them leaves the workspace. The API is described by an [OpenAPI 3.1 document](https://app.holofin.ai/docs/api/v1.1/openapi.json), which you can feed to a client generator, import into Postman, or hand to an agent alongside the For LLMs files at the top of this page. Workflows --------- Everything in Holofin runs through workflows. A workflow is a document processing pipeline that classifies, segments and extracts data from a document in a single API call. Document Fraud Analysis Classify Segment Extract JSON Configure your workflow once in the dashboard, then trigger it via the API. The workflow handles routing, error handling and the aggregation of results from each step. [Learn more about workflows](https://app.holofin.ai/docs/api/v1.1/workflows/) --- ### Quickstart URL: https://app.holofin.ai/docs/api/v1.1/quickstart/ Quickstart ========== v1.1 Send your first document through Holofin and read the structured result. Four steps, one endpoint, about five minutes. Before you start ---------------- You need two things, both from the dashboard: 1 ### An API key Keys are workspace-scoped and stay active even if the person who created them leaves. [Create an API key](https://app.holofin.ai/settings/api-keys/) 2 ### A workflow Its ID looks like `wf_abc123` and appears in the workflow's overview page. [Open your workflows](https://app.holofin.ai/workflows/) There is no separate upload, parse or extract call. A workflow does the whole pipeline, so this quickstart only ever touches `/workflow-runs/`. 1. Send a document ------------------ Documents travel base64-encoded in the JSON body. Build the body in a file, so the command works on macOS and Linux and for files of any size. The call returns immediately with a `run_id`; processing continues in the background. ### Create a workflow run ``` base64 < statement.pdf | tr -d '\n' \ | jq -Rs '{workflow_id: "wf_abc123", document_name: "statement.pdf", document_b64: .}' \ > body.json curl -X POST https://app.holofin.ai/api/v1.1/workflow-runs/ \ -H "X-API-Key: $HOLOFIN_API_KEY" \ -H "Content-Type: application/json" \ -d @body.json ``` ### 202 Accepted ``` { "run_id": "wr_abc123xyz789", "workflow_id": "wf_abc123", "workflow_name": "Statement intake", "document_id": "doc_def456uvw", "extraction_id": null, "document_name": "statement.pdf", "status": "pending", "created_at": "2026-01-15T10:30:00Z", "metadata": {} } ``` Pass a `metadata` object to carry your own reference through to every response and webhook. To file the document under a dossier, add `case_id`, see [Dossiers](https://app.holofin.ai/docs/api/v1.1/cases/). Field values for the dossier go through `POST /cases/`, never through a run. 2. Wait for the result ---------------------- Two ways to learn the run has finished. Use webhooks in production; polling is fine while you are getting started. ### Webhooks (recommended) Register an endpoint once and Holofin posts a signed `workflow-run.completed` event when the run lands, along with `workflow-run.failed` and `workflow-run.paused`. Set `webhook_url` on a single run to override your endpoints for that run only, which is handy when testing against a tunnel. [Set up webhooks](https://app.holofin.ai/docs/api/v1.1/webhooks/) ### Polling Fetch the run until `status` reaches a terminal value: `completed`, `failed` or `cancelled`. The two non-terminal values are `pending` and `running`, plus `paused` when the workflow is waiting on a human reviewer. ### Poll until the run is terminal ``` RUN_ID=wr_abc123xyz789 while :; do STATUS=$(curl -s "https://app.holofin.ai/api/v1.1/workflow-runs/$RUN_ID" \ -H "X-API-Key: $HOLOFIN_API_KEY" | jq -r .status) echo "$STATUS" # paused is not terminal: a reviewer will resume the run case "$STATUS" in completed|failed|cancelled) break ;; esac sleep 5 done ``` 3. Read the result ------------------ A completed run carries the aggregated output of every node that ran. The two fields you will use most: | Field | What it holds | | --- | --- | | extraction\_result | A `document_parts` array, one entry per logical document found in the file, each with its `part_type`, `pages`, `validation_status` and extracted `data`. `null` when no extractor node completed. | | fraud\_analysis | The forensic verdict: `risk_level`, `risk_score`, `risk_factors` and `trust_factors`. `null` when no fraud analyzer node ran. | One PDF can hold several documents. Holofin segments it, so `document_parts` is an array even for a single-document file. Read the first part with `jq '.extraction_result.document_parts[0].data'`. [The full result structure](https://app.holofin.ai/docs/api/v1.1/extraction/#result-structure) 4. Handle the unhappy path -------------------------- Before you ship, decide what each of these does in your integration: | Case | What you get | | --- | --- | | The run fails | `status: "failed"` and a `workflow-run.failed` webhook. Partial results from nodes that did complete are still on the run. | | The workflow needs a human | `workflow-run.paused`, then `workflow-run.confirmed` once a reviewer decides. A paused run is not a failed one. | | Extraction is unreliable | A part with `validation_status` of `WARNING` or `ERROR`. The run still completes, so check the part, not just the run. | | You are out of credits | `402 Payment Required` on the create call. See [Errors](https://app.holofin.ai/docs/api/v1.1/errors/). | Where to go next ---------------- [### Build the workflow Classification, segmentation, extraction and human review nodes, and how they chain.](https://app.holofin.ai/docs/api/v1.1/workflows/)[### Write validation rules HoloLang turns your business rules into the verdicts that come back on every part.](https://app.holofin.ai/docs/api/v1.1/hololang/)[### Read the fraud verdict Risk levels, signals and what each trust factor is telling you.](https://app.holofin.ai/docs/api/v1.1/fraud-analysis/)[### The full endpoint reference Every parameter on every workflow-run endpoint, including node runs and PDF downloads.](https://app.holofin.ai/docs/api/v1.1/workflow-runs/) --- ### Reliability and limits URL: https://app.holofin.ai/docs/api/v1.1/reliability/ Reliability and limits ====================== v1.1 What is safe to retry, what happens when you send the same thing twice, and which limits stop a call. Read this before you put an integration into production. Repeating a call ---------------- Holofin has no `Idempotency-Key` header. Whether a repeat is safe depends on the endpoint, as listed below. | Call | Safe to repeat? | What a repeat does | | --- | --- | --- | | POST /workflow-runs/ | No | Creates a **second run** and consumes credits again. There is nothing in the payload Holofin deduplicates on, not even an identical document. Track the `run_id` on your side before retrying. | | POST /cases/ | Yes | Merge semantics keyed on `case_id`. Only the keys you send are touched, an explicit `null` clears one, and re-sending identical values changes nothing: no revision, no re-evaluation. | | POST /workflow-runs/{run\_id}/cancel | Harmless | Nothing changes, but a run that is already `completed`, `failed` or `cancelled` answers `409 CANNOT_CANCEL`. Treat that 409 as success, not as an error to retry. | | PATCH and DELETE | Yes | Webhook and organization updates settle on the state you sent. | | All GET endpoints | Yes | Read-only. | A timed-out `POST /workflow-runs/` may still have created the run. Before resending, list runs with `created_after` set just before your call and match on `document_name`. List items are trimmed: `metadata` is only on `GET /workflow-runs/{run_id}`, so fetch a candidate by ID to confirm it is yours. Retrying failures ----------------- | Status | Retry? | | --- | --- | | 400, 409, 413, 415 | Never. The request or the resource state is wrong and will stay wrong. Fix it. | | 401, 403 | Never. The key is missing, inactive, expired, or the caller's IP is not on the key's allow-list. | | 402 | Not until credits are topped up. Retrying burns nothing but will keep failing. | | 404 | Never for a resource in another workspace. A run you just created is visible immediately, so a 404 there means the ID is wrong. | | 429 | For the per-key rate limit, after the `Retry-After` seconds. For the sandbox allowance, backing off does not help. See limits below. | | 5xx | Yes, with exponential backoff, but only for calls the table above marks safe to repeat. | [Every error code and its payload](https://app.holofin.ai/docs/api/v1.1/errors/) Limits ------ Limits that stop a call: | Limit | Signal | What to do | | --- | --- | --- | | Request rate | 429, `Retry-After` | 100 requests per minute per API key, across all authenticated endpoints. Wait the number of seconds in `Retry-After`. Use webhooks rather than tight polling loops. | | Document size | 400 PAGE\_LIMIT\_EXCEEDED, 413 | 200 pages per document, 100 MB per request body (base64 adds a third to the file size). | | Credit balance | 402 INSUFFICIENT\_CREDITS | Watch [usage](https://app.holofin.ai/docs/api/v1.1/usage/) and alert before you hit zero. Every credit type counts toward the same balance. | | Sandbox allowance | 429 SANDBOX\_LIMIT\_EXCEEDED | A sandbox workspace has a fixed credit allowance in total, not per period. Backing off does not help; the allowance has to be raised. | | Page size on lists | limit, max 100 | Higher values are clamped rather than rejected. Page with `next_token`. | | IP allow-list on a key | 403 | Set per key. Add your egress addresses in [developer settings](https://app.holofin.ai/settings/api-keys/) before moving hosts. | Webhook delivery ---------------- Deliveries retry on your behalf, so your receiver only needs to be idempotent and fast: * Any `2xx` counts as delivered. Anything else is retried. * Up to **7 attempts over 48 hours**, with exponential backoff plus jitter. * An endpoint is marked `FAILING` after 5 consecutive failures, and keeps receiving events. * Because deliveries retry, **your handler will see the same event more than once**. Key it on `run_id` and the event type. Acknowledge first, process afterwards. A receiver that does the work before returning `200` turns a slow database into a duplicate delivery. [The full retry schedule](https://app.holofin.ai/docs/api/v1.1/webhooks/#webhook-retries) Pagination ---------- List endpoints are cursor-paginated, not offset-paginated. You get a `next_token`; you pass it back. There are no page numbers and no total count, which is what keeps paging stable while new runs are being created underneath you. ### Walk every page ``` TOKEN="" while :; do PAGE=$(curl -s -G "https://app.holofin.ai/api/v1.1/workflow-runs/" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ --data-urlencode "limit=100" \ ${TOKEN:+--data-urlencode "next_token=$TOKEN"}) echo "$PAGE" | jq -r '.runs[].run_id' TOKEN=$(echo "$PAGE" | jq -r '.pagination.next_token // empty') [ -z "$TOKEN" ] && break done ``` | Parameter | Description | | --- | --- | | limit | Page size, default and maximum 100. Larger values are clamped. | | next\_token | Opaque cursor, returned as `pagination.next_token`. Omit it for the first page; it is absent from the response once `pagination.has_next` is false. Do not parse it or build one yourself. | | workflow\_id | Only runs of that workflow. | | status | `completed`, `failed` or `cancelled`. | | created\_after, created\_before | ISO 8601 bounds. The pair a reconciliation job wants. | **Listing only returns terminal runs.** A run still `pending` or `running` does not appear, so do not poll a listing to track progress. To follow one run, fetch it by ID; to be told when any run finishes, use webhooks. --- ### Authentication URL: https://app.holofin.ai/docs/api/v1.1/authentication/ Authentication ============== Every endpoint in the Holofin API requires an API key. This page covers how to send it, how to restrict a key by IP or expiry, and the errors returned when authentication fails. [API Key Authentication](#api-key-authentication) ------------------------------------------------- Send your API key in the `X-API-Key` header on every request. Requests must use HTTPS; calls over plain HTTP fail, and so do calls without a key. The examples in these docs read the key from `$HOLOFIN_API_KEY`: `export HOLOFIN_API_KEY=sk-holo-...` ### Example request with API key ``` curl "https://app.holofin.ai/api/v1.1/workflow-runs/?limit=1" \ -H "X-API-Key: $HOLOFIN_API_KEY" ``` Never commit an API key to a repository. [Creating API Keys](#creating-api-keys) --------------------------------------- Create API keys in your workspace settings. You can create a separate key for each integration or environment. [Create your API key in Settings](https://app.holofin.ai/settings/api-keys/) [IP Allowlisting](#ip-allowlisting) ----------------------------------- You can restrict an API key to specific IP addresses or CIDR ranges. Once set, only requests from the allowed IPs are accepted. * Navigate to Settings → API Keys * Click the options menu (…) on a key * Select "IP Whitelist" * Add one IP address or CIDR range per line Supports IPv4, IPv6, and CIDR notation (e.g., `192.168.1.0/24`). Leave empty to allow requests from any IP. When a request is rejected due to IP restrictions, the API returns: ### 403 Response - IP Not Allowed ``` { "error": "Request from this IP address is not allowed." } ``` [Key Expiration](#key-expiration) --------------------------------- API keys can be created with an expiration date. When creating a key, choose from preset durations (1 month, 3 months, 6 months, 1 year) or set a custom date. Keys without an expiration date remain valid until manually disabled or deleted. When an expired key is used, the API returns: ### 403 Response - Expired Key ``` { "error": "API key has expired." } ``` [Authentication Error Responses](#error-responses) -------------------------------------------------- When authentication fails, the API will return one of the following error responses: ### 401 - Missing API Key This error occurs when no `X-API-Key` header is provided. ### 401 Response ``` { "error": "API key required in X-API-Key header." } ``` ### 403 - Invalid or Inactive API Key This error occurs when the provided API key is invalid, inactive, or has been revoked. ### 403 Response ``` { "error": "Invalid or inactive API key." } ``` [View API Logs](https://app.holofin.ai/logs/) --- ### Errors URL: https://app.holofin.ai/docs/api/v1.1/errors/ Errors ====== The HTTP status tells you what kind of failure it is; the `code` tells you which one. Branch on the code, never on the message text, which can change. [Error format](#error-format) ----------------------------- Endpoint errors carry an `error` object with a stable `code` and a human-readable `message`: ### 404 Not Found ``` { "error": { "code": "RUN_NOT_FOUND", "message": "Workflow run wr_abc123xyz789 not found" } } ``` A few errors carry a plain string instead, with no code: authentication failures, the per-key rate limit, an oversized body, malformed JSON on `POST /workflow-runs/`, and unexpected `500`s. ### 403 Forbidden ``` { "error": "Invalid or inactive API key." } ``` Read both forms the same way: ### Python ``` error = response.json().get("error") if isinstance(error, dict): code, message = error["code"], error["message"] else: code, message = None, error # branch on response.status_code ``` Every response carries an `X-Request-ID` header (`req_...`). Quote it when you contact support; the same ID appears in your [API logs](https://app.holofin.ai/logs/). [HTTP status codes](#status-codes) ---------------------------------- | Status | Meaning | | --- | --- | | 400 | The request is invalid: a missing or malformed parameter, or a document that cannot be read. Fix it before sending again. | | 401 | No `X-API-Key` header. | | 402 | The organization cannot be billed for this document: no credits left, or no active plan. | | 403 | The key is invalid, inactive or expired, the caller's IP is not on the key's allow-list, or the organization may not use this endpoint. | | 404 | The resource does not exist in your organization. | | 405 | The method is not supported on this path. The `Allow` header lists the ones that are. | | 409 | The resource is not in a state that allows the call, or a limit on the number of resources is reached. | | 413 | The request body is over 100 MB. | | 415 | A run or part PDF was requested for a source document that is not a PDF. | | 429 | Over the per-key request rate (wait for `Retry-After`), or over the sandbox credit allowance. See [limits](https://app.holofin.ai/docs/api/v1.1/reliability/#limits). | | 500 | Something failed on our side. Retry with backoff if the call is [safe to repeat](https://app.holofin.ai/docs/api/v1.1/reliability/#repeating-a-call). | [Error codes](#error-codes) --------------------------- Every code, the status it comes with, and the endpoints that return it. | HTTP | Code | Meaning | Returned by | | --- | --- | --- | --- | | 400 | APPLICATIONS\_DISABLED | Dossiers are not enabled for your organization. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run), [List dossiers](https://app.holofin.ai/docs/api/v1.1/cases/#list), [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update), [List the organization's dossier configurations](https://app.holofin.ai/docs/api/v1.1/case-configs/#list), [One configuration's full contract](https://app.holofin.ai/docs/api/v1.1/case-configs/#detail) | | 400 | CONFLICTING\_INPUT | Both `document_b64` and `document_pages_b64` were sent. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 400 | FIELDS\_WITHOUT\_PARTY | A role carries several participants and one of them has no `party_ref`. | [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update) | | 400 | INVALID\_CASE\_ID | `case_id` is empty or longer than 255 characters. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run), [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update) | | 400 | INVALID\_CLONE\_WORKFLOWS | `clone_workflows` is not a boolean. | [Create a child organization](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#create) | | 400 | INVALID\_DOCUMENT | The content is not valid base64, or not a PDF, office document or image Holofin reads. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 400 | INVALID\_EVENTS | `events` is not a list, or names an event that cannot be subscribed to. | [Register a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#create-webhook), [Update a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#update-webhook) | | 400 | INVALID\_EXTERNAL\_REF | `external_ref` is not a string, or too long. | [Create a child organization](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#create) | | 400 | INVALID\_FILTER | A filter has a value outside the allowed set; the message lists them. | [List workflow runs](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#list-workflow-runs), [List dossiers](https://app.holofin.ai/docs/api/v1.1/cases/#list), [Read a dossier's change trail](https://app.holofin.ai/docs/api/v1.1/cases/#activities) | | 400 | INVALID\_INPUT\_FIELD | A value in `case_fields` has the wrong shape for its field. | [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update) | | 400 | INVALID\_JSON | The body is not a JSON object. | [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update), [Register a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#create-webhook), [Update a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#update-webhook), [Create a child organization](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#create), [Update a child organization](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#update) | | 400 | INVALID\_NAME | `name` is missing, not a string, or too long. | [Register a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#create-webhook), [Update a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#update-webhook), [Create a child organization](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#create), [Update a child organization](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#update) | | 400 | INVALID\_PARTY\_REF | A participant reference is not a distinct, non-empty string of at most 255 characters. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run), [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update) | | 400 | INVALID\_PARTY\_TYPE | `party_type` is not one of `PM`, `PP`, `PPNS` or `BE`. | [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update) | | 400 | INVALID\_STATUS | `status` is not one of the allowed values. | [Update a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#update-webhook), [Update a child organization](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#update) | | 400 | INVALID\_URL | `url` is malformed or resolves to a private address. | [Register a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#create-webhook), [Update a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#update-webhook) | | 400 | INVALID\_WEBHOOK\_URL | `webhook_url` is malformed, not `http(s)`, or resolves to a private address. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 400 | MISSING\_CONFIG\_ID | A new dossier needs a `config_id`. | [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update) | | 400 | MISSING\_DOCUMENT | Neither `document_b64` nor `document_pages_b64` was sent. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 400 | MISSING\_URL | `url` is missing. | [Register a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#create-webhook), [Update a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#update-webhook) | | 400 | MISSING\_WORKFLOW\_ID | `workflow_id` is missing. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 400 | PAGE\_LIMIT\_EXCEEDED | The document has more than 200 pages. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 400 | PARTY\_WITHOUT\_CASE | `party_refs` was sent without `case_id`. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 400 | UNKNOWN\_CASE\_ID | `case_id` names no dossier. Create it with `POST /cases/` first. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 400 | UNKNOWN\_CONFIG | `config_id` names no case configuration. | [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update) | | 400 | UNKNOWN\_INPUT\_FIELD | A key in `case_fields` that the configuration does not declare; the message lists them. | [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update) | | 400 | UNKNOWN\_PARAMETER | An unsupported query parameter; the message lists the supported ones. | [List dossiers](https://app.holofin.ai/docs/api/v1.1/cases/#list), [Read a dossier's change trail](https://app.holofin.ai/docs/api/v1.1/cases/#activities), [List the organization's dossier configurations](https://app.holofin.ai/docs/api/v1.1/case-configs/#list), [One configuration's full contract](https://app.holofin.ai/docs/api/v1.1/case-configs/#detail) | | 400 | UNKNOWN\_PARTY\_REF | A reference names no participant of the dossier; the message lists them. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 400 | UNKNOWN\_ROLE | A participant role the configuration does not define; the message lists the allowed ones. | [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update) | | 400 | UNSUPPORTED\_FIELD | The body carries a key that belongs to another endpoint, such as `case_fields` on a run. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run), [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update) | | 402 | INSUFFICIENT\_CREDITS | No credits remaining. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 402 | NO\_ACTIVE\_SUBSCRIPTION | The organization has no active plan. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 402 | PLAN\_NOT\_PAID | The plan is not active yet. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 403 | NOT\_ALLOWED | The key's organization may not use this endpoint. | [List child organizations](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#list), [Create a child organization](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#create), [Get a child organization](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#get), [Update a child organization](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#update) | | 404 | DOCUMENT\_FILE\_UNAVAILABLE | The document file is no longer stored, for example after its retention period. | [Download the run's document](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run-pdf), [Download a document part](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-part-pdf) | | 404 | NOT\_FOUND | No resource with this ID in your organization. | [Read a dossier's status, requirements and gaps](https://app.holofin.ai/docs/api/v1.1/cases/#read), [Read a dossier's change trail](https://app.holofin.ai/docs/api/v1.1/cases/#activities), [One configuration's full contract](https://app.holofin.ai/docs/api/v1.1/case-configs/#detail), [Get a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#management-api), [Update a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#update-webhook), [Delete a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#management-api), [Get a child organization](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#get), [Update a child organization](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#update) | | 404 | PART\_NOT\_FOUND | No part with this key on the run. | [Download a document part](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-part-pdf) | | 404 | RUN\_NOT\_FOUND | No run with this ID in your organization. | [Get a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run), [Download the run's document](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run-pdf), [Download a document part](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-part-pdf), [List a run's node executions](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#list-node-runs), [Cancel a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#cancel-workflow-run) | | 404 | WORKFLOW\_NOT\_FOUND | No workflow with this ID in your organization. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 405 | METHOD\_NOT\_ALLOWED | The method is not supported on this path; see the `Allow` header. | [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update) | | 409 | CANNOT\_CANCEL | The run is already `completed`, `failed` or `cancelled`. Treat it as success. | [Cancel a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#cancel-workflow-run) | | 409 | CONFIG\_CONFLICT | The dossier exists under another configuration, which cannot change. Open a new dossier. | [Create or update a dossier](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update) | | 409 | LIMIT\_REACHED | The maximum number of child organizations is reached. | [Create a child organization](https://app.holofin.ai/docs/api/v1.1/sub-organizations/#create) | | 409 | RUN\_NOT\_TERMINAL | A part PDF was requested before extraction finished. | [Download a document part](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-part-pdf) | | 409 | WEBHOOK\_LIMIT\_REACHED | The organization already has 5 endpoints. | [Register a webhook endpoint](https://app.holofin.ai/docs/api/v1.1/webhooks/#create-webhook) | | 415 | UNSUPPORTED\_DOCUMENT\_TYPE | A PDF was requested for a source document that is not a PDF, such as a spreadsheet. | [Download the run's document](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run-pdf), [Download a document part](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-part-pdf) | | 429 | SANDBOX\_LIMIT\_EXCEEDED | The document needs more sandbox credits than remain. Backing off does not help. | [Create a workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) | | 500 | INTERNAL\_ERROR | An unexpected error. | Any endpoint | | 500 | PDF\_RESOLUTION\_FAILED | The run or part PDF could not be produced. | [Download the run's document](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run-pdf), [Download a document part](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-part-pdf) | [What to retry](https://app.holofin.ai/docs/api/v1.1/reliability/)[Authentication](https://app.holofin.ai/docs/api/v1.1/authentication/) --- ### Changelog URL: https://app.holofin.ai/docs/api/v1.1/changelog/ Changelog ========= Updates to the Holofin API, newest first. Entries marked **Behaviour change** can affect existing integrations. November 2026 Latest -------------------- * Fraud factors say where their evidence comes fromEach entry of `risk_factors` and `trust_factors` now has `sources`: `forensic`, `template`, `content` or `seal`. The field is new, so nothing breaks; read it to route a finding to the right reviewer. See [Fraud analysis](https://app.holofin.ai/docs/api/v1.1/fraud-analysis/#fraud-analysis-object). * Bank statement validation codes referenceThe codes now have [their own table](https://app.holofin.ai/docs/api/v1.1/document-schemas/#bank-statement-validation-codes), grouped by what to do about them. The codes are unchanged. Their `description` in `GET /agents/{id}` is shorter, so branch on the code, never on that text. * Runs are no longer refused over a daily page volume`POST /workflow-runs/` no longer answers `429 RATE_LIMIT_EXCEEDED` once an organization has processed a set number of pages in 24 hours. The code is retired; you can drop its handling. The per-key request rate and the sandbox credit allowance still answer 429. [Docs](https://app.holofin.ai/docs/api/v1.1/reliability/#limits). October 2026 ------------ * **Behaviour change:** fraud analysis checks for pages redrawn by AIScans and image uploads can now raise the risk factor `ai_page_redrawn` (severity `high`), so a scan that read `normal` can now read `warning`. [Docs](https://app.holofin.ai/docs/api/v1.1/fraud-analysis/#ai-redrawn-pages). * Bank statements: a misread transaction date now raises a warningA block of transactions out of date order with its neighbours, where swapping day and month or moving the month by one restores the order (10/09 returned as 2026-10-10), now raises the warning `TRANSACTION_DATE_OUT_OF_SEQUENCE`, or is corrected and carries the informational `TRANSACTION_DATE_CORRECTED_FROM_PAGE_IMAGE`. A part that was `OK` can now carry this warning. [Docs](https://app.holofin.ai/docs/api/v1.1/document-schemas/#bank-statement). * Bank statements: an account without a statement now says soA part that names an account but has no balances and no transactions, typically a line of the bank’s account summary, now carries the informational code `ACCOUNT_WITHOUT_STATEMENT`. Its `validation_status` stays `null`. [Docs](https://app.holofin.ai/docs/api/v1.1/document-schemas/#bank-statement). * Fraud analysis can be limited to native PDFs or bank statementsOn request, Holofin can restrict an organization’s fraud analysis to native PDFs, bank statements, or both. A document left out is not billed for fraud and reads `"status": "skipped"` with a `skip_reason`; treat it as not analysed, not as clean. [Docs](https://app.holofin.ai/docs/api/v1.1/fraud-analysis/#fraud-analysis-object). * Bank statements: why the opening balance is missingA current-account part with no opening balance now also carries the informational code `STATEMENT_LIKELY_MISSING_PAGE`: the first page is probably missing. `validation_status` is unchanged. Branch on it to ask for the missing page. [Docs](https://app.holofin.ai/docs/api/v1.1/document-schemas/#bank-statement). September 2026 -------------- * Bank statements: a closing balance its next statement contradictsWhen a closing balance is the exact negative of the next statement’s opening balance on the same account, both parts now raise the warning `CLOSING_BALANCE_CONTRADICTED_BY_SIBLING`: one read lost a sign. A part that was `OK` can now carry this warning. [Docs](https://app.holofin.ai/docs/api/v1.1/document-schemas/#bank-statement). * Bank statements: more misread IBANs correctedMore IBANs failing their MOD-97 check are now corrected. Such parts carry the informational code `ACCOUNT_NUMBER_CORRECTED_FROM_SEGMENTER` instead of `INVALID_IBAN_CHECKSUM`; treat it like `ACCOUNT_NUMBER_CORRECTED_FROM_SIBLING`. [Docs](https://app.holofin.ai/docs/api/v1.1/document-schemas/#bank-statement). * Arithmetic checks: tolerance per formulaTotals checks on built-in extractors now allow 1 € per non-zero amount in the formula (10 € minimum) instead of a flat 10 €, so long totals rounded line by line now pass. Each check returns the `tolerance` it applied. * Cases APIRun a dossier end to end without the Holofin UI. New endpoints: `POST /cases`, `GET /cases/{case_id}`, `GET /cases/{case_id}/activities` and `GET /case-configs`. To attach a document, pass `case_id` and `party_refs` when creating the workflow run. Dossier webhooks send the same body as `GET /cases/{case_id}`. [Docs](https://app.holofin.ai/docs/api/v1.1/cases/). * HoloLang: arrays and quantifiersRules can now work on arrays: per-element aggregates, `ALL()`, `ANY()`, `IN`, indexed paths and `ARRAY_*` functions, and `@document` passes the whole payload to a rule. Note that `ALL()` on an empty array is `false`. [Docs](https://app.holofin.ai/docs/api/v1.1/hololang/syntax/#aggregate-functions). * Health endpoint`GET /api/v1.1/health` reports whether Holofin is accepting and processing work, overall and for `api`, `document_intake`, `processing` and `webhooks`. It needs no API key. It returns `503` only during an outage; `degraded` returns `200`. [Docs](https://app.holofin.ai/docs/api/v1.1/health/). * Volumes and throughput in one call`GET /api/v1.1/metrics` returns both blocks for a window; add `include=volumes` or `include=throughput` for one. `/metrics/volumes` and `/metrics/throughput` are unchanged. [Docs](https://app.holofin.ai/docs/api/v1.1/metrics/). August 2026 ----------- * **Behaviour change:** timestamps normalizedEvery API timestamp now ends in `Z`. Workflow-run timestamps previously carried an invalid `+00:00Z` suffix. * **Behaviour change:** 2D-Doc address enrichment movedThe address resolved from a 2D-Doc seal is now under `enrichments..` (`status`, `normalized`, `ban_id`, `granularity`). `enrichments.ddoc_` is no longer written: if you read it, switch to the new path. Stored rules were migrated automatically. [Docs](https://app.holofin.ai/docs/api/v1.1/enrichment/#addresses). * 2D-Doc schema fieldType a schema property `"x-holofin-type": "2ddoc"` to capture only 2D-Doc seals instead of every barcode on the page. Records and seal verification are the same as for `barcode` fields, which are unchanged. One per schema, at root level, with JSON type `"array"`. [Docs](https://app.holofin.ai/docs/api/v1.1/extraction/). * Identity documents: in-house verification optionIdentity-document agents can use an in-house read of the document instead of the external provider. It is opt-in per agent and returns the same `checks[]` with `type: "id_verification"`. * Identity documents: recapture and specimen detectionTwo new warnings in `checks[]` (`type: "id_verification"`): `ID_SPECIMEN_MATCH` when the document matches a published specimen card, and `ID_RECAPTURE_SUSPECTED` when the image metadata points to a photo of a screen. Neither raises `ERROR`. * **Behaviour change:** in-house identity document checksAn identity document now reports `validation_status: "ERROR"` instead of `OK` when its printed zone disagrees with the MRZ, or when the CEV seal on a 2021 French CNI fails signature verification or its sealed MRZ differs from the document. Holofin also checks that issue and expiry dates are coherent. Results appear in `checks[]` with `type: "id_verification"`. New `validation_codes`: `ID_CROSS_*_MISMATCH`, `ID_CROSS_CHECKS_VALID`, `ID_CROSS_NOT_CHECKED`, `ID_CEV_SIGNATURE_VALID`, `ID_CEV_SIGNATURE_INVALID`, `ID_CEV_MRZ_MISMATCH`, `ID_CEV_MISSING`, `ID_CEV_UNRESOLVED`, `ID_CEV_NOT_CHECKED`, `ID_DATES_INCOHERENT`, `ID_VALIDITY_WINDOW_UNUSUAL`. * Runs with no matching routeA run that completes without extraction because no workflow route matched the detected document type now says so: `no_route_matched` is `true` and `unmatched_classifications` lists the detected types. Both fields are on `GET /workflow-runs/{run_id}` and the `workflow-run.completed` webhook. Before, such a run looked like any other completed run. [Docs](https://app.holofin.ai/docs/api/v1.1/workflow-runs/). * Company registry check on `siren` fieldsProperties typed `siren` in the Schema Builder are looked up in the French company registry before validation rules run (a printed SIRET is accepted). New `business` codes: `SIREN_RESOLVED`, `SIREN_NOT_FOUND`, `SIREN_ENRICHMENT_UNAVAILABLE`. [Code registry](https://app.holofin.ai/docs/api/v1.1/document-schemas/#validation-codes). * **Behaviour change:**`siren` failures raise `ERROR`A part carrying a `siren` field can now report `validation_status: "ERROR"` when the registry does not recognise the number, or when the lookup could not run. Schemas without a `siren` field are unaffected. * PDF downloads`GET /workflow-runs/{run_id}/pdf` returns the uploaded document; `GET /workflow-runs/{run_id}/parts/{part_key}/pdf` returns one part's pages merged. Both redirect (`302`) to a signed URL valid ~10 minutes, or return `url` and `expires_at` with `?format=json`. Run details and webhooks also carry `document_pdf_url` and a per-part `pdf_url`. * New `checks_summary` objectOne rolled-up verdict per rule category (`extraction`, `business`, `id_verification`): `OK`, `WARNING`, `ERROR`, or `null` when the category ran no checks. `validation_status` stays the combined verdict to gate on. * Full rule inventory in `checks`Each part's `checks` now lists every rule that ran (passed, failed and skipped) with `type`, code, label, status, severity and a failure message. Codes join directly to `validation_codes`, which is unchanged. Opt-in details add `formula`, `values`, `expected`, `actual` and `tolerance`. * Identity document typeIdentity-document parts carry a flat `document_type` in `data` (the verification provider's classification, for example `"Passport"`). Omitted when verification did not run or could not classify; a `document_type` you define yourself wins. * **Behaviour change:** declined identity checks raise `ERROR`Identity verification results carry real severities under `type: id_verification`. A *Declined* document now reports `validation_status: "ERROR"` instead of `OK`. The same `ID_*` tokens appear in `validation_codes`. --- July 2026 --------- * Rule categories in `checks`Every `checks` entry declares a `type`: **extraction** rules judge whether the document was read correctly (formats, arithmetic, checksums), **business** rules whether it is acceptable under your policy (freshness, name matching). `validation_status` and `validation_codes` keep their meaning, so no integration needs to change. * TIFF upload support`document_b64` and `document_pages_b64` accept TIFF alongside PNG and JPG. Each frame of a multi-page TIFF becomes one document page. * New `_NOT_PERFORMED` codesFinancial statement rules whose source data is absent no longer fail validation. The segment stays `OK` and traces the skip in `validation_codes`. [Code registry](https://app.holofin.ai/docs/api/v1.1/document-schemas/#validation-codes). * OCR confidence scores (opt-in)Per-field `ocr_confidence` (`min`, `mean`, `engine`) on document properties and transactions. Off by default, contact us to enable. [Details](https://app.holofin.ai/docs/api/v1.1/document-schemas/#ocr-confidence). --- June 2026 --------- * PNG / JPG upload support`document_b64` accepts a PNG or JPG image in addition to PDF. Images are converted to PDF server-side. * New `document_pages_b64` fieldSend several images as an array to combine them into the pages of one document, in order. Provide either `document_b64` or `document_pages_b64`, not both. --- May 2026 -------- * New `validation_codes` fieldEach `document_parts` entry carries stable upper-snake tokens describing decisions taken during validation, whether they fired an error, a warning or a neutral note. Additive and always present, possibly empty. [Code registry](https://app.holofin.ai/docs/api/v1.1/document-schemas/#validation-codes). * Opening-balance waiversSavings and securities statements (compte titres, PEA, CTO) with no opening balance are accepted as `OK` and traced with `SAVING_ACCOUNT_WO_START_BALANCE_IS_EXPECTED` or `SECURITIES_ACCOUNT_WO_START_BALANCE_IS_EXPECTED`. --- April 2026 ---------- * New bank account fields[Bank statements](https://app.holofin.ai/docs/api/v1.1/document-schemas/#bank-statement) expose `advisor_name` (conseiller) and `branch_name` (agence). Both nullable, defaulting to `null`. --- February 2026 ------------- ### Fraud Analysis A new Fraud Analyzer workflow node checks each document for tampering, metadata anomalies and edits. It returns risk factors and trust factors in the workflow run response and in webhooks. [Read the documentation](https://app.holofin.ai/docs/api/v1.1/fraud-analysis/) * [MCP Server](https://app.holofin.ai/docs/api/v1.1/ai-assistants/)The `holofin-mcp` package lets AI coding assistants (Claude Code, Cursor, Windsurf) query workflow runs, extraction results and fraud analysis over the Model Context Protocol. * [Fraud Analysis API](https://app.holofin.ai/docs/api/v1.1/fraud-analysis/)New documentation page covering the fraud analysis schema, risk levels, risk factors and trust factors. `GET /workflow-runs/{run_id}` returns a root-level `fraud_analysis_result`, and `node-run.completed` events for `fraudAnalyzer` nodes carry risk level, score, signal count and triggered categories. * [Per-key IP allowlisting](https://app.holofin.ai/docs/api/v1.1/authentication/#ip-allowlisting)Restrict an API key to specific IPv4/IPv6 addresses or CIDR ranges. Requests from unlisted IPs are rejected with `403`. * [API key expiration](https://app.holofin.ai/docs/api/v1.1/authentication/#key-expiration)Set an expiry when creating a key (presets or a custom date). Expired keys return `403`. --- January 2026 ------------ The [Workflows API](https://app.holofin.ai/docs/api/v1.1/workflows/): custom document pipelines combining classification, segmentation and extraction, built in a visual editor. ### New Endpoints * POST [Create Workflow Run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/)Submit documents to a workflow. * GET [Get Workflow Run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run)Status and results of an execution. * GET [List Node Runs](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#list-node-runs)Per-node execution detail. * POST [Cancel Workflow Run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#cancel-workflow-run)Stop a pending or running execution. * EVENT [Node-level webhooks](https://app.holofin.ai/docs/api/v1.1/webhooks/#node-events)`node-run.completed` and `node-run.failed` events, with node results and workflow context. ### Removed * **Behaviour change:** extraction endpoints retired`POST /extract`, `GET /jobs/{job_id}`, `GET /documents` and `GET /documents/{document_id}` are gone from v1 and v1.1. Send documents with `POST /workflow-runs/` and read results with `GET /workflow-runs/{run_id}`. [Docs](https://app.holofin.ai/docs/api/v1.1/workflow-runs/). --- Version 1.1 November 12, 2025 ----------------------------- * List DocumentsNew `GET /api/v1.1/documents` endpoint, paginated, returning a `document_parts` array so sub-documents are directly accessible. Removed in January 2026. * [Flat document parts](https://app.holofin.ai/docs/api/v1.1/document-schemas/)Each part carries `part_type` directly (for example `"CERFA-2033-A"`), replacing the previous wrapper pattern. Support added for [many CERFA forms](https://app.holofin.ai/docs/api/v1.1/document-schemas/) across simplified and normal tax regimes. * [Field renames](https://app.holofin.ai/docs/api/v1.1/document-schemas/)`coherence_check_status` becomes `validation_status` (`OK` / `WARNING` / `ERROR`). Document-level review fields become `review_status`, `reviewed_by_user`, `reviewed_at`, `rejected_by_user`, `rejected_at`, with values `REVIEWED` / `NOT_REVIEWED` / `REJECTED`. --- Version 1.0.1 September 26, 2025 -------------------------------- * [Financial statements (FR)](https://app.holofin.ai/docs/api/v1.1/document-schemas/)French financial statement extraction, including CERFA 2033-A. Responses and webhooks carry activity logs for a full audit trail of document changes. * [Usage stats](https://app.holofin.ai/docs/api/v1.1/usage/)Track API usage and extraction consumption. --- Version 1.0.0 January 10, 2025Initial Release --------------------------------------------- First release: core document extraction with webhook support. * POST Extract DocumentUpload a document for extraction. Removed in January 2026. * GET Check Job StatusMonitor extraction progress. Removed in January 2026. * GET Get ResultsRetrieve extracted, structured data. Removed in January 2026. * EVENT [Webhook notifications](https://app.holofin.ai/docs/api/v1.1/webhooks/)Real-time updates when processing completes. --- ## Processing ### Workflows URL: https://app.holofin.ai/docs/api/v1.1/workflows/ Workflows ========= v1.1 Every document you send to Holofin is processed by a workflow: a configured sequence of steps that classify, segment and extract data from it. This page covers nodes, routing, and running a workflow through the API. What are Workflows? ------------------- A workflow is a reusable document processing pipeline that you configure once and execute via a single API call. Instead of calling separate APIs for classification, segmentation, and extraction, you submit a document to a workflow and receive structured results. A workflow can combine these steps: * Classification: identify the document type (bank statement, invoice, tax form) * Segmentation: split multi-document PDFs into individual documents * Extraction: pull structured data based on document type * Validation: optional human review gates for quality control How Workflows Work ------------------ When you submit a document to a workflow, it flows through connected nodes. Each node performs a specific task and routes the document to the next step based on configurable conditions. Document Fraud Analysis Classify Segment Extract Results A typical workflow: classify the document, segment if needed, extract structured data Workflow Nodes -------------- Each step in a workflow is called a node. Nodes are connected by edges that define the execution order and routing conditions. | Node Type | Purpose | Example | | --- | --- | --- | | Fraud Analyzer | Runs forensic fraud analysis on the document | Detect tampering, metadata anomalies, suspicious edits | | Classifier | Categorizes documents into types | Identify as "Bank Statement" or "Invoice" | | Segmenter | Splits multi-document packages | Extract individual forms from a tax package | | Extractor | Extracts structured data | Pull transactions from a bank statement | | Human Review | Pauses for human approval | Verify low-confidence extractions | Conditional Routing ------------------- Workflows support branching logic to handle different scenarios. Connect nodes with conditional edges to route documents based on classification results, confidence scores, or other criteria. | Condition Type | Description | Use Case | | --- | --- | --- | | Classification match | Route based on detected document type | Send invoices to invoice extractor, statements to statement extractor | | Confidence threshold | Route based on AI confidence score | Send low-confidence results to human review | | If/Else branching | Split flow into multiple paths | Process single-page vs multi-page documents differently | | Default fallback | Catch-all when no conditions match | Route unrecognized documents to manual processing | Configure routing in the workflow builder by clicking on edges between nodes and setting conditions. No code is required. Executing a Workflow -------------------- To process a document, you create a workflow run via the API. The run tracks the execution state as your document moves through each node. Create a workflow runPOST ``` curl -X POST https://app.holofin.ai/api/v1.1/workflow-runs/ \ -H "X-API-Key: $HOLOFIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_abc123", "document_b64": "JVBERi0xLjQK...", "document_name": "mydocument.pdf" }' ``` See the [Workflow Runs API](https://app.holofin.ai/docs/api/v1.1/workflow-runs/) for complete documentation on request parameters, response format, and managing workflow executions. Workflow Status --------------- A workflow run progresses through these states: pendingrunningcompleted Runs may also be paused (waiting for approval), failed, or cancelled Getting Results --------------- Once a workflow run completes, you can retrieve the aggregated results from all extraction nodes: Get workflow run resultsGET ``` curl https://app.holofin.ai/api/v1.1/workflow-runs/wr_def456 \ -H "X-API-Key: $HOLOFIN_API_KEY" ``` See the [Get Workflow Run API](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run) for complete response format and result structure. Using Webhooks -------------- Instead of polling for status, configure a [webhook](https://app.holofin.ai/docs/api/v1.1/webhooks/) to receive notifications when a workflow run completes. The webhook payload includes the full extraction results. Next Steps ---------- [Workflow Runs APICreate, monitor, and cancel workflow runs](https://app.holofin.ai/docs/api/v1.1/workflow-runs/)[WebhooksGet notified when processing completes](https://app.holofin.ai/docs/api/v1.1/webhooks/)[SegmentationLearn about document splitting](https://app.holofin.ai/docs/api/v1.1/segmentation/)[ClassificationLearn about document categorization](https://app.holofin.ai/docs/api/v1.1/classification/) --- ### Classification URL: https://app.holofin.ai/docs/api/v1.1/classification/ Document Classification ======================= v1.1 The classifier identifies the type of each PDF you send and returns a confidence score and metadata tags. Workflows use the result to route the document. [Overview](#overview) --------------------- The Classifier is a workflow node that reads a document and assigns it one of the classes you define. You configure your Classifier Agent with the document classes you need: system classes such as bank statements and tax forms, custom classes of your own, or both. Document PDF file Classifier Agent Your custom classes Bank Statement Confidence:98% [Classifier Agent](#classifier-agent) ------------------------------------- A Classifier Agent is the building block for document classification in your workflows. Each agent is configured with: ### Classes Configuration Define which document types your classifier can identify. System and custom classes can be mixed. ### Classification Rules Add global rules to guide classification logic, handle edge cases, and disambiguate between similar document types. ### Confidence Scores AI confidence (0-1) indicating classification certainty. Route documents to human review when confidence is low. ### Workflow Routing Use classification results to route documents to different extractors or processing steps in your workflow. [System Classes](#system-classes) --------------------------------- System classes come with tuned classification prompts. Add them to your Classifier Agent for common financial document types: | Class | Description | | --- | --- | | bank\_statement | Monthly or quarterly bank account statements with transactions | | fr\_bilan\_actif | French balance sheet - Asset side | | fr\_bilan\_passif | French balance sheet - Liability side | | fr\_compte\_resultat | French income statement | | fr\_cerfa\_2033\_a | French tax form - Simplified balance sheet (assets) | | fr\_cerfa\_2050 | French tax form - Full accounting balance sheet (assets) | | de\_susa | German trial balance (Summen- und Saldenliste) | | be\_interne\_jaarrekening\_bilan\_actif | Belgian internal annual accounts - Balance sheet (Activa) | | be\_interne\_jaarrekening\_bilan\_passif | Belgian internal annual accounts - Balance sheet (Passiva) | | be\_interne\_jaarrekening\_compte\_resultat | Belgian internal annual accounts - Income statement (Resultatenrekening) | System classes automatically expand to include all supported year versions (e.g., `fr_cerfa_2050` includes 2023, 2024, 2025 variants). ### [Classification tell-tales: BE Interne Jaarrekening](#be-interne-jaarrekening-telltales) Belgian *interne jaarrekening* documents are not deposited at the NBB and vary in layout. The classifier relies on a combination of signals to distinguish them from French balance sheets or NBB-deposited accounts: * **Dutch financial vocabulary**: *Balans*, *Activa* / *Passiva*, *Resultatenrekening*, *Boekjaar*, *Eigen vermogen*, *Vaste activa*, *Vlottende activa*, *Handelsvorderingen*, *Handelsschulden*, *Omzet*, *Bedrijfsresultaat*, *Winst of verlies*. * **NBB rubric codes in the line items**: composite codes like `20/58`, `21/28`, `29/58`, `10/15`, `10/49`, `42/48`, `170/4`, `60/66A`, `70/66`, `70/67`, `600/8`, `640/8`, `630/1`. These are unique to the Belgian PCMN. * **Schema type markers**: explicit mentions of *micro-schéma* / *microschema*, *schéma abrégé* / *verkort schema*, or *schéma complet* / *volledig schema* in headers or footers. Maps to the `schema_type` metadata (`micro`, `abrege`, `complet`). * **Belgian VAT format**: `BE0123456789` (BTW-nummer / numéro TVA) near the company header. * **Absence of NBB deposit markers**: no *"Jaarrekening neergelegd bij de Nationale Bank van België"* footer, no 8-digit NBB deposit number. Distinguishes *interne* from *neergelegde* accounts. * **Bilingual (FR/NL) column headers**: many internal templates expose both languages side-by-side, unlike purely French or purely Dutch NBB filings. [Custom Classes](#custom-classes) --------------------------------- Create custom classes for document types specific to your business. Each custom class needs: * **Name** - A clear, descriptive identifier (e.g., "Supplier Invoice", "Quarterly Report") * **Description** - Instructions for the AI on how to identify this document type The more detailed your description, the more accurately the classifier will identify your documents. Include visual cues, structural patterns, and distinguishing characteristics. [Result Structure](#result-structure) ------------------------------------- When classification completes, the result includes the identified document type and confidence score. ### Classification Result ``` { "classification": "bank_statement", "confidence": 0.95, "reasoning": "Document contains bank account header with IBAN, date range, and transaction list with debits/credits columns typical of bank statements" } ``` ### Result Fields | Field | Type | Description | | --- | --- | --- | | classification | string | The class name matching one of your configured classes | | confidence | number | AI confidence score between 0 and 1 | | reasoning | string | AI explanation for the classification decision | [API Configuration](#api-configuration) --------------------------------------- When using the [Workflow Runs API](https://app.holofin.ai/docs/api/v1.1/workflow-runs/), classifier node results include the agent configuration used for the classification. ### Classifier Node Run ``` { "node_type": "classifier", "config": { "classifier_id": "ca_8kL2mNpQ4xYz", "classifier_name": "Invoice Classifier", "classes_config": [ { "name": "invoice", "is_system_type": true, "system_type_key": "invoice" }, { "name": "receipt", "is_system_type": false, "description": "Purchase receipts from retail stores" }, { "name": "contract", "is_system_type": false, "description": "Legal agreements and contracts" } ], "classifier_rules": "When in doubt between invoice and receipt, check for tax ID presence" }, "results": { "classification": "invoice", "confidence": 0.95, "reasoning": "Document contains invoice header, line items, and total amount" }, "status": "completed", "started_at": "2024-01-15T10:30:01Z", "completed_at": "2024-01-15T10:30:03Z", "duration_ms": 2000 } ``` ### Config Fields | Field | Type | Description | | --- | --- | --- | | classifier\_id | string | Unique identifier for the Classifier Agent (prefix: `ca_`) | | classifier\_name | string | Display name of the Classifier Agent | | classes\_config | array | Class definitions for the classifier | | classes\_config[].name | string | Class name identifier | | classes\_config[].is\_system\_type | boolean | Whether this is a pre-built system class | | classes\_config[].system\_type\_key | string | System type key (present when `is_system_type` is true) | | classes\_config[].description | string | Custom description (present when `is_system_type` is false) | | classifier\_rules | string | Global rules to guide classification logic and handle edge cases | [Back to Overview](https://app.holofin.ai/docs/api/v1.1/) --- ### Segmentation URL: https://app.holofin.ai/docs/api/v1.1/segmentation/ Document Segmentation ===================== v1.1 Segmentation splits a PDF that holds several documents into individual documents. It finds each document's page boundaries and partition key (for example an IBAN), and each resulting document is processed separately downstream. [Overview](#overview) --------------------- Segmentation splits a PDF that merges several documents (bank statements, invoices, financial forms) into individual documents. It finds the boundaries between them and creates one file per document. Page classification only labels the pages of a single document. Segmentation creates new PDF files and keeps a link from each one to the parent file it came from. Multi-page PDF 10 pages Segmenter AI-powered splitting Segment 1 Pages 1-3 Segment 2 Pages 4-6 Segment 3 Pages 7-10 [Key Concepts](#key-concepts) ----------------------------- ### Section Types Define the categories of documents the segmenter can identify. Each section type has a name and description that guides the AI classification. ### Partition Keys Unique identifiers (like IBAN or account number) used to group pages belonging to the same logical document, even when interleaved. ### Confidence Scores AI confidence (0-1) in segment classification. Configure minimum thresholds to control when automatic segmentation occurs. ### Automatic Routing Segments can be automatically routed to specific extractors based on their type, so each document goes to the extractor built for it. [Use Cases](#use-cases) ----------------------- 1 ### Bank Statements with Multiple Accounts Split a combined PDF containing statements for different bank accounts. Input 10-page PDF with 3 different bank accounts Segmentation AI identifies IBANs as partition keys Output 3 separate PDF files, one per account 2 ### Multi-Document Financial Packet Split a combined document containing different document types. Input PDF with invoice (pp 1-2), bank statement (pp 3-5), contract (pp 6-8) Segmentation AI classifies by document type Output 3 separate documents routed to appropriate extractors [Result Structure](#result-structure) ------------------------------------- Segmentation output is returned by `GET /api/v1.1/workflow-runs/{run_id}` in the `segmentation_result` object, which holds two views of the same result. Use whichever suits your integration. No extra call is needed. There is no separate top-level `splits` field. For the document grouping, use **`segmentation_result.sections`**: one entry per identified document, with the page range of each. Each entry *is* a document; you group by iterating `sections`, not by `partition`. ### Per-section view (document grouping) `segmentation_result.sections`: one entry per identified document, with the list of `pages` that belong to it. This is the view to use when you need each document's page range. Adjacent documents of the same type stay separate entries here (the per-page view below cannot distinguish them). ### segmentation\_result ``` { "segmentation_result": { "sections": [ {"section_id": "identity_document", "pages": [1], "partition_key": null}, {"section_id": "invoice", "pages": [2, 3], "partition_key": null}, {"section_id": "bank_details", "pages": [4], "partition_key": "FR7630004020360001026054720"} ], "pages": [ {"page": 1, "section": "identity_document", "partition": null}, {"page": 2, "section": "invoice", "partition": null}, {"page": 3, "section": "invoice", "partition": null}, {"page": 4, "section": "bank_details", "partition": "FR7630004020360001026054720"} ] } } ``` ### Per-page view `segmentation_result.pages`: a flat list with one entry per page, giving the identified `section` type and the `partition` identifier (e.g. an IBAN) when one is detected. Convenient for page-level lookups, but it does not preserve document boundaries between adjacent same-type documents; use `sections` for that. The same `sections` shape is also available per node (with the segmenter's `confidence` and `reasoning`) on `GET /api/v1.1/workflow-runs/{run_id}/node-runs/`. ### Segmenter node results (per section) ``` { "results": { "confidence": 0.96, "reasoning": "Page 1 is an identity document, pages 2-3 an invoice, page 4 a bank details form (RIB).", "sections": [ {"section_id": "identity_document", "pages": [1], "partition_key": null}, {"section_id": "invoice", "pages": [2, 3], "partition_key": null}, {"section_id": "bank_details", "pages": [4], "partition_key": "FR7630004020360001026054720"} ], "pages": [ {"page": 1, "section": "identity_document", "partition": null}, {"page": 2, "section": "invoice", "partition": null}, {"page": 3, "section": "invoice", "partition": null}, {"page": 4, "section": "bank_details", "partition": "FR7630004020360001026054720"} ] } } ``` ### Per-page fields (`segmentation_result`) | Field | Type | Description | | --- | --- | --- | | pages | array | One entry per page in the document, sorted by page number. | | pages[].page | integer | 1-indexed page number. | | pages[].section | string | Section type identifier for the page (e.g., "invoice", "bank\_details"). | | pages[].partition | string | null | Partition key value (e.g., IBAN). `null` when no partition key is configured or none is detected. | ### Per-section fields (`segmentation_result.sections`) | Field | Type | Description | | --- | --- | --- | | sections | array | One entry per identified document (the page-grouping view). Group by iterating this array. | | sections[].section\_id | string | Section type identifier for the document. | | sections[].pages | array | 1-indexed page numbers belonging to this document. | | sections[].partition\_key | string | null | Partition key value (e.g., IBAN). Only distinguishes documents of the *same* type; not needed for grouping. `null` when no partition key is configured or none is detected. | The `/node-runs/` endpoint returns the same `sections` array per segmenter node, together with the segmenter's `confidence` and `reasoning`. See the [Workflow Runs API](https://app.holofin.ai/docs/api/v1.1/workflow-runs/) reference for the full node object. [How It Works](#how-it-works) ----------------------------- Segmentation is enabled in document processing workflows. When activated, the segmenter analyzes each page of the document using AI to: 1. **Classify pages:** Identify the document type for each page 2. **Detect boundaries:** Find where one document ends and another begins 3. **Identify partitions:** Group pages by unique identifiers (when configured) 4. **Create segments:** Generate new PDF files for each identified segment 5. **Route for processing:** Send segments to appropriate extractors **System Segmenter:** For bank statements, a system segmenter automatically splits documents by bank account when this feature is enabled for your organization. [API Configuration](#api-configuration) --------------------------------------- When using the [Workflow Runs API](https://app.holofin.ai/docs/api/v1.1/workflow-runs/), the `/node-runs/` endpoint returns each segmenter node with both the agent configuration used and the segmentation results. ### Segmenter Node Run ``` { "node_type": "segmenter", "status": "completed", "agent_id": "sa_3vRt7wXp9kLm", "is_global_agent": false, "config": { "segmenter_name": "Bank Account Splitter", "custom_rules": null, "prompt": null, "sections": [ { "section_id": "bank_account", "section_description": "Bank statement pages with transactions", "partition_key": "iban", "partition_description": "IBAN identifying the account" } ] }, "results": { "confidence": 0.95, "reasoning": "All pages belong to a single account identified by its IBAN.", "sections": [ {"section_id": "bank_account", "pages": [1, 2, 3], "partition_key": "FR7630004020360001026054720"} ], "pages": [ {"page": 1, "section": "bank_account", "partition": "FR7630004020360001026054720"}, {"page": 2, "section": "bank_account", "partition": "FR7630004020360001026054720"}, {"page": 3, "section": "bank_account", "partition": "FR7630004020360001026054720"} ] }, "started_at": "2024-01-15T10:30:03Z", "completed_at": "2024-01-15T10:30:08Z", "duration_ms": 5000 } ``` For a **Global (system) Segmenter**, `config` is `null` and `is_global_agent` is `true`; the `results` block is populated the same way. ### Config Fields | Field | Type | Description | | --- | --- | --- | | segmenter\_name | string | Display name of the Segmenter Agent. | | custom\_rules | string | null | Custom segmentation rules, if defined. | | prompt | string | null | Custom prompt, if defined. | | sections | array | null | Section type definitions. | | sections[].section\_id | string | Section type identifier. | | sections[].section\_description | string | Description guiding the AI in identifying this section type. | | sections[].partition\_key | string | Partition key used to group pages of the same document (e.g., `iban`). Empty when not configured. | | sections[].partition\_description | string | Description of the partition key. | [Back to Overview](https://app.holofin.ai/docs/api/v1.1/) --- ### Document Extraction URL: https://app.holofin.ai/docs/api/v1.1/extraction/ Document Extraction =================== v1.1 Send a financial document and get back validated JSON: transactions, balances, field values, and the checks run against them. Supported inputs: PDF, PNG, JPG, and TIFF. Send a single file as `document_b64`, or send several images as `document_pages_b64` (an array) to combine them into the pages of one document. Images are converted to PDF server-side. Extraction returns named fields, tables and transactions rather than raw text, and validates the data for consistency. Each document type (like `bank-statement`, `CERFA-2033-A`, or `bilan-actif`) returns data in its own standardized format. Document Fraud Analysis warning Classifier bank\_statement Segmenter 3 segments Extractor bank-statement Structured JSON [How Extraction Works](#how-it-works) ------------------------------------- Extraction automatically identifies the document type and applies the appropriate schema to extract structured data. Each document type has its own extraction template that defines which fields are read and how the results are formatted. The extracted data is then validated for consistency. For example, bank statement transactions must balance, and calculated fields on tax forms must match their sum totals. [Multi-Document Files](#multi-document-files) --------------------------------------------- If your PDF contains multiple documents (for example, several bank statements or a mix of different financial forms), the API returns all extracted data in a single response. Each document becomes a separate item in the `document_parts` array. Your file:combined\_docs.pdf (10 pages) Extracted as: • Bank statement (pages 1-5) • Balance sheet (pages 6-8) • Income statement (pages 9-10) API returns:One response with 3 document\_parts Each document part includes: * Its own `part_type` identifying the document schema * A `validation_status` showing if the data passed consistency checks * A `pages` array showing which pages from your original file this document came from * The extracted `data` in the format specific to that document type ### Multi-Segment Extraction Result ``` { "extraction_id": "wea_E58wA3KMSneeh85z", "document_name": "combined_financial_docs.pdf", "extraction_date": "2025-01-15T10:30:00Z", "document_parts": [ { "key": "segment_1", "part_type": "bank-statement", "label": "Qonto - Account FR76...", "validation_status": "OK", "validation_codes": [], "checks": [ { "type": "extraction", "code": "BALANCE_EQUATION", "label": "Balance equation", "description": null, "status": "passed", "severity": "error", "message": null, "reason": null } ], "pages": [1, 2, 3, 4, 5], "data": { "bank_name": "Qonto", "transactions": [...] } }, { "key": "segment_2", "part_type": "CERFA-2033-A", "label": "Simplified Balance Sheet 2024", "validation_status": "WARNING", "validation_codes": [], "checks": [ { "type": "extraction", "code": "ACTIF_TOTAL_BRUT", "label": "Total actif brut", "description": null, "status": "skipped", "severity": "warning", "message": null, "reason": "column not present on the document" } ], "pages": [6, 7, 8], "version": "2024", "data": { "028": { "value": 60571, "label": "Immobilisations corporelles" } } } ] } ``` [Document Part Types](#document-part-types) ------------------------------------------- Each document part has a `part_type` field that identifies its schema. The part type determines what fields are extracted, how data is structured, and which validation rules apply. Use these identifiers when configuring extractors or processing API responses. | part\_type | Description | Key Fields | Validation | | --- | --- | --- | --- | | bank-statement | Bank account statements with transactions and balances | `transactions[]`, `start_balance`, `end_balance` | Balance equation checks | | utility\_bill | Utility bills (electricity, gas, water) with consumption periods | `consumptions[]`, `dp_ref`, `fluid` | Unit conversion, total consistency | | bilan-actif | French balance sheet - Assets side | Field codes with gross/depreciation/net values | Field relationships, sum validations | | bilan-passif | French balance sheet - Liabilities side | Field codes for equity, liabilities | Balance sheet equation checks | | compte-resultat | French income statement | Revenue, expenses, profit/loss items | P&L equation validations | | CERFA-2033-A | Simplified balance sheet (Regime Simplifie) | Field codes with `_net_n` and `_net_n1` suffixes | Year-over-year consistency | | CERFA-2033-B | Simplified income statement | Revenue and expense field codes | P&L sum checks | | CERFA-2033-C | Fixed assets and depreciation schedule | Asset movements, depreciation details | Depreciation calculations | | CERFA-2050 | Balance sheet - Assets (Regime Reel Normal) | Detailed asset field codes | Asset sum validations | | CERFA-2051 | Balance sheet - Liabilities (Regime Reel Normal) | Detailed liability field codes | Liability sum validations | | CERFA-2052 | Income statement (Regime Reel Normal) | Revenue and expense details | Revenue/expense checks | | CERFA-2053 | Income statement continued (Regime Reel Normal) | Additional P&L line items | Continuation consistency | | de-susa | German trial balance (Summen- und Saldenliste) | Account number, debit/credit sums | Trial balance equation | For detailed field specifications and complete schemas, see the [Document Schemas](https://app.holofin.ai/docs/api/v1.1/document-schemas/) page. [Schema Mapping](#schema-mapping) --------------------------------- The `part_type` value directly maps to a JSON schema that defines the structure of extracted data. Each schema specifies required fields, data types, and nested structures. [Validation & Consistency Checks](#validation) ---------------------------------------------- Extracted data is automatically validated using type-specific consistency rules. Validation results appear in the `validation_status` field for each document part. Each document part also carries `checks`: the full inventory of every check that ran against the part (passed, failed, or skipped) in one normalized entry shape. A passing document therefore shows *what* was verified, so forty arithmetic reconciliations and a single presence check can be told apart. Every entry declares its category in `type`: * **`extraction`** rules judge whether the document was read correctly (field formats and lengths, arithmetic totals, checksums). A failure means the returned data may be wrong. * **`business`** rules judge whether the document is acceptable under your own policy (for example, a proof of address no older than six months, or a name that must match the account holder). A failure means the data is correct but the document does not meet your requirements. ``` { "validation_status": "ERROR", "validation_codes": ["ID_TOO_OLD"], "checks": [ { "type": "extraction", "code": "ACTIF_TOTAL_BRUT", "label": "Total actif brut", "description": "Somme des postes d'actif brut", "status": "passed", "severity": "error", "message": null, "reason": null, "formula": "@AA + @AB + @AC = @AT", "values": {"AA": 12000.0, "AB": 3400.0, "AC": 0.0}, "expected": 15400.0, "actual": 15400.0, "tolerance": 10.0 }, { "type": "business", "code": "ID_TOO_OLD", "label": "Document freshness", "description": "Document must be dated within the last 6 months", "status": "failed", "severity": "error", "message": "Document dated 2025-09-14, older than 6 months", "reason": null } ] } ``` * `validation_status` and `validation_codes` remain the combined verdict of both categories and their behaviour is unchanged, so existing integrations do not need to be updated. Read the failed entries' `type` when you need to tell "we may have misread this" apart from "the data is right but the document is not acceptable". * For a per-category verdict without walking the entries, the top level of the response carries `checks_summary`: `{"extraction": ..., "business": ..., "id_verification": ...}`, each `OK`, `WARNING`, `ERROR` or `null` (that category ran no checks, which is not the same as `OK`). * `status` says what happened: `passed`, `failed`, or `skipped` (the document does not carry the data the rule needs; `reason` explains). `severity` says how loud a failure would be: a passing `error`-severity rule tells you the check *can* block. * `code` is the same token that appears in `validation_codes`, so the two can be joined directly. `message` is set only on failures. * The last five keys (`formula`, `values`, `expected`, `actual`, `tolerance`) are returned only for organizations with **rule details** enabled; ask your Holofin contact. Keys are omitted, never `null`, when unavailable; absence uniformly means "not available". ### Validation Status Values | Status | Meaning | Example | | --- | --- | --- | | OK | All consistency checks passed | Balance equation matches within tolerance | | WARNING | Soft issues detected, data likely usable | Minor formatting inconsistencies, missing optional fields | | ERROR | Validation errors detected | Balance mismatch, required fields missing, calculation errors | | null | No validation rules apply | Document type has no consistency checks configured | [Extraction Result Structure](#result-structure) ------------------------------------------------ The extraction API returns a standardized JSON structure with root-level metadata and a `document_parts` array containing extracted data for each segment. ### Root Level Fields | Field | Type | Description | | --- | --- | --- | | extraction\_id | string | Unique document identifier | | document\_name | string | Original filename | | document\_url | string | Absolute URL to view/edit extraction in Holofin app | | category | string | Document category from classification (e.g., `BANK_STATEMENT`, `UTILITY_BILL`, `FINANCIAL_STATEMENT`) | | extraction\_date | string | ISO 8601 timestamp of extraction completion | | validation\_status | string | null | Overall validation status (worst status across all parts) | | checks\_summary | object | Per-category rollup of `validation_status`. Keys `extraction`, `business` and `id_verification`, each `OK`, `WARNING`, `ERROR` or `null`. `null` means that category ran no checks, which is not the same as `OK`. Answers "did every rule of this category pass" without walking the per-part `checks` arrays; `validation_status` remains the union of the three and the field to gate on. | | document\_parts | array | Array of extracted document parts | ### Document Part Structure | Field | Type | Description | | --- | --- | --- | | key | string | Stable identifier for this document part (always `segment_N` format). Use this as a reference when working with the API. For human-readable names, use the `label` field instead. | | part\_type | string | Schema identifier (e.g., `bank-statement`, `utility_bill`, `CERFA-2033-A`) | | label | string | null | Human-readable description of this document part (e.g., account number for bank statements, form title and year for tax documents) | | validation\_status | string | null | Overall validation result for this part, combining extraction quality and business rules: `OK`, `WARNING`, `ERROR`, or `null` | | checks | array of objects | The full rule inventory for this part: one flat array of every check that ran, passed, failed and skipped alike. Each entry carries `type` (`extraction`, `business` or `id_verification`), `code`, `label`, `description`, `status` (`passed` / `failed` / `skipped`), `severity`, `message` and `reason`. **Extraction** checks judge whether the document was read correctly; **business** checks judge your own acceptance rules; **id\_verification** checks carry the identity-verification provider's verdict on identity documents. The `code` is the same token used in `validation_codes`, so the two join directly. A category that ran no checks contributes no entries. Organizations with rule details enabled additionally receive `formula`, `values`, `expected`, `actual` and `tolerance` per entry where available; absent keys uniformly mean the data is not available. | | validation\_codes | array of strings | Stable upper-snake tokens describing decisions taken during validation (e.g. `BALANCE_MISMATCH`, `SAVING_ACCOUNT_WO_START_BALANCE_IS_EXPECTED`, `SECURITIES_ACCOUNT_WO_START_BALANCE_IS_EXPECTED`). Parts whose schema declares a field typed `siren` also carry the company-registry outcome: `SIREN_RESOLVED` when the French registry recognises the number, `SIREN_NOT_FOUND` when it does not or the printed value is not a usable SIREN / SIRET, and `SIREN_ENRICHMENT_UNAVAILABLE` when the lookup could not be performed. The last two are errors. Always present, may be empty. Branch on codes for behavior; rely on `validation_status` for the rolled-up verdict. | | enrichments | object | omitted | Post-extraction sidecar keyed by field path (e.g. `address`, `issuer.address`), one entry per field typed [Address (FR)](#address-enrichment) or `siren` in the schema; each entry names its own `kind`. Absent entirely when the schema has no such fields. | | pages | array of integers | Page numbers from original document where this part appears | | version | string | null | Document version/year when applicable (e.g., `2024` for CERFA forms) | | data | object | Extracted data in schema-specific structure | ### [Address Enrichment](#address-enrichment) Fields typed `Address (FR)` in the schema builder are checked against the French national address database (BAN) after extraction. The result is attached as a sidecar under `enrichments` on the document part, keyed by the field's path (e.g. `address`, `issuer.address`, `parties[0].address`). The extracted string itself is never modified; `enrichments` sits alongside it. A document part whose schema has no `Address (FR)` field carries no address entry, and `enrichments` is omitted entirely when the schema has no enriched field of any kind. Address entries are marked `kind: "address_fr"` and carry one of three statuses: | Status | Meaning | Fields | | --- | --- | --- | | matched | Resolved to a real street or delivery point | `normalized`, `housenumber`, `street`, `postcode`, `city`, `citycode`, `ban_id`, `lat`, `lon`, `score`, `granularity` | | not\_found | BAN was checked; no match at street level or better. Also returned when BAN answered with a different street than the document's, in which case `reason` is `substituted` | `granularity`, `score`, `reason` | | unchecked | Could not be checked (BAN outage or timeout). Not a negative result | `reason` | `unchecked` means Holofin has no opinion, not that the address is suspect. Treat it the same as a field with no enrichment entry at all. `not_found` is the meaningful negative signal. `ban_id` is the stable identifier to use when matching the same address across multiple documents: it survives spelling and formatting differences that raw string comparison won't. **Read `granularity`, not just `status`.** On a `matched` entry, `granularity: "street"` means the street was confirmed but the specific house number was not. If you are screening for fabricated addresses, require `granularity: "housenumber"`. ### enrichments Example ``` { "enrichments": { "address": { "status": "matched", "normalized": "12 Avenue du General de Gaulle 92100 Boulogne-Billancourt", "housenumber": "12", "street": "Avenue du General de Gaulle", "postcode": "92100", "city": "Boulogne-Billancourt", "citycode": "92012", "ban_id": "92012_2758_00012", "lat": 48.833, "lon": 2.241, "score": 0.97, "granularity": "housenumber" }, "parties[0].address": { "status": "not_found", "granularity": "none", "score": 0.31 }, "other_address": { "status": "unchecked", "reason": "lookup_failed" } } } ``` ### Complete Example ### Full Extraction Response ``` { "extraction_id": "wea_E58wA3KMSneeh85z", "document_name": "monthly_statements.pdf", "document_url": "https://app.holofin.ai/workflow/extractor/wea_xyz123/", "category": "BANK_STATEMENT", "extraction_date": "2025-01-15T14:22:31.249137+00:00", "validation_status": "OK", "checks_summary": { "extraction": "OK", "business": null, "id_verification": null }, "document_parts": [ { "key": "segment_1", "part_type": "bank-statement", "label": "Qonto - FR76304042036000102605472", "validation_status": "OK", "validation_codes": [], "pages": [1, 2, 3, 4, 5], "data": { "bank_name": "Qonto", "currency": "EUR", "account_number": "FR76304042036000102605472", "advisor_name": "Jean Dupont", "branch_name": "Paris Opéra", "start_balance": 2450.75, "end_balance": 5825.25, "start_date": "2024-01-01", "end_date": "2024-01-31", "transactions": [ { "transaction_date": "2024-01-05", "value_date": "2024-01-05", "amount": 1500.00, "description": "Wire transfer from Client ABC", "credit": 1500.00, "debit": null } ] } }, { "key": "segment_2", "part_type": "bank-statement", "label": "Qonto - DE89370400440532013000", "validation_status": "WARNING", "pages": [6, 7, 8, 9], "data": { "bank_name": "Qonto", "currency": "EUR", "account_number": "DE89370400440532013000", "advisor_name": null, "branch_name": "Berlin Mitte", "start_balance": 10250.00, "end_balance": 10248.50, "start_date": "2024-01-01", "end_date": "2024-01-31", "transactions": [] } } ] } ``` [Custom Extractors](#custom-extractors) --------------------------------------- Custom extractors let you define your own extraction schemas for any document type. Each custom extractor returns a `part_type` matching the extractor's slug (e.g., `my-invoice-extractor`). The `data` object follows the schema you defined in the extractor builder. ### Barcode Fields Custom extractor schemas can include a barcode field that automatically detects and decodes barcodes from the document pages. This includes QR codes, Data Matrix, PDF417, Code 128, and other common formats. When present, the extraction results include an array of detected barcodes. | Field | Type | Description | | --- | --- | --- | | barcode\_type | string | Detected barcode format (e.g., `QR Code`, `Data Matrix`, `PDF417`, `Code 128`) | | page | integer | Page number where the barcode was detected | | text | string | Decoded barcode content | ### Custom extractor with barcode data ``` { "key": "segment_1", "part_type": "my-extractor", "label": "My Extractor", "pages": [1, 2], "data": { "invoice_number": "INV-2026-001", "barcodes": [ { "barcode_type": "Data Matrix", "page": 1, "text": "DC04FR04FPE3FFFF20700401FR432..." }, { "barcode_type": "QR Code", "page": 2, "text": "https://example.com/verify/abc123" } ] } } ``` Only one barcode field is allowed per custom extractor schema and it must be at root level. The barcode field detects all common barcode formats including QR codes, Data Matrix, PDF417, Code 128, and more. ### 2D-Doc Seals (French visible digital seals) The Data Matrix in the example above is a **2D-Doc**: the signed seal printed on French administrative and commercial documents: avis d'impôt, justificatifs de domicile, bulletins de salaire, RIB, relevés de compte, titres d'identité. It carries the issuer's own copy of the key data, signed with a certificate accredited by ANTS (France Titres). A payload beginning with `DC` followed by two digits is a 2D-Doc. Whenever a barcode field decodes such a payload, Holofin verifies it automatically. No configuration is required, and it works for every accredited issuer, not only public administrations. #### A dedicated 2D-Doc field A schema property can also be declared as `"x-holofin-type": "2ddoc"`. It has the same record shape and the same `minItems` requiredness as a barcode field, but only Data Matrix symbols carrying a 2D-Doc payload are kept. A QR code or an EAN on the same page is discarded rather than filling the field. Use it when the document is expected to carry a seal and the other symbols are noise; use `barcode` when you want everything the page holds. Both may be declared on the same schema, in which case a 2D-Doc appears in both fields. As with `barcode`, at most one `2ddoc` field is allowed per schema and it must sit at the root. Its JSON type must be exactly `"array"`: a page with no seal yields `[]`, never `null`. Three independent things are checked: | Check | Question it answers | | --- | --- | | Signature | Was the seal altered since the issuer signed it? Verified cryptographically against the ANTS trust list. | | Completeness | Does the payload carry every field the specification makes mandatory for its document type? A seal can verify and still be missing them, which is what a stripped and re-signed payload looks like. | | Consistency | Do the values we extracted from the page match what the issuer sealed? A mismatch means the printed document contradicts its own seal. | Verification **fails only on proof**. A document with no seal, an unknown certificate, or an unreadable payload passes, so an outage or an unlisted issuer cannot reject a genuine document. #### Document types Every seal declares the kind of document it was issued for, and that determines which fields it carries: an avis d'impôt seals declarants and amounts, a justificatif de domicile seals a postal address, a relevé de compte seals an IBAN and period balances. Checking the type catches a genuine seal lifted from a different document. One kind of document usually maps to **several** type codes, because each revision of the specification mints a new one and older documents stay valid. For example, an avis de situation déclarative is type `18`, `24` or `27` depending on when it was issued. Accept the whole family rather than a single code. The full per-type field reference lives on the [HoloLang page](https://app.holofin.ai/docs/api/v1.1/hololang/fields/#ddoc-fields-by-type), which also shows which fields are mandatory for a type and which are optional. #### Reading the result Seal outcomes reach the API through the validation fields your integration already consumes, not through a separate block. A failed check appears in the part's `validation_status` and `validation_codes`, and under `checks`. The decoded seal itself is also returned, under the part's `enrichments`, keyed by the 2D-Doc field: the verdict (`status`, `signature_valid`), the header, and the sealed content under its own names. **The signature is checked whether or not you write a rule.** Declaring a field with the `2D-Doc` type is enough: every run reports `TWODDOC_SIGNATURE_VALID`, `TWODDOC_SIGNATURE_INVALID`, `TWODDOC_UNRESOLVED`, `TWODDOC_SEAL_ABSENT` or `TWODDOC_STRUCTURE_INCOMPLETE` on its own. Only `TWODDOC_SIGNATURE_INVALID` is an error. A seal we cannot verify points to a gap in our trust store, not a fault in the document. Write a HoloLang rule when you want to go further and pin a sealed value against an extracted one. See [enriched verdict fields](https://app.holofin.ai/docs/api/v1.1/hololang/fields/#derived-fields) for the paths and the syntax. **A valid signature proves integrity, not currency.** It shows the seal was not altered since it was issued. It does not show that the document is the most recent one on file. Confirming that requires a lookup with the issuing administration, which Holofin does not perform. Activity Logs Structure ----------------------- The `activity_logs` field is the audit trail of user actions on the extracted data: mutations (data edits), comments, and validation actions. It appears in both API responses and webhook payloads. ### Activity Log Entry Structure Each activity log entry follows this structure: | Field | Type | Description | | --- | --- | --- | | id | string | Unique identifier for this activity (e.g., "mut\_abc123", "com\_xyz456") | | type | string | Activity type: `data_edit`, `comment`, or `validation` | | timestamp | string | ISO 8601 timestamp when the activity occurred | | user | object | User who performed the action, containing `id`, `name`, and optionally `external_id` (present when the user was provisioned via partner SSO) | | details | object | Type-specific details about the activity | ### Activity Types #### `data_edit` Activities Manual modifications to extracted data. The `details` object contains: * `mutation_type` - Type of change: `UPDATE`, `ADD`, `DELETE`, or `RESTORE` * `target` - What was modified: `transaction` or `property` * `location` - (For transactions) Object with `page` and `row` numbers * `property_name` - (For properties) The field identifier that was modified (e.g., `bank_name`, `028_net_n`) * `changes` - (For UPDATE) Object showing what changed with `from` and `to` values * `original_data` - (For DELETE) The data that was deleted * `new_data` - (For ADD) The data that was added #### `comment` Activities User comments added to the document. The `details` object contains: * `content` - The text content of the comment #### `validation` Activities Document validation or invalidation actions. The `details` object contains: * `action` - Either `VALIDATED` or `INVALIDATED` * `comment` - Optional reason or comment for the validation decision ### Debit/Credit Swap Corrections When a user moves a transaction amount from debit to credit (or the reverse), the UPDATE mutation shows one field changing from a value to null and the other from null to a value. **Important:** Both `debit` and `credit` fields are always included in the changes object, even when null, so swap corrections can be detected. #### Example: Debit to Credit Correction ### Debit → Credit Correction ``` { "id": "mut_425c376a", "type": "data_edit", "timestamp": "2025-09-26T11:41:07Z", "user": { "id": "user_123", "name": "John Doe", "external_id": "partner-uuid-123" // Optional, present for partner SSO users }, "details": { "mutation_type": "UPDATE", "target": "transaction", "location": {"page": 1, "row": 2}, "changes": { "debit": { "from": 880.55, // Had debit value "to": null // Debit removed }, "credit": { "from": null, // No credit originally "to": 880.55 // Credit added with same amount } } } } ``` ### Financial Statement Field Corrections Property mutations apply to document-level fields, including financial statement (CERFA) fields. Unlike transaction mutations which use `location`, property mutations specify the field directly via `property_name`. #### Example: CERFA Field Correction ### CERFA Field Update ``` { "id": "mut_f8a2b1c3", "type": "data_edit", "timestamp": "2025-09-26T14:30:00Z", "user": { "id": "user_123", "name": "John Doe", "external_id": "partner-uuid-123" }, "details": { "mutation_type": "UPDATE", "target": "property", "property_name": "028_net_n", "changes": { "from": 26024, "to": 26124 } } } ``` ### Complete Activity Log Example ### Sample Activity Logs Array ``` "activity_logs": [ { "id": "mut_a1b2c3", "type": "data_edit", "timestamp": "2025-06-02T14:20:00Z", "user": { "id": "user_123", "name": "John Doe", "external_id": "partner-uuid-123" }, "details": { "mutation_type": "UPDATE", "target": "transaction", "location": {"page": 1, "row": 5}, "changes": { "description": { "from": "AMZN MARKETPLACE", "to": "Amazon - Office Supplies" } } } }, { "id": "com_d4e5f6", "type": "comment", "timestamp": "2025-06-02T14:21:00Z", "user": { "id": "user_456", "name": "Jane Smith" }, "details": { "content": "Please verify the Amazon transactions" } }, { "id": "val_g7h8i9", "type": "validation", "timestamp": "2025-06-02T14:24:00Z", "user": { "id": "user_789", "name": "Manager Bob" }, "details": { "action": "VALIDATED", "comment": "All corrections verified and approved" } } ] ``` [Back to Overview](https://app.holofin.ai/docs/api/v1.1/) --- ### Address enrichment URL: https://app.holofin.ai/docs/api/v1.1/extraction/#address-enrichment (A section of the page it links to, included above) ### 2D-Doc seals URL: https://app.holofin.ai/docs/api/v1.1/extraction/#two-d-doc (A section of the page it links to, included above) ### Field enrichment URL: https://app.holofin.ai/docs/api/v1.1/enrichment/ Field enrichment ================ v1.1 Extraction tells you what a document says. Enrichment tells you whether the outside world agrees. Certain schema field types are resolved against a reference after extraction, and the answer is attached alongside the extracted value. The contract ------------ Three rules hold for every kind of enrichment, and they are the ones that matter when you write consuming code: 1. **The extracted string is never modified.** It stays exactly what is printed on the document. Enrichment is a sidecar next to it, never a correction of it. 2. **Every enriched field gets an entry**, keyed by the field's path, under `enrichments` on the document part. 3. **Every entry has one of three statuses.** Not two. "We could not check" and "we checked and found nothing" are different facts and must not collapse into one. | status | Meaning | How to treat it | | --- | --- | --- | | matched | The reference recognises this value, and the entry carries its structured record. | The only status on which the enriched keys are populated. | | not\_found | The reference was asked and gave a terminal negative. | This is the meaningful negative signal. Act on it. | | unchecked | No opinion: an outage, a timeout, or the lookup switched off. A `reason` says which. | Treat as a field with no entry at all. **Never** as suspicion. | Reporting an outage as "this company does not exist" is the single most damaging way to consume this data. Gate on `status == "matched"` and branch on `not_found`; let `unchecked` fall through to whatever you do when there is no signal. The three kinds --------------- A field is enriched because of the type it was given in the schema builder. Each entry names its own `kind`, so you can tell them apart without knowing the schema. | Field type | kind | Resolved against | | --- | --- | --- | | Address (FR) | address\_fr | The French national address database (BAN). A network lookup. | | SIREN | siren | The French company registry. A network lookup. | | 2D-Doc | 2ddoc | Nothing external: the seal is decoded and its signature verified locally. | A document part whose schema declares none of these carries no `enrichments` key at all. The key is omitted, not empty. Addresses --------- A matched address entry carries `normalized`, `housenumber`, `street`, `postcode`, `city`, `citycode`, `ban_id`, `lat`, `lon`, `score` and `granularity`. Read granularity, not just status. On a `matched` entry, `granularity: "street"` means the street was confirmed but the house number was not. If you are screening for fabricated addresses, require `granularity: "housenumber"`. `ban_id` is the identifier to match on across documents. The address a cryptographically valid 2D-Doc seal carries is resolved the same way, and the verdict sits **under the seal field's own address group**, beside the sealed lines: `enrichments..` carries `status`, `normalized`, `ban_id`, `granularity` and the other address keys next to `complete`, `ligne4`, `code_postal`, `commune`. Those keys are Holofin's, not the issuer's, and are listed in the seal's `computed`. Invalid or unresolved seals get no lookup: an unverified payload must never gain a confident, BAN-attributed address. (Until August 2026 this was a separate top-level entry keyed `ddoc_`; see the [changelog](https://app.holofin.ai/docs/api/v1.1/changelog/).) Companies --------- A matched `siren` entry carries the registry record verbatim, under the registry's own French key names: `denomination`, `statut_consolide`, `entreprise_cessee`, `capital`, a `siege` object, `identite`, `representants`, and more. A printed SIRET is accepted: the leading nine digits are looked up, and the full fourteen are recorded as `siret_source`. Invoices and KBIS print SIRET far more often than a bare SIREN. The outcome also appears in the part's validation codes, which is often the easier thing to branch on: | Code | Meaning | | --- | --- | | SIREN\_RESOLVED | The registry recognises the number. | | SIREN\_NOT\_FOUND | It does not, or the printed value is not a usable SIREN or SIRET. An error. | | SIREN\_ENRICHMENT\_UNAVAILABLE | The lookup could not be performed. Also an error, but not a statement about the company. | Writing rules against enrichment -------------------------------- Enriched data hangs off the field it came from: `@enriched..`. Naming the field is what lets one document carry a billing address and a delivery address, each with its own verdict. ### Gate on status, always ``` IF @enriched.adresseFacturation.status == 'matched' THEN ASSERT @enriched.adresseFacturation.postcode == @codePostal ``` Without the `IF`, the rule fails every time the lookup was unavailable, turning an outage on our side into a rejected document on yours. **Only top-level fields are addressable from a rule.** Entries for array members are keyed with dots and brackets in the key itself, for example `parties[0].address`, and a rule cannot name them. They are still present in the API response, so consume them there. [### The sidecar in a real response Where `enrichments` sits in the extraction result, with a full example payload.](https://app.holofin.ai/docs/api/v1.1/extraction/#address-enrichment)[### Every addressable key The complete `@enriched.*` vocabulary a rule can name, per kind and per document type.](https://app.holofin.ai/docs/api/v1.1/hololang/fields/) --- ### Fraud Analysis URL: https://app.holofin.ai/docs/api/v1.1/fraud-analysis/ Fraud Analysis ============== v1.1 Fraud analysis looks for signs that a PDF was tampered with or forged. When a Fraud Analyzer node is included in your workflow, the workflow run response includes risk scoring, risk factors, and trust indicators. [Overview](#overview) --------------------- Fraud analysis examines each uploaded document. Its individual signals are grouped into **findings**: risk factors that raise suspicion and trust factors that support authenticity. Fraud analysis results appear in two places: * **Workflow run response**: the `fraud_analysis_result` field at the root level of the [GET workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run) response, alongside `extraction_result` * **Node webhook**: the `node-run.completed` event for `fraudAnalyzer` nodes includes a summary with `risk_level`, `risk_score`, and `signal_count` [Workflow Run Response](#workflow-run-response) ----------------------------------------------- When a workflow includes a Fraud Analyzer node, the [GET workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run) response includes a root-level `fraud_analysis_result` field: ### GET /api/v1.1/workflow-runs/{run\_id} ``` { "run_id": "wr_abc123xyz789", "status": "completed", "classification_result": { ... }, "extraction_result": { ... }, "fraud_analysis_result": { "status": "completed", "risk_level": "warning", "risk_score": 42.5, "risk_factors": [ { "id": "content_tampering", "label": "Document Content Modified", "severity": "high", "details": "The document shows signs of post-creation editing...", "sources": ["forensic"] }, { "id": "metadata_anomalies", "label": "Metadata Inconsistencies", "severity": "medium", "details": "The PDF metadata contains contradictory information...", "sources": ["forensic"] } ], "trust_factors": [ { "id": "legitimate_producer", "label": "Known Bank Software", "passed": true, "details": "The document was generated by software commonly used by banks.", "sources": ["forensic"] } ] } } ``` If no Fraud Analyzer node ran in the workflow, `fraud_analysis_result` is `null`. When your organization limits fraud analysis to native PDFs or to bank statements (set up by Holofin on request), a document outside those filters is not analysed or billed for fraud. Its `fraud_analysis_result` is `{"status": "skipped", "skip_reason": "not_native_pdf", "risk_level": null, "risk_score": null, "risk_factors": [], "trust_factors": []}`, with `skip_reason``"not_native_pdf"` (a scan, a photo or an image upload) or `"not_bank_statement"`. Treat it as "not analysed", not as a clean document. [`fraud_analysis_result` Object](#fraud-analysis-object) -------------------------------------------------------- | Field | Type | Description | | --- | --- | --- | | status | string | `"completed"`, or `"skipped"` when your organization's fraud filters left the document out. | | skip\_reason | string | Only when `status` is `"skipped"`: `"not_native_pdf"` or `"not_bank_statement"`. | | risk\_level | string | Overall risk assessment: `"trusted"`, `"normal"`, `"warning"`, or `"high_risk"`. `null` when skipped. | | risk\_score | number | Numeric risk score from 0 to 100. Higher means more suspicious. | | risk\_factors | array | Detected risk indicators. Each factor represents a forensic finding that increases risk. | | trust\_factors | array | Trust indicators. Each factor represents a positive signal about document authenticity. | | [].sources | array of strings | On every risk and trust factor: where its evidence comes from. `"forensic"`: a test on the file itself. `"template"`: a comparison with documents the issuer is known to produce. `"content"`: the document's own figures disagree. `"seal"`: the signed 2D-Doc seal. A factor can have several. | ### [Risk Levels](#risk-levels) | Level | Score Range | Meaning | | --- | --- | --- | | `trusted` | 0 – 9 | No suspicious indicators detected. Document appears authentic. | | `normal` | 10 – 29 | Minor anomalies found, likely benign. | | `warning` | 30 – 59 | Significant anomalies that warrant manual review. | | `high_risk` | 60 – 100 | Strong evidence of tampering or forgery. Document should be verified through alternative channels. | ### [Risk Factor Object](#risk-factor-object) | Field | Type | Description | | --- | --- | --- | | id | string | Unique finding identifier (e.g., `"content_tampering"`, `"metadata_anomalies"`) | | label | string | Human-readable title for the finding | | severity | string | Severity level: `"low"`, `"medium"`, `"high"`, or `"critical"` | | details | string | null | AI-generated explanation of what was detected and why it matters | ### [Pages redrawn by AI](#ai-redrawn-pages) The risk factor `ai_page_redrawn` (label « Page redrawn by AI », severity `high`) means a page looks redrawn by an AI image generator to change a figure, a name or a date. What to do: do not rely on the figures of the flagged page, and ask for the document from its issuer. ### [Trust Factor Object](#trust-factor-object) | Field | Type | Description | | --- | --- | --- | | id | string | Unique finding identifier (e.g., `"legitimate_producer"`, `"consistent_metadata"`) | | label | string | Human-readable title for the trust indicator | | passed | boolean | `true` if the trust check passed, `false` if the check could not confirm trust | | details | string | null | AI-generated explanation of the trust indicator | [Node Webhook Payload](#node-webhook) ------------------------------------- When a `fraudAnalyzer` node completes, a `node-run.completed` webhook is sent with a summary in the `result` field: ### Fraud Analyzer Node Completed ``` { "id": "whev_abc123def456", "event": "node-run.completed", "created_at": "2025-06-02T14:30:00Z", "data": { "object": { "run_id": "wr_xyz789ghi012", "workflow_id": "wf_abc123def456", "document_id": "doc_abc123def456", "node_id": "fraud_analyzer_1", "node_type": "fraudAnalyzer", "status": "COMPLETED", "timestamp": "2025-06-02T14:30:00Z", "result": { "analysis_id": "pda_abc123def456", "risk_level": "warning", "risk_score": 42.5, "signal_count": 5, "categories_triggered": ["tampering", "metadata_anomaly"], "signal_summary": { "tampering": 2, "metadata_anomaly": 3 } } } } } ``` The node webhook provides a quick summary. For the full risk and trust factors, use the `fraud_analysis_result` field in the [workflow run response](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run). | Field | Type | Description | | --- | --- | --- | | analysis\_id | string | Unique analysis identifier (prefix: `pda_`) | | risk\_level | string | Overall risk level: `"trusted"`, `"normal"`, `"warning"`, or `"high_risk"` | | risk\_score | number | Numeric risk score (0–100) | | signal\_count | integer | Total number of forensic signals triggered | | categories\_triggered | array | Signal categories that had triggers (e.g., `"tampering"`, `"metadata_anomaly"`) | | signal\_summary | object | Count of triggered signals per category | [Workflow Runs Docs](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run)[Webhooks Docs](https://app.holofin.ai/docs/api/v1.1/webhooks/) --- ## Integrations ### Webhooks URL: https://app.holofin.ai/docs/api/v1.1/webhooks/ Webhooks ======== Holofin can POST to your endpoint when a workflow run completes, fails or pauses for review, when a reviewer confirms its results, and when a dossier changes state. This page covers registering endpoints, the payloads, signatures and retries. [Overview](#overview) --------------------- A webhook is an HTTP POST that Holofin sends to an endpoint you register whenever an event it subscribes to occurs, so you do not have to poll the API. **Note:** Webhook endpoints can be configured through your [organization settings](https://app.holofin.ai/settings/webhooks/), or registered programmatically with the [Management API](#management-api) below. [How Webhooks Work](#how-it-works) ---------------------------------- 1. **Configure endpoint:** Register your webhook URL in organization settings 2. **Select events:** Choose which events you want to receive 3. **Receive notifications:** When events occur, we send POST requests to your endpoint 4. **Process events:** Your application handles the webhook payload 5. **Acknowledge receipt:** Return a 2xx status code to confirm delivery within 30 seconds [Per-Run Override URL](#per-run-override) ----------------------------------------- Registered endpoints receive events for **every** run in your organization. When you want the events for a **single run** delivered somewhere else, pass a `webhook_url` when you create the run with [POST /workflow-runs/](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run): ### Create a run with a per-run webhook URL ``` curl -X POST https://app.holofin.ai/api/v1.1/workflow-runs/ \ -H "X-API-Key: $HOLOFIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_abc123xyz", "document_b64": "JVBERi0xLjQKJeLjz9MKNCAwIG9i...", "webhook_url": "https://your-app.example.com/hooks/run-42" }' ``` Behavior: * **Replaces registered endpoints.** For that run, events go **only** to the override URL; your registered endpoints receive nothing for it. All other runs are unaffected. * **All events for the run.** The override receives the full set: `workflow-run.*`, `node-run.*`, `extraction.*`, and the later `workflow-run.confirmed` / `workflow-run.republished` events. Event-subscription filters do not apply. * **Same envelope, security, and retries.** Payloads use the same [envelope](#webhook-payload) and flow through the same [retry schedule](#webhook-retries). * **Signing reuses your endpoint secret.** Deliveries are signed with the `X-Holofin-Signature` header using the secret of your existing webhook endpoint, so the [same verification code](#webhook-security) works. If your organization has no endpoint configured, there is no secret to reuse: deliveries are sent **unsigned** and the create response includes a `warnings` entry. * **Validated up front.** The URL must be an `http(s)` URL reachable from the public internet. Malformed URLs and private/reserved addresses are rejected immediately with `400 INVALID_WEBHOOK_URL` (SSRF protection), and the run is not created. **Tip:** the override URL is scoped to the run's entire lifetime, including human-review events that can fire hours later. Keep the endpoint reachable for that whole period; a one-shot listener will miss them. [Managing Endpoints (API)](#management-api) ------------------------------------------- You can register and manage webhook endpoints programmatically instead of using the dashboard, for example to provision one webhook per project. All routes are authenticated with your [API key](https://app.holofin.ai/docs/api/v1.1/authentication/) via the `X-API-Key` header and are scoped to the organization that owns the key. | Method & path | Description | | --- | --- | | POST /api/v1.1/webhooks | Register a new webhook endpoint | | GET /api/v1.1/webhooks | List this organization's endpoints | | GET /api/v1.1/webhooks/{id} | Fetch a single endpoint | | PATCH /api/v1.1/webhooks/{id} | Update url, name, events or status | | DELETE /api/v1.1/webhooks/{id} | Remove an endpoint | **Good to know:** a maximum of **5** endpoints per organization is enforced. The URL must be publicly reachable over `http`/`https` (private and reserved addresses are rejected to prevent SSRF). The signing `secret` is returned **once**, in the create response. Store it; it is not shown again. ### Register an endpoint Send a JSON body with the target `url`. `name` (defaults to `"API webhook"`), `description` and `events` are optional. An empty or omitted `events` list subscribes the endpoint to **all** event types; otherwise pass a subset of the [subscribable events](#event-types). ### Example Request ``` curl -X POST "https://app.holofin.ai/api/v1.1/webhooks" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.example.com/webhooks/holofin", "name": "Project Falcon", "events": ["workflow-run.completed", "workflow-run.failed"] }' ``` ### Response (201 Created) ``` { "id": "whep_a1b2c3d4e5f6g7h8", "name": "Project Falcon", "url": "https://your-app.example.com/webhooks/holofin", "events": ["workflow-run.completed", "workflow-run.failed"], "status": "active", "description": "", "created_at": "2026-07-22T14:30:00Z", "secret": "b7f3...store-this-it-is-only-returned-once" } ``` Use the returned `secret` to verify the `X-Holofin-Signature` header on incoming deliveries (see [Webhook Security](#webhook-security)). On subsequent `GET` responses the `secret` field is omitted. ### Update, disable or delete `PATCH` accepts any subset of `url`, `name`, `description`, `events` and `status`. Set `"status": "disabled"` to pause deliveries without deleting the endpoint, and `"status": "active"` to resume (which also clears the failure streak). `DELETE` returns `204 No Content`. ### Disable an endpoint ``` curl -X PATCH "https://app.holofin.ai/api/v1.1/webhooks/whep_a1b2c3d4e5f6g7h8" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"status": "disabled"}' ``` Errors are returned as `{"error": {"code": "...", "message": "..."}}` with codes such as `MISSING_URL`, `INVALID_URL`, `INVALID_EVENTS`, `INVALID_STATUS`, `WEBHOOK_LIMIT_REACHED` (HTTP 409) and `NOT_FOUND` (HTTP 404). [Event Types](#event-types) --------------------------- Workflow events notify you about the status of document processing workflows. | Event | Description | | --- | --- | | workflow-run.completed | Workflow has completed successfully. All nodes have finished processing. | | workflow-run.failed | Workflow has failed due to an error. The payload includes the error message. | | workflow-run.paused | Workflow is paused and waiting for human approval (e.g., at a Human Review node). The payload includes the pause reason and which node triggered the pause. | | workflow-run.confirmed | Extraction has been confirmed (validated) by a user. Fired once, when the user first validates. The payload includes the full extraction data, the user who confirmed, and an optional comment. | | workflow-run.republished | An already-confirmed extraction has been re-pushed upstream by a user (e.g. after edits, or to recover from a downstream consumer that missed the original `workflow-run.confirmed`). The payload mirrors `workflow-run.confirmed` and additionally includes the original confirmation metadata. | | node-run.completed | Individual agent node has completed. The payload includes the node result data specific to the node type (classifier, segmenter, extractor, humanReview, validator). | | node-run.failed | Individual agent node has failed. The payload includes the error message. | Dossier events notify you when a [dossier](https://app.holofin.ai/docs/api/v1.1/cases/) changes state as documents and values arrive. Like every other event, they are delivered only to the endpoints subscribed to them. | Event | Description | | --- | --- | | case.updated | The dossier's status or coherence changed: a document was processed, a value was pushed, a rule verdict moved. The payload carries the full dossier projection (see [Dossier Event Payload](#dossier-events)). | | case.completed | The dossier transitioned to `complete`. Fired in addition to the `case.updated` for that same change, so subscribe to it alone when you only care about the finish line. | [Webhook Payload](#webhook-payload) ----------------------------------- All webhook payloads follow a consistent structure: ### Webhook Payload Structure ``` { "id": "whev_abc123def456", "event": "workflow-run.completed", "created_at": "2025-06-02T14:30:00Z", "data": { "object": { "run_id": "wr_xyz789ghi012", "workflow_id": "wf_abc123def456", "workflow_name": "Bank Statement Processing", "document_id": "doc_abc123def456", "status": "completed", "validation_status": "OK", "no_route_matched": false, "unmatched_classifications": null, "timestamp": "2025-06-02T14:30:00Z" } } } ``` For `workflow-run.completed` events, the payload includes: * `validation_status` - Composite status reflecting both consistency-check outcome and manual review state, in priority order: + `"VALIDATED"`: a reviewer has confirmed the extraction + `"INVALIDATED"`: a reviewer has reset / rejected a previous confirmation + `"PASSED"`: consistency checks passed, no manual review yet + `"WARNING"`: consistency checks raised warnings, no manual review yet + `"ERROR"`: consistency checks raised errors, no manual review yet + `"NO_VALIDATION"`: no validators were configured + `null`: no extraction aggregator (e.g., classification-only workflow)The consistency portion is the worse of the extraction and business outcomes, so this field keeps its existing meaning. * `no_route_matched` - `true` when the run completed without any extraction because the detected document type had no matching route in the workflow (a branching step fell through to its "all others" path and no extractor ran). The workflow itself ran without error; its routing does not handle the document's type. * `unmatched_classifications` - When `no_route_matched` is `true`, the detected classification(s) or segment type(s) that had no route (e.g. `["carte-nationale-didentit"]`). `null` otherwise. For `workflow-run.paused` events, the payload includes additional context about why the workflow paused: * `pause_reason` - Why the workflow paused (e.g., "approval" for human review) * `paused_at_node` - The node ID where the workflow is waiting For `workflow-run.failed` events, the payload includes: * `error` - Error message describing what went wrong For `workflow-run.confirmed` events, the payload includes: * `confirmed_by` - Email of the user who confirmed the extraction * `confirmed_at` - ISO 8601 UTC timestamp when the extraction was confirmed * `comment` - Optional comment provided during confirmation * `extraction_data` - Full extraction data (same format as `extraction.processed` payload). Includes the per-part `checks` rule inventory, so payload size grows with the number of checks a document type runs (a fully validated financial statement carries ~40 entries per part). Also carries a run-level `document_pdf_url` field (the [original document PDF](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run-pdf)) and a `pdf_url` field on every `document_parts` entry (that [part's pages as one PDF](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-part-pdf)); fetching either requires the `X-API-Key` header, and `?format=json` on them yields a time-limited direct link. For `workflow-run.republished` events, the payload includes: * `status` - Always `"republished"` * `republished_by` - Email of the user who triggered the republish * `republished_at` - ISO 8601 UTC timestamp of the republish action * `originally_confirmed_by` - Email of the user who first confirmed the extraction (may be `null` if the original reviewer's account no longer exists) * `originally_confirmed_at` - ISO 8601 UTC timestamp of the original confirmation (may be `null`) * `comment` - Optional comment provided when republishing * `extraction_data` - Full extraction data (same format as `workflow-run.confirmed`, including `document_pdf_url` and per-part `pdf_url`, both requiring the `X-API-Key` header to fetch); typically reflects edits applied since the original confirmation Use `republished_at` together with `originally_confirmed_at` to detect re-publishes and decide whether to upsert or version downstream records. Republishes can fire multiple times for the same run. ### [Node Event Payload](#node-events) Node events (`node-run.completed`, `node-run.failed`) provide granular updates as each workflow node executes. For `node-run.completed` events, the payload includes a `result` field with node-specific output data: ### Node Completed Payload (Classifier Example) ``` { "id": "whev_abc123def456", "event": "node-run.completed", "created_at": "2025-06-02T14:30:00Z", "data": { "object": { "run_id": "wr_xyz789ghi012", "workflow_id": "wf_abc123def456", "document_id": "doc_abc123def456", "node_id": "classifier_1", "node_type": "classifier", "status": "COMPLETED", "timestamp": "2025-06-02T14:30:00Z", "result": { "classification": "bank_statement", "confidence": 0.95, "reasoning": "Document contains bank account headers..." } } } } ``` The `result` field structure varies by node type: | Node Type | Result Fields | | --- | --- | | classifier | `classification`, `confidence`, `all_classes`, `reasoning` | | segmenter | `segments[]`, `segment_count` | | extractor | `aggregator_id`, extraction status (full data via extraction aggregator API) | | fraudAnalyzer | `analysis_id`, `risk_level`, `risk_score`, `signal_count`, `categories_triggered`, `signal_summary` ([details](https://app.holofin.ai/docs/api/v1.1/fraud-analysis/)) | | humanReview | `approved`, approval data | | validator | Validation results | For `node-run.failed` events, the payload includes: * `error` - Error message describing what went wrong in the node ### [Dossier Event Payload](#dossier-events) Dossier events (`case.updated`, `case.completed`) carry the dossier itself: `data.object` is the same projection that `GET /api/v1.1/cases/{case_id}` answers with. The field-by-field reference lives on the [Dossiers page](https://app.holofin.ai/docs/api/v1.1/cases/#read) and is not duplicated here. Three envelope keys ride along inside the object: * `event` - The event type, repeated inside the object * `previous_status` - The `case_status` before this change, lowered like `case_status` itself, or `null` when there was none * `timestamp` - ISO 8601 UTC timestamp of the evaluation that fired the event ### Dossier Completed Payload ``` { "id": "whev_abc123def456", "event": "case.completed", "created_at": "2026-08-10T08:04:08Z", "data": { "object": { "case_id": "case_9f3k2m81xq7c", "config_id": "6dc962d3-14df-4d41-b858-75f2b24b648f", "config_name": "Crédit professionnel", "case_status": "complete", "decision": "none", "coherence": "ok", "created_at": "2026-08-10T07:59:02Z", "evaluated_at": "2026-08-10T08:04:07.804604Z", "fields": { "montant": {"label": "Montant du prêt", "value": 250000, "state": "provided", "required": true} }, "requirements": [ {"key": "releve_bancaire", "label": "Bank statement", "state": "satisfied", "min": 2, "max": null, "provided": 2, "accepted_categories": ["releve_bancaire"], "conditional": false, "activation": null, "documents": [ {"document_id": "doc_k2VgTmN4pQ7cWxYz", "part_key": "", "state": "processed", "name": "releve_juin.pdf", "run_id": "wr_3fW7kZq9YtM5cHx2", "category": "releve_bancaire", "pages": [1], "owners": []}, {"document_id": "doc_8fRsLdH6wB3nJvQa", "part_key": "", "state": "processed", "name": "releve_juillet.pdf", "run_id": "wr_6bN4pDs8RvK1mTy3", "category": "releve_bancaire", "pages": [1], "owners": []} ]} ], "participants": {}, "rules": [ {"code": "case_rule_a3f19c04d2", "label": "Capital social suffisant", "kind": "consistency", "severity": "blocking", "status": "ok", "reason": null, "depends_on": [ {"type": "requirement", "key": "kbis", "party_ref": null, "state": "ready"}, {"type": "dossier", "key": "dossier", "party_ref": null, "state": "ready"}], "documents": [], "values": [{"field": "kbis.capital", "value": 50000, "document_id": "doc_k2VgTmN4pQ7cWxYz"}, {"field": "dossier.montant", "value": 25000}], "provisional": false, "rule": "ASSERT @case.documents.kbis.capital >= @case.montant", "description": null} ], "unassigned_documents": [], "event": "case.completed", "previous_status": "pending_review", "timestamp": "2026-08-10T08:04:08Z" } } } ``` Dossier events are organization-level, not run-level: a [per-run override URL](#per-run-override) never receives them, and they always go to your registered endpoints, honoring each endpoint's event subscription. When a dossier transitions to `complete`, both events fire for that same change with distinct event `id`s. ### Envelope Fields | Field | Type | Description | | --- | --- | --- | | id | string | Unique identifier for this webhook event (prefix: `whev_`) | | event | string | Type of event that occurred | | created\_at | string | ISO 8601 UTC timestamp when the event was created | | data.object | object | Event-specific data payload (see below) | ### Data Object Fields | Field | Type | Description | | --- | --- | --- | | run\_id | string | Workflow run identifier (prefix: `wr_`) | | workflow\_id | string | Workflow definition identifier (prefix: `wf_`) | | workflow\_name | string | Human-readable workflow name | | document\_id | string | Document identifier (prefix: `doc_`) | | status | string | Workflow status: `completed`, `failed`, or `paused` | | validation\_status | string | null | Composite validation result (completed only): `VALIDATED`, `INVALIDATED`, `PASSED`, `WARNING`, `ERROR`, `NO_VALIDATION`, or `null`. See the list above for the full priority order. | | timestamp | string | ISO 8601 UTC timestamp when the status changed | | error | string | Error message (failed events only) | | pause\_reason | string | Why the workflow paused (paused events only) | | paused\_at\_node | string | Node where workflow is waiting (paused events only) | | confirmed\_by | string | Email of the user who confirmed (confirmed events only) | | confirmed\_at | string | ISO 8601 UTC timestamp of confirmation (confirmed events only) | | comment | string | null | Optional comment provided during confirmation (confirmed events only) | | extraction\_data | object | null | Full extraction data including all segments (confirmed events only). Carries the per-part `checks` rule inventory; payload size grows with the number of checks the document type runs. | [Webhook Security](#webhook-security) ------------------------------------- Holofin signs every webhook request with HMAC so you can check that it came from Holofin and was not modified. ### Signature Verification Each webhook request includes a signature in the `X-Holofin-Signature` header. This signature is generated using HMAC-SHA256 with your webhook secret. ### Python Example ``` import hmac import hashlib def verify_webhook_signature(payload, signature, secret): """ Verify the webhook signature to ensure authenticity. Args: payload: The raw request body (bytes) signature: The signature from X-Holofin-Signature header secret: Your webhook secret Returns: bool: True if signature is valid """ expected_signature = hmac.new( secret.encode('utf-8'), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected_signature, signature) # In your webhook handler: @app.route('/webhook', methods=['POST']) def handle_webhook(): payload = request.get_data() signature = request.headers.get('X-Holofin-Signature') if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET): return 'Invalid signature', 401 # Process the webhook... return 'OK', 200 ``` [Retries & Reliability](#webhook-retries) ----------------------------------------- Failed deliveries are retried automatically: * **Success criteria:** Any 2xx HTTP status code is considered successful * **Retry schedule:** Failed webhooks are retried with exponential backoff plus jitter to prevent thundering herd * **Max attempts:** Up to 7 retry attempts over 48 hours * **Status change:** Endpoints marked as "FAILING" after 5 consecutive failures (but continue receiving events) ### Retry Schedule | Attempt | Base Delay | Jitter Range | Cumulative Time | | --- | --- | --- | --- | | 1 | Immediate | - | 0 seconds | | 2 | 1 minute | + 0-30 seconds | ~1 minute | | 3 | 5 minutes | + 0-60 seconds | ~6 minutes | | 4 | 30 minutes | + 0-2 minutes | ~36 minutes | | 5 | 2 hours | + 0-5 minutes | ~2.5 hours | | 6 | 6 hours | + 0-10 minutes | ~8.5 hours | | 7 | 12 hours | + 0-15 minutes | ~20.5 hours | | 8 | 24 hours | + 0-30 minutes | ~45 hours | **Note:** Jitter is added to each retry to prevent multiple webhooks from retrying at the exact same time, reducing server load. [Best Practices](#best-practices) --------------------------------- * Always verify signatures Validate the HMAC signature on every webhook to ensure requests are from Holofin. * Handle events idempotently Use the event ID to ensure you process each event only once, even if it's delivered multiple times. * Respond quickly Return a 2xx status code within 30 seconds. Process the event asynchronously if your handler needs more time. * Monitor failures Check your webhook logs regularly to identify and fix any delivery issues. [Testing Webhooks](#testing) ---------------------------- You can test your webhook integration using the test button in the webhook settings. [Back to Overview](https://app.holofin.ai/docs/api/v1.1/) --- ### MCP Server URL: https://app.holofin.ai/docs/api/v1.1/ai-assistants/ MCP Server ========== v1.1 Connect Claude, or any MCP-capable assistant, to Holofin and ask about a case in natural language: its extractions, rule verdicts, participants, documents and history, as the pipeline recorded them. [Overview](#overview) --------------------- Holofin runs a hosted **Model Context Protocol (MCP)** server at `https://mcp.holofin.ai/mcp`. There is nothing to install: your assistant connects to it directly over HTTPS, authenticating with OAuth 2.1 or an MCP key. Every result is a **projection of what the pipeline decided**. The tools never recompute a verdict: a rule that failed is reported as the engine decided it, with the values it compared and the document each value came from. Answers built from these tools can therefore always cite a document and a page. ### Architecture ``` Your assistant (Claude, Claude Code, Cursor, …) ↕ MCP protocol (streamable HTTP) mcp.holofin.ai/mcp ↕ your organization's cases, documents and verdicts ``` [Connecting](#installation) --------------------------- Two ways to authenticate. **OAuth 2.1** is the one to prefer: you sign in with your Holofin account, no secret is copied into a config file, and access can be revoked from your side at any time. **MCP keys** (prefix `hf_mcp_`) suit server-to-server integrations. These are *not* the same as the REST API keys used elsewhere in this documentation. Create an MCP key from your organization settings. ### Claude, and other OAuth clients Add a custom connector pointing at the server URL and complete the sign-in prompt: ### Server URL ``` https://mcp.holofin.ai/mcp ``` ### Claude Code ### Terminal ``` claude mcp add --transport http holofin https://mcp.holofin.ai/mcp ``` ### Cursor / Windsurf Add to your MCP configuration file (`.cursor/mcp.json` or equivalent): ### mcp.json ``` { "mcpServers": { "holofin": { "url": "https://mcp.holofin.ai/mcp", "headers": { "Authorization": "Bearer hf_mcp_YOUR_KEY" } } } } ``` [Available Tools](#tools) ------------------------- Your assistant discovers these automatically. Start with `list_cases` to find a case, then `get_case` for the whole picture, then the `explain_…` tools to drill into anything that is not satisfied. Tools accept either the case reference you supplied or the internal dossier id, so an id read from one result can be passed straight to the next call. | Tool | Description | | --- | --- | | list\_cases | The organization’s cases, filtered by status, decision, coherence or date, and paged | | get\_case | One case: its status, every expected document and what is missing, the rule verdicts worst-first, participants and decision | | explain\_rule | Why one check reached its verdict: the values it compared and the document each came from | | explain\_requirement | Why one expected document is still missing, what satisfies it, and the checks that speak for it | | get\_participants | The parties on the case: declared fields, registry (SIREN) and address lookups, documents owned. Contains personal data | | get\_document\_checks | Every check that ran on a document: extraction quality, business rules, identity verification, 2D-Doc seals, fraud indicators | | get\_case\_history | What changed and who did it: decisions, comments, evaluation transitions | | list\_documents | A case’s documents with categories, owning party and processing state | | get\_document\_page | One page of a document as markdown, with citation provenance | | get\_document\_overview | A whole document as markdown | | search\_dossier | Lexical search across the case’s document text, returning page-level hits | | get\_extraction | The full structured extraction for a document | | get\_page\_image | A signed image URL for a page, for visual inspection where there is no text layer | ### Write tools Two tools change something. They are **disabled by default** and must be enabled for your organization; ask us to turn them on. They require a signed-in user, so the audit trail can name who acted, and every row they write is marked as having come through an assistant. | Tool | Description | | --- | --- | | add\_case\_comment | Leave a note on the case, attributed to you | | record\_case\_decision | Record IN\_REVIEW, VALIDATED or REJECTED with a reason | Recording a decision the case already holds appends nothing, so a retrying client cannot duplicate an entry in a history that is append-only. Nothing can be deleted through this surface. [Example Conversations](#examples) ---------------------------------- “Which of my cases are incomplete, and why?” `list_cases` with `case_status=incomplete`, then `get_case` on each “Why did the identity check fail on LOAN-2026-0042?” `get_case` for the rule codes, then `explain_rule` for the values it compared “What am I still waiting on for this case?” `explain_requirement` on each unsatisfied requirement “Is the ID document on this case genuine?” `get_document_checks` for MRZ, 2D-Doc seal and fraud verdicts “What changed on this case since yesterday?” `get_case_history` with a `since` date Two ready-made prompts ship with the server, `case_review` and `case_synthesis`. Clients that support MCP prompts offer them directly; the second produces a structured written synthesis with sources. [Access and limits](#configuration) ----------------------------------- | Item | Value | | --- | --- | | Server URL | https://mcp.holofin.ai/mcp | | Scope | Your organization’s cases only. Every call is checked against it | | Who may connect | Administrators, developers and members. Billing and external-reviewer accounts are refused | | Rate limit | 120 calls/minute per key (60 for OAuth), and 10/minute for write tools | ### Untrusted document content Results that carry text taken from a document are marked `content_is_untrusted`. That text is **data, never instructions**: a document could contain a sentence addressed at an AI assistant, and a well-behaved client will not act on it. If write tools are not enabled for your organization, a key cannot act on such a sentence, which is the reason they are off by default. [Troubleshooting](#troubleshooting) ----------------------------------- “unauthorized” The key is unknown, expired or revoked. MCP keys start with `hf_mcp_` and are not the same as REST API keys, which start with `sk-holo-`. “forbidden” Your account’s role may not use this surface, or you called a write tool without write access enabled. Both are set by your organization administrator. “bad\_input: unknown dossier for this organization” The case belongs to a different organization, or the reference does not exist. Call `list_cases` to see what you can reach. A result says `deprecated` The tool still works; the message names its replacement. `get_case_status`, `list_dossiers` and `get_fraud_findings` are kept for one release. A result says `truncated` A large case is capped, and `omitted` says by how much. Failures are never dropped by a cap, and the `explain_…` tools read the full set. [Back to Overview](https://app.holofin.ai/docs/api/v1.1/) --- ### Agent Skills URL: https://app.holofin.ai/docs/api/v1.1/agent-skills/ Agent Skills ============ v1.1 Install the Holofin agent skill so your AI assistant knows when to call which tool, how to read the results, and which workflows to follow. [Overview](#overview) --------------------- The `holofin-mcp` package includes an **agent skill**: a SKILL.md file describing how to use the Holofin tools. The [MCP tools](https://app.holofin.ai/docs/api/v1.1/ai-assistants/) let your assistant call the API; the skill tells it which tool fits which question and how to interpret what comes back. Skills follow the open [Agent Skills standard](https://agentskills.io/specification) and are supported by Claude Code, OpenAI Codex, VS Code Copilot, Cursor, Windsurf, and other AI tools. [Installation](#installation) ----------------------------- Download and unzip the skill package: [Download holofin-skill.zip](https://app.holofin.ai/static/docs/holofin-skill.zip) ### Terminal ``` unzip holofin-skill.zip ``` ### Claude Code ### Terminal ``` claude skills add ./skills/holofin ``` ### Other tools Copy the `skills/holofin/` directory into your tool's skills directory (e.g., `.claude/skills/`, `.cursor/skills/`). [What the Skill Covers](#coverage) ---------------------------------- Reviewing extracted data Navigating document parts, checking validation status, presenting bank statement and tax form results Investigating fraud analysis Interpreting risk levels, understanding risk/trust factors, summarizing forensic findings Monitoring workflow runs Tracking run status, debugging failures, checking credit usage [Back to Overview](https://app.holofin.ai/docs/api/v1.1/) --- ### HoloLang overview URL: https://app.holofin.ai/docs/api/v1.1/hololang/ HoloLang ======== v1.1 HoloLang is a formula language, close to Excel, for checking extracted data against your business rules. This page covers why it exists, five starter rules, and the reference chapters. [Why HoloLang?](#why-hololang) ------------------------------ Validating extracted documents requires complex business rules that change frequently. HoloLang separates these rules from application code, allowing: * Domain experts to write and maintain validation rules directly * Readable syntax that mirrors accounting terminology * Safe execution with no side effects or security risks * Tolerance-based comparisons for floating-point accounting values [Quick Start](#quick-start) --------------------------- Five rules covering the most common validation patterns: ### Five Essential HoloLang Rules ``` # 1. Required field - ensure a field is present REQUIRED @company_name # 2. Format validation - validate SIRET number format VALIDATE @company_siret FORMAT SIRET # 3. Balance check - assets must equal liabilities (within tolerance) ASSERT @total_assets == @total_liabilities WITHIN 10 # 4. Conditional rule - require capital for SARL companies IF @company_type == 'SARL' THEN REQUIRED @capital_social # 5. Warning - alert on negative equity (non-blocking) WARN IF @equity < 0 ``` [Reference Chapters](#chapters) ------------------------------- The full reference is organized in four chapters: * [Syntax](https://app.holofin.ai/docs/api/v1.1/hololang/syntax/): field references, literals, operators, functions and predicates, date operations and aggregate functions * [Rule Types](https://app.holofin.ai/docs/api/v1.1/hololang/rules/): REQUIRED, VALIDATE FORMAT, ASSERT, IF/THEN and WARN, plus length validation and formulas * [Sealed & Derived Fields](https://app.holofin.ai/docs/api/v1.1/hololang/fields/): verdict fields published by FORMAT validators, and the 2D-Doc sealed-field catalogue per document type * [Examples & Behavior](https://app.holofin.ai/docs/api/v1.1/hololang/examples/): a complete CERFA example, evaluation behaviors and error messages --- ### HoloLang syntax URL: https://app.holofin.ai/docs/api/v1.1/hololang/syntax/ HoloLang / Syntax ================= v1.1 Field references, literals, operators, date operations and aggregate functions: the parts every rule is built from. [Syntax Reference](#syntax-reference) ------------------------------------- ### Field References Fields from the data model are referenced with the `@` prefix. Field names must match the data structure exactly: ``` # Simple field references @company_siret @f110_net_n @exercise_start_date # Nested field access (dot notation for objects with subproperties) @salary.amount @employee.address.city @grossremuneration.iso4217currencycode ``` For nested objects, use dot notation to access subproperties. Each part of the path must exist in the data. The whole payload.`@document` is a reserved root naming every extracted field as *one* value. It is legal wherever a container is (`STRINGIFY(@document)` hands the whole document to an `LLM_JUDGE` prompt) and is a type error in a comparison. `@document.total` is `@total` spelled from the root, indexes included (`@document.reps[0].nom`). Like `enriched`, it wins over a schema field of the same name. In a dossier's validity rules `@document` is the document being judged; in a dossier's consistency rules the bare requirement inside `STRINGIFY` (`STRINGIFY(@case.documents.releve)`) is the bound document's whole payload. ``` # Every field of the document, as one JSON value ASSERT LLM_JUDGE(CONCAT('Is this a genuine payslip? ', STRINGIFY(@document))) ASSERT STRINGIFY(@document) CONTAINS 'SARL' # A field spelled from the root - the same thing as @total ASSERT @document.total == @document.subtotal + @document.tax WITHIN 0.01 ``` ### Literals Numbers (integers and decimals, can be negative) and strings (single or double quoted): ``` # Numbers 100 -50.5 0 # Strings 'SARL' "active" ``` ### Operators | Operator | Type | Description | | --- | --- | --- | | == | Comparison | Equal | | != | Comparison | Not equal | | >< >= <= | Comparison | Greater/less than (or equal) | | IN  NOT IN | Membership | Is the left value one of the values on the right? The right side takes three forms, and the *spelling* decides which: a parenthesised list of literals (`@doc_type IN ('18', '24')`); an **array expression** (`ARRAY_AGG(@reps[].nom)`, `ARRAY(@a, @b)`, `ARRAY_CONCAT`, `ARRAY_SLICE`, `ARRAY_REVERSE`), tested element by element as a set of whole values, as the list is; or a single *field* carrying several values in one string, such as a sealed list of forenames, which splits on `/` and `,` only, so `MARIE CLAIRE` and `Jean-Pierre` stay whole. Case and accents fold as with `==` in every form. The left side is never split, and an empty right side fails rather than passing | | ===   !== | Comparison | Strictly equal / not equal. No case or accent folding, no numeric or date conversion. Use when the exact characters matter | | CONTAINS  NOT CONTAINS | Substring | Does the right value appear *inside* the left one? The mirror of `IN`, which looks for a whole value among several. Case and accents fold as with `==`. When the left side is an array, it matches if *any* element contains the text. An empty right side always fails, and a missing left side is false, so `NOT CONTAINS` passes on a missing field, as `NOT IN` does | | AND OR NOT | Logical | Combine and negate conditions (case-insensitive). `NOT` binds tighter than `AND`/`OR`, which have **equal** precedence and group left to right. Parenthesise whenever both appear | | + - \* / | Arithmetic | Arithmetic in formulas and on the right side of a comparison, **numeric only**. `*` and `/` bind tighter than `+` and `-` | | ^ | Arithmetic | Power. Binds tightest and groups right to left, so `2 ^ 3 ^ 2` is 512. `0 ^ -1`, a negative base with a fractional exponent, and overflow all fail the rule rather than returning a non-number | | & | Text | Builds text. `+` never concatenates, so a rule means one thing whatever the extractor wrote. `&` binds *loosest*, so `'total: ' & @ht + @tva` joins the sum. An absent field renders as an empty string, a whole number loses its decimal point, a date renders ISO; a list or object operand fails the rule | Precedence, tightest first: `( )` → functions → `^` → `*``/` → `+``-` → `&` → comparisons → `NOT` → `AND`/`OR`. The *left* side of a comparison takes a field or a function call, never bare arithmetic: `ASSERT @a - @b > 0` does not parse. Write `ASSERT ABS(@a - @b) > 0`, or move the arithmetic to the right side. [Functions and Predicates](#functions) -------------------------------------- Every function below returns a value and may therefore lead a comparison, which plain arithmetic cannot. The *predicates* are the exception: they answer a yes/no question and belong where a condition goes. | Signature | Description | | --- | --- | | @field IN ('a', 'b', 'c') @field NOT IN ('a', 'b') ISPRESENT(@field) ISBLANK(@field) DDOC\_BENEF\_NAME\_MATCH(@enriched..beneficiaire, @firstname, @lastname) | **Predicates.** Is the field filled in? Legal inside `IF`, `ASSERT` and `WARN IF`, never inside a calculation. An empty string, a whitespace-only string, an empty list, an empty object and an absent field are all blank; `0` and `false` are present. This is the same definition `REQUIRED` uses, so the operand is a plain `@field`: `ISBLANK(@items)` already is the array test | | LLM\_JUDGE(prompt) STRINGIFY(value) | **Asks a model to decide a question you compose.** You build the whole prompt with `CONCAT` or `&`, dropping values into it with `STRINGIFY`, which renders any value as JSON text and is the only way an object or a list reaches a string (`CONCAT` renders a container as empty on purpose). It answers true or false. The only construct here whose answer is not a pure function of your data: it calls out to a language model, so it costs money and latency and two evaluations may differ once the cached verdict expires. Reach for it only when no deterministic comparison will do (`SLUGIFY(@a) == SLUGIFY(@b)` and `CONTAINS` are free), and gate it behind a cheap condition such as `IF @a != @b THEN ASSERT LLM_JUDGE(…)`. It **fails closed**: a timeout or an unreadable answer fails the rule rather than passing it, and a judge that is not configured at all reports the rule as an error instead of a failure | | STARTS\_WITH(@field, prefix) ENDS\_WITH(@field, suffix) CONTAINS\_SUBSTR(@field, needle) | **String predicates**, BigQuery's spellings. They test a prefix, a suffix or a substring, NOT presence, so the blankness rules above do not apply. Case and accents are folded on both sides, and an empty prefix/needle is `false` rather than vacuously true. Like the row above they go where a CONDITION goes, never inside a calculation, so write `ASSERT STARTS_WITH(@iban, 'FR')` and never `== 'true'`. `CONTAINS_SUBSTR` is the function spelling of the `CONTAINS` operator and behaves identically, list haystacks included | | CONTAINS\_ALL\_WORDS(haystack, needle) | **Bags of words, not characters.** True when every WORD of the needle appears somewhere in the haystack, in any order and not necessarily next to each other. `CONTAINS_ALL_WORDS('GABRIEL RAPHAEL BRAMI', 'GABRIEL BRAMI')` is true where `CONTAINS` is false. This is what survives a middle name the page omits, a `NOM Prénom` inversion, or a `SARL` that moved to the other end. Whole words only: `'GABRIELLE BRAMI'` does **not** contain the words of `'GABRIEL BRAMI'`, which is what separates it from `CONTAINS_SUBSTR`. Case, accents and punctuation are folded; the haystack comes first and may carry extra words, so either direction is `CONTAINS_ALL_WORDS(@a, @b) OR CONTAINS_ALL_WORDS(@b, @a)`. An empty needle and a missing haystack are both `false`. A list haystack matches when ANY element carries every word. **On people, read it as "could be the same person", not "is"**: a sibling's `RAPHAEL BRAMI` is also inside `GABRIEL RAPHAEL BRAMI`, so pin the identifying words or add a second check where that would matter | | ALL(condition) ANY(condition) | **Quantifiers over one array's elements.** The condition is evaluated once per element, and its `[]` references resolve to that element: `ALL(@releves[].solde > 0)`, `ANY(@representants[].nom == @nom_client)`. The condition may combine `AND`/`OR`/`NOT` and nest `ISPRESENT` or a string predicate. It may also read a scalar field beside the element references, and carry arithmetic on the RIGHT side of a comparison (`ALL(@plafond >= @lines[].qty * @lines[].price)`). It needs at least one `[]` reference, and all of them must be on the same array. It may not nest an aggregate or another quantifier. Like `ISPRESENT`, `ALL` and `ANY` are predicates: they go where a condition goes (`IF`, `ASSERT`, `WARN IF`, under `NOT`), and `ALL(…) == 'true'` is a parse error. **Never vacuous**: an empty or absent array is `false` for both, so “no element does” is written `NOT ANY(…)`. A failure reports a tally, for example `ALL(@bs[].solde > 0) [2 of 3 elements]`. The names are ours: SQL's `ALL`/`ANY` are subquery comparison operators, a different shape | | CONCAT(a, b, ...) LOWER(value) UPPER(value) LENGTH(value) LEFT(value, n) RIGHT(value, n) SUBSTRING(value, position[, length]) | **String functions**, BigQuery's spellings. Unlike the predicates above they return a value, so they go inside a calculation or on either side of a comparison: `ASSERT LENGTH(@iban) == 27`, `ASSERT LEFT(@siret, 9) == @siren`. `CONCAT` takes two or more arguments and joins them as text, like `&`, with one difference: `&` fails the rule on a list or an object, `CONCAT` renders it as empty. Positions are 1-based and a negative `SUBSTRING` position counts back from the end, as in BigQuery: `SUBSTRING(@iban, -4)` is the last four characters. A length of zero or less gives an empty string rather than an error. **A missing value reads as the empty string**, where BigQuery would return NULL. A number that is a whole number renders without its `.0`, so `LENGTH` of a 13-digit invoice number is 13. A list or an object renders as empty; use `STRINGIFY` to turn one into text | | SLUGIFY(value) | **Compare two human-typed strings for “the same thing”.** Removes accents, lower-cases, and turns every run of characters that are not letters or digits into one `-`: `SLUGIFY("Éditions Château d'Yquem, SARL")` is `editions-chateau-d-yquem-sarl`. So `SLUGIFY(@a) == SLUGIFY(@b)` ignores case, accents, punctuation and spacing without a regex. Ours, not BigQuery's. Word order still counts; for that use `CONTAINS_ALL_WORDS` | | IFNULL(value, fallback) | The value, or the fallback when it is absent by the definition above. Arithmetic on a missing field fails the rule by design, so this is how an optional field enters a formula: `@total = @base + IFNULL(@bonus, 0)`. Only absence takes the fallback; an error inside the first argument still fails the rule | | MIN(a, b, ...) MAX(a, b, ...) | Smallest / largest of two or more values. The argument count decides which function you get: with a SINGLE bracketed argument (`MIN(@bank_statements[].start_date)`) they are the array aggregates instead (see [Aggregate Functions](#aggregate-functions)). A single argument without `[]` is an error | | ROUND(value[, digits]) | Rounds half *up*, the way an invoice does: `ROUND(2.5)` is 3. Without `digits`, rounds to an integer | | ABS(value) | Absolute value of a numeric expression. Wrap a difference to compare a gap without caring about its direction. For “equal within a tolerance” prefer `WITHIN`, which already means exactly that | | DATEDIFF(end, start, unit) | Signed count of *complete* calendar units from `start` to `end`, first date minus second. 17 years and 11 months is `17 YEAR`, which is what an age check means. Arguments are date terms only: a field, `DATE(...)`, `DATETIME(...)` or `TODAY` | ``` # Presence as a question: an optional block that must be complete once started IF ISPRESENT(@guarantor) THEN REQUIRED @guarantor_income IF ISBLANK(@married_name) THEN REQUIRED @birth_name WARN IF ISBLANK(@iban) ASSERT NOT ISBLANK(@tax_number) # Substring, folded like == ASSERT @line_label CONTAINS 'urssaf' ASSERT @line_label NOT CONTAINS 'cancelled' IF @title CONTAINS 'invoice' THEN REQUIRED @total_incl_tax # Every element / at least one element - conditions, never vacuous ASSERT ALL(@statements[].balance > 0) IF ANY(@representatives[].role == 'Manager') THEN REQUIRED @company_register WARN IF NOT ALL(@statements[].is_valid == 'false' OR @statements[].balance > 0) # Membership in an array expression, element by element ASSERT @client_name IN ARRAY_AGG(@representatives[].name) ASSERT @naf_code NOT IN ARRAY_CONCAT(@excluded_codes, @suspended_codes) # Fallbacks, so an optional field can enter a formula @total = @base + IFNULL(@bonus, 0) ASSERT IFNULL(@married_name, @birth_name) == @account_holder # Scalar helpers ASSERT @rent <= MAX(@income_1, @income_2) / 3 @vat = ROUND(@amount_excl_tax * 0.2, 2) # Age and gaps ASSERT DATEDIFF(TODAY, @birth_date, YEAR) >= 18 ASSERT ABS(DATEDIFF(@signature_date, @issue_date, DAY)) <= 30 # Power, and building text @area = @side ^ 2 @full_name = @first_name & ' ' & @last_name ASSERT @account_holder == @first_name & ' ' & @last_name ``` [Date Operations](#date-operations) ----------------------------------- HoloLang supports date literals, the `TODAY` keyword for relative date checks, and date interval tolerances. ### Date Literals Use `DATE(year, month, day)` for fixed date comparisons: ``` # Compare against a fixed date ASSERT @due_date > DATE(2026, 01, 15) ASSERT @start_date < @end_date # Equality with date interval tolerance ASSERT @filing_date == DATE(2026, 01, 15) WITHIN 7 DAY ``` ### TODAY - Relative Date Checks Use `TODAY` for dynamic date comparisons relative to the current date. Combine with `+` or `-` and a date interval for offset checks: ``` # Must be in the future ASSERT @due_date > TODAY # Must be within the last 6 months ASSERT @statement_date > TODAY - 6 MONTH # Must not be more than 1 year out ASSERT @end_date < TODAY + 1 YEAR # Must be within 3 weeks of today ASSERT @filing_date == TODAY WITHIN 3 WEEK # Conditional: active items must expire in the future IF @is_active == 1 THEN ASSERT @expiry_date > TODAY ``` | Interval Unit | Example | Description | | --- | --- | --- | | DAY | TODAY + 30 DAY | Calendar days | | WEEK | TODAY - 3 WEEK | Calendar weeks (7 days) | | MONTH | TODAY + 6 MONTH | Calendar months (Jan 15 + 1 MONTH = Feb 15) | | QUARTER | TODAY + 2 QUARTER | Calendar quarters (3 months) | | YEAR | TODAY - 1 YEAR | Calendar years | Note:`TODAY` is case-insensitive (`today`, `Today` all work). Date intervals use calendar-based arithmetic: `TODAY + 1 MONTH` from January 31 gives February 28 (not 30 days later). [Aggregate Functions](#aggregate-functions) ------------------------------------------- Aggregate functions operate on array fields. Use `[]` to indicate array iteration: The one bracket rule.`[]` means *per element*, and it only ever lives inside an aggregate's argument: `COUNT(@representants[])`, `SUM(@items[].amount)`, `ARRAY_AGG(@reps[].nom)`. A *bare* array path is the array as ONE value, which is what the `ARRAY_*` value functions take: `ARRAY_LENGTH(@representants)`, `ARRAY_TO_STRING(@representants, ', ')`, `STRINGIFY(@representants)`. A `[]` anywhere else is a parse error with a hint, and a path carries at most one `[]`. | Function | Description | Syntax | | --- | --- | --- | | SUM | Sum of field values in array | SUM(@array[].field) | | COUNT | Number of items in array | COUNT(@array[]) | | AVG | Average of field values | AVG(@array[].field) | | ARRAY\_AGG | Collect one value per element into an ARRAY. `ORDER BY` sits *inside* the parentheses (BigQuery's slot), `WHERE` outside like every aggregate; both name element-relative fields (dotted allowed). Skips absent values by `COUNT`'s non-null rule, so `ARRAY_LENGTH(ARRAY_AGG(@x[].f)) == COUNT(@x[].f)` always holds | ARRAY\_AGG(@array[].field [ORDER BY field [ASC|DESC]]) [WHERE field OP value] | | STRING\_AGG | Join one value per element into a single string; the delimiter defaults to `','` (BigQuery's default) | STRING\_AGG(@array[].field[, delimiter] [ORDER BY ...]) [WHERE ...] | | MIN MAX | Smallest / largest element: numbers, dates AND strings (dates in date order). One bracketed argument; two or more arguments is the scalar variadic form above. An empty or fully-filtered set is *absent* (quiet), like any missing path | MIN(@array[].field) [WHERE ...] | | WHERE | Filter array items before aggregating (compare one item field to a literal) | SUM(@array[].field) WHERE type == 'x' | ``` # Sum all amounts in line items @total = SUM(@items[].amount) # Count number of transactions ASSERT COUNT(@transactions[]) > 0 # Verify total matches sum of parts ASSERT @invoice_total == SUM(@line_items[].subtotal) WITHIN 0.01 # Check average value ASSERT AVG(@scores[].value) >= 50 # Combine with other expressions @grand_total = SUM(@items[].amount) + @tax + @shipping # Filtered aggregates (WHERE clause on an item field) @revenue = SUM(@items[].amount) WHERE category == 'Revenue' ASSERT COUNT(@transactions[]) > 0 AND SUM(@transactions[].amount) != 0 # Collect elements into an array, and scan it ASSERT ARRAY_AGG(@representants[].nom) CONTAINS @nom_client # Join elements into one string (delimiter defaults to ',') ASSERT STRING_AGG(@lignes[].description, ' | ') CONTAINS 'panneau' # Oldest / newest element - MIN/MAX work on numbers, dates and strings ASSERT MIN(@bank_statements[].start_date) <= TODAY - 3 MONTH ASSERT MAX(@bank_statements[].end_date) >= TODAY - 1 MONTH ``` Note: Aggregate functions return 0 for empty arrays. Non-numeric values in the array are ignored. That quiet skip belongs to the bare-path spelling (`SUM(@x[].amount)`). The element-expression spelling below is loud instead: the spelling chooses the contract. ### Element Expressions The aggregate argument may be a full expression over one array's `[]` references (arithmetic, `CONCAT`, any value function), evaluated once per element: ``` # Per-element arithmetic: line totals against the invoice total ASSERT SUM(@lignes[].quantite * @lignes[].prix_unitaire) == @total WITHIN 0.01 # Per-element composition, ordered, filtered, joined into one readable string ASSERT ARRAY_TO_STRING(ARRAY_AGG(CONCAT(@bank_statements[].start_date, ' - ', @bank_statements[].end_date) ORDER BY start_date) WHERE is_valid == 'true', ', ') CONTAINS '2026-01-01' # A scalar reference beside element references is legal - it reads the # document once per element ASSERT ARRAY_AGG(CONCAT(@societe, ':', @reps[].nom)) CONTAINS 'ACME' ``` Three rules apply: * All `[]` references in one aggregate must share the same array. Two different arrays are refused with an explanation. * Aggregates cannot nest. * The expression form is **loud on bad data**: a non-numeric string inside `SUM`'s arithmetic errors the rule, where the bare-path form keeps its historical quiet skip. ### Quantifiers: ALL / ANY `ALL(condition)` and `ANY(condition)` evaluate one condition once per element of one array, the same element context an aggregate's argument gets. They are *predicates*, not values: they go where a condition goes and take `NOT`, `AND` and `OR`. ``` # Every statement in credit ASSERT ALL(@releves[].solde > 0) # A condition on any element, gating a requirement IF ANY(@representants[].qualite == 'Gérant') THEN REQUIRED @kbis # Composed per element; a scalar field beside the element references is legal WARN IF NOT ALL(@bs[].is_valid == 'false' OR @bs[].solde > 0) ASSERT ANY(@representants[].nom == @nom_client) # Predicates nest inside; arithmetic is allowed on the RIGHT side ASSERT ALL(ISPRESENT(@representants[].nom)) ASSERT ALL(@plafond >= @lines[].qty * @lines[].price) # "No element does" - because an empty array is false for ANY as well ASSERT NOT ANY(@bs[].solde < 0) ``` Four rules apply: * The condition holds at least one `[]` reference, all on one array. Two roots are refused. * No aggregate or quantifier nests inside. * A quantifier is never a value, so `ALL(…) == 'true'` is a parse error. * **Neither is ever vacuous.** An empty or absent array is `false` for `ALL` as for `ANY`, so an array with nothing in it never passes as “every element passed”. Write “no element does” as `NOT ANY(…)`. A failing quantifier reports a tally, `ALL(@bs[].solde > 0) [2 of 3 elements]`, and the same `2/3` lands in the rule's actual values. In a dossier's consistency rules the array may also be a requirement's documents, for example `ALL(@case.documents.releves[].siren == @case.documents.kbis.siren)`; see the dossier documentation. ### Array Value Functions These take the **bare** array: the array as one value, no `[]`. Names and semantics are BigQuery's; the recorded divergences follow the language's own missing-path rule (quiet absence) rather than BigQuery's NULL errors. | Signature | Description | | --- | --- | | ARRAY(@a, @b, ...) | Build an array from scalar values. SKIPS absent values (BigQuery errors on NULL; recorded divergence). An array argument is a loud error pointing at `ARRAY_CONCAT` | | ARRAY\_CONCAT(@arr1, @arr2, ...) | Merge arrays into one; an absent operand reads as empty | | ARRAY\_LENGTH(@arr) | Number of elements. An absent array is 0; a present scalar raises loudly | | ARRAY\_TO\_STRING(@arr, delimiter) | Join the elements into text. THE way an array reaches `CONCAT` chains, failure messages and `LLM_JUDGE` prompts as readable text. Absent → `''`; object elements raise loudly (a container has no text value) | | ARRAY\_FIRST(@arr) ARRAY\_LAST(@arr) | First / last element. Empty or absent → quietly absent (BigQuery raises; recorded divergence, because our missing-path rule is quiet) | | ARRAY\_SLICE(@arr, start, end) | BigQuery's contract verbatim: 0-based, BOTH offsets inclusive, negative counts from the end, out-of-range clamps | | ARRAY\_REVERSE(@arr) | The elements in reverse order | Where an array value can flow. An array value is accepted in four places: * as an `ARRAY_*` function's array operand; * inside `STRINGIFY`; * as the LEFT (haystack) side of `CONTAINS`, `CONTAINS_SUBSTR` and `CONTAINS_ALL_WORDS`, where `ARRAY_AGG(@reps[].nom) CONTAINS 'DUPONT'` scans the elements; * as the RIGHT side of `IN` / `NOT IN`, where it is a set of whole values. `@nom_client IN ARRAY_AGG(@reps[].nom)` tests each element with the engine's own `==`, as `IN ('a', 'b')` does, and an empty set is `false`, never an error. Only an array *expression* gets this; the older `@x IN @field` keeps its split-the-string reading. Scalar positions refuse an array at authoring time: `ARRAY_AGG(...) == 'x'`, arithmetic, `CONCAT` arguments (use `ARRAY_TO_STRING`), and `LLM_JUDGE`'s prompt directly (wrap it in `STRINGIFY` or `ARRAY_TO_STRING`). ### Element Indexing A path may select one element by position: 0-based like BigQuery's bare subscript, and negative counts from the end (our recorded divergence: BigQuery only has that in `ARRAY_SLICE`). By deliberate contrast, `SUBSTRING`, `LEFT` and `RIGHT` stay 1-based, BigQuery's own split. ``` # First and last element ASSERT @representants[0].nom == @nom_dirigeant ASSERT @representants[-1].nom == @nom # Composes with dotted paths and with iteration ASSERT @a[0].b[2].c == 5 ASSERT COUNT(@reps[0].adresses[]) > 0 # Out of range resolves quietly to absent, like any missing path - gate it: IF COUNT(@representants[]) > 0 THEN ASSERT ISPRESENT(@representants[0].nom) ``` An indexed path is legal everywhere a plain reference is (comparisons, `ISPRESENT`/`ISBLANK`, function arguments), but not as a formula assignment target. --- ### HoloLang rules URL: https://app.holofin.ai/docs/api/v1.1/hololang/rules/ HoloLang / Rule Types ===================== v1.1 The five rule types (REQUIRED, VALIDATE FORMAT, ASSERT, IF/THEN and WARN), plus length validation and formula syntax. [Rule Types](#rule-types) ------------------------- | Rule Type | Syntax | Purpose | | --- | --- | --- | | Presence | REQUIRED @field | Ensure field exists and is non-empty | | Format | VALIDATE @field[, @field2, ...] FORMAT TYPE | Pattern validation (SIRET, DATE, MRZ\_TD1, etc.) | | Formula | @total = @a + @b | Computed field assignment | | Assertion | ASSERT @a == @b WITHIN n | Equality check with tolerance | | Comparison | ASSERT @a > 0 | Exact comparison (no tolerance) | | Conditional | IF cond THEN rule | Execute rule when condition is met | | Warning | WARN IF cond | Non-blocking alert | ### REQUIRED - Presence Validation Ensures a field is present and non-empty: ``` # Basic requirement REQUIRED @company_name # Conditional requirement IF @company_type == 'SA' THEN REQUIRED @capital_social ``` ### VALIDATE FORMAT - Format Validation Validates field values against predefined or custom patterns: | Format | Description | Example Value | | --- | --- | --- | | SIRET | French 14-digit business ID | 12345678901234 | | DATE | Date (YYYYMMDD or DD/MM/YYYY) | 31/12/2024 | | EMAIL | Email address | user@example.com | | NOT\_EMPTY | Non-empty string | Any non-blank value | | IBAN | Bank account: country, length and ISO 7064 mod-97 check digits | FR14 2004 1010 0505 0001 3M02 606 | | /regex/ | Custom regex pattern | /^[A-Z]{3}-\d{4}$/ | | MRZ\_TD1 | TD1 identity card MRZ (ICAO 9303, 3 lines × 30 chars) | Requires 3 fields (see below) | | MRZ\_TD3 | TD3 passport MRZ (ICAO 9303, 2 lines × 44 chars) | Requires 2 fields (see below) | | MRZ\_CONSISTENCY | MRZ agrees with the visual fields; the layout (TD1, TD2, TD3, French CNI) is detected | Requires 7 fields (see below) | | MRZ\_TD1\_CONSISTENCY MRZ\_TD3\_CONSISTENCY MRZ\_FRA\_CNI\_CONSISTENCY | Same check for one fixed layout, with the MRZ split into one field per line | Requires 9 fields (see below) | | ADDRESS\_FR | French address exists, confirmed to the house number (Base Adresse Nationale) | 8 Boulevard du Port 80000 Amiens | | ADDRESS\_FR\_STREET | French address exists at street level; the house number is not checked | Rue Sainte-Catherine 33000 Bordeaux | **2D-Doc seals need no FORMAT rule.** A schema field declared with the `2D-Doc` type is decoded and its signature verified on every run, and the result is published as `@enriched..*` plus its own `TWODDOC_*` check codes. The former `FORMAT TWODDOC` and `FORMAT TWODDOC_CONSISTENCY` verbs were removed in August 2026 and no longer parse. See [enriched verdict fields](https://app.holofin.ai/docs/api/v1.1/hololang/fields/#derived-fields). #### IBAN Key Verification `FORMAT IBAN` verifies a bank account number against the ISO 13616 structure and the ISO 7064 MOD-97-10 check digits. Everything is computed locally; no bank or external service is contacted. ``` # Country, length and mod-97 check digits VALIDATE @iban FORMAT IBAN # A FORMAT line only judges the value that is there. # Demand presence on its own line: REQUIRED @iban VALIDATE @iban FORMAT IBAN ``` Grouping separators and case are normalised before the check, so `FR14 2004 1010 0505 0001 3M02 606`, `fr1420041010050500013m02606` and the hyphenated form all pass. Spaces (including the non-breaking and narrow non-breaking spaces French typography puts in grouped numbers), hyphens and dots are stripped. The validator checks, in order, so a failure names the actual problem: * Shape: two letters, two digits, then letters or digits * Country: the ISO 3166-1 prefix is a known IBAN country * Length: exact, per country (27 for FR, 22 for DE, 16 for BE…) * Key: the MOD-97-10 remainder over the rearranged string equals 1 Two behaviours to plan for: * **An empty or missing value passes.** Presence is expressed with `REQUIRED` or `FORMAT NOT_EMPTY` on its own line, so an absent optional field is never reported as a forged one. * Around twenty countries issue IBANs without appearing in the ISO 13616 registry (the Maghreb and much of francophone Africa). No authoritative length exists for them, so only the key is enforced. Enforcing a guessed length would reject genuine customer accounts. A valid key proves the value was transcribed correctly, which is what makes it worth running over extracted text: it catches every single-character substitution and very nearly all digit transpositions. It says nothing about whether the account exists or belongs to the holder. Unlike `ADDRESS_FR`, this validator publishes no derived fields. #### Address Coordinates When `ADDRESS_FR` resolves an address, it also publishes the point the reference dataset places it at, as `@enriched..latitude` and `@enriched..longitude`, where `` is the field the `VALIDATE` line ran on. Both are decimal degrees, stored as numbers rather than strings, so later lines can compute with them. ``` # The VALIDATE line publishes the coordinates for the lines below it VALIDATE @adresse FORMAT ADDRESS_FR IF @enriched.adresse.granularity == 'housenumber' THEN ASSERT @enriched.adresse.latitude > 41 ``` Two properties to write rules against: * **They come as a pair or not at all.** A verdict with no point publishes neither field, so a rule that reads one without a guard reports a missing field rather than comparing against an invented zero. * **Granularity decides whether they mean anything.** A `housenumber` or `street` match points at the building or the street. A `municipality` match points at the town centre, which is a different place from the address. Gate on `@enriched..granularity` before trusting a coordinate. Their main consumer is dossier-level: a document uploaded as a geotagged photo can be compared against the address printed on it, which answers « was this proof of address photographed anywhere near the address it shows? ». See [Photo Capture Fields](https://app.holofin.ai/docs/api/v1.1/hololang/fields/#capture-fields). A rule *inside* the extractor asks that same question without these fields, and without this `VALIDATE` line: it names the address field and reads `@enriched..exif_distance_km`, measured against the enrichment sidecar that every *Address (FR)* schema field gets on its own. See [Was the photo taken at the address?](https://app.holofin.ai/docs/api/v1.1/hololang/fields/#capture-address-distance). #### Multi-field Format Validation Some validators require multiple fields. Use comma-separated field references to pass all required values: ``` # MRZ validation for TD1 identity cards (3 lines × 30 characters) # Validates: character set, line lengths, all 4 ICAO 9303 check digits VALIDATE @mrz_line1, @mrz_line2, @mrz_line3 FORMAT MRZ_TD1 # MRZ validation for TD3 passports (2 lines × 44 characters) # Validates: character set, line lengths, all 5 ICAO 9303 check digits VALIDATE @mrz_line1, @mrz_line2 FORMAT MRZ_TD3 ``` The `MRZ_TD1` validator checks: * Each line is exactly 30 characters, using only A-Z, 0-9, and < * Document number check digit (line 1, position 15) * Date of birth check digit (line 2, position 7) * Expiry date check digit (line 2, position 15) * Global check digit spanning both lines (line 2, position 30) The `MRZ_TD3` validator checks: * Each line is exactly 44 characters, using only A-Z, 0-9, and < * Document type is P (passport) * Document number check digit (line 2, position 10) * Date of birth check digit (line 2, position 20) * Expiry date check digit (line 2, position 28) * Personal number check digit (line 2, position 43; < or 0 accepted when the field is unused) * Global check digit (line 2, position 44) #### MRZ Consistency `MRZ_TD1` and `MRZ_TD3` prove the zone is well formed. The consistency formats prove it says the same thing as the rest of the document: they decode the MRZ and compare it with the visual fields, so a photo-edited birth date that left the MRZ untouched fails. The fields are positional; list them in this order. ``` # One field holding the whole MRZ, lines separated by newlines. # The layout is detected from the line count and the length of the data line. VALIDATE @mrz, @date_of_birth, @date_of_expiry, @document_number, @nationality, @sex, @country_code FORMAT MRZ_CONSISTENCY # One field per MRZ line, for a single layout. A two-line layout leaves the third slot empty. VALIDATE @mrz_line1, @mrz_line2, @mrz_line3, @date_of_birth, @date_of_expiry, @document_number, @nationality, @sex, @country_code FORMAT MRZ_TD1_CONSISTENCY ``` * **No MRZ on the document passes.** Make the MRZ `REQUIRED` if its absence should fail. * **Another layout is skipped**, so several consistency rules can sit on one schema. A data line of the wrong length fails when the zone's own document code names this layout: that is a corrupt MRZ, not someone else's. * **A character outside A-Z, 0-9 and < is skipped** as unreadable, not failed. * **A visual field left empty is not compared.** If none of them could be compared the rule fails, because that almost always means the field names do not match the extractor schema. The verdict fields these validators publish (`@enriched..*`) and the per-type 2D-Doc sealed-field catalogue are documented in [Sealed & Derived Fields](https://app.holofin.ai/docs/api/v1.1/hololang/fields/#derived-fields). #### Length Validation Syntax The `LENGTH` modifier checks an exact character count or a range without a regex: | Base Type | Description | Example | | --- | --- | --- | | NUMERIC | Only digits (0-9) | VALIDATE @numero FORMAT NUMERIC LENGTH 13 | | ALPHANUMERIC | Letters and digits (a-z, A-Z, 0-9) | VALIDATE @code FORMAT ALPHANUMERIC LENGTH 10 | | ALPHA | Only letters (a-z, A-Z) | VALIDATE @country FORMAT ALPHA LENGTH 2 | | TEXT | Any characters | VALIDATE @name FORMAT TEXT LENGTH 10-100 | **Length Specifications:** * `LENGTH N` - Exactly N characters * `LENGTH N-M` - Between N and M characters (inclusive) ``` # Built-in formats VALIDATE @company_siret FORMAT SIRET VALIDATE @fiscal_year_end FORMAT DATE VALIDATE @contact_email FORMAT EMAIL VALIDATE @iban FORMAT IBAN # LENGTH syntax - Exact length VALIDATE @numerofiscaldeclarant FORMAT NUMERIC LENGTH 13 VALIDATE @siren FORMAT NUMERIC LENGTH 9 VALIDATE @country_code FORMAT ALPHA LENGTH 2 # LENGTH syntax - Range VALIDATE @company_name FORMAT TEXT LENGTH 2-100 VALIDATE @reference FORMAT ALPHANUMERIC LENGTH 5-20 # Custom regex pattern (for complex patterns) VALIDATE @phone FORMAT /^\+33[0-9]{9}$/ VALIDATE @code FORMAT /^[A-Z]{3}-\d{4}$/ # Multi-field validation (MRZ check digits for ID cards and passports) VALIDATE @mrz_line1, @mrz_line2, @mrz_line3 FORMAT MRZ_TD1 VALIDATE @mrz_line1, @mrz_line2 FORMAT MRZ_TD3 ``` ### Formula Rules - Computed Fields Define computed fields using addition and subtraction. This is a pure assignment without tolerance checking: ``` # Sum line items @f110_brut = @f010_brut + @f014_brut + @f028_brut # Calculate net value @net = @gross - @depreciation # Balance sheet equation @total_passif = @capitaux_propres + @dettes # With nested object fields (e.g., payslip with currency amounts) @netsalary.amount = @grosssalary.amount - @deductions.amount ``` ### ASSERT - Assertions ASSERT rules validate field values. Use `WITHIN` for numeric equality comparisons to handle floating-point tolerance: ``` # Equality with tolerance (recommended for numeric ==) ASSERT @total == @expected WITHIN 0.01 ASSERT @actif_total == @passif_total WITHIN 10 # Exact equality (WITHIN is optional - when omitted, exact match is used) ASSERT @city == 'MADRID' ASSERT @status == 'active' # Other comparisons (exact match, no WITHIN needed) ASSERT @profit >= 0 ASSERT @debt_ratio < 1.0 ASSERT @balance != 0 ASSERT @status != 'inactive' # Strict equality: no case or accent folding, no conversion of any kind ASSERT @reference === 'aB12-X' ``` Note:`WITHIN` is optional for `==` comparisons. Use it for numeric values to handle rounding differences. Omit it for exact string or value matching. Other operators (`>`, `<`, `>=`, `<=`) always perform exact comparisons. String `==` and `!=` ignore case and accents, because the same value legitimately reaches two fields in two presentations (a 2D-Doc seal carries `PILCZER` where the extracted field reads `Pilczer`, and a machine-readable zone cannot carry `Ü` at all). Everything else still counts: punctuation, digits and internal spacing, so `'2.1'` and `'2.10'` remain different. When the exact characters are the point, `===` and `!==` compare the values as they are: no case or accent folding, no numeric or date conversion, and a missing field stays missing rather than becoming an empty string. ### IF/THEN - Conditional Rules Execute a rule only when a condition is met. If the condition is false, the rule passes automatically: ``` # Conditional presence IF @company_type == 'SA' THEN REQUIRED @capital # Conditional assertion IF @is_active == 'yes' THEN ASSERT @balance == @expected WITHIN 0.01 IF @profit > 0 THEN ASSERT @tax > 0 # Conditional formula IF @is_consolidated == 'true' THEN @total = @a + @b + @c # Conditional format validation (skip the check when a related field is absent) IF @nomdeclarant2 != '' THEN VALIDATE @numerofiscaldeclarant2 FORMAT NUMERIC LENGTH 13 IF @country == 'FR' THEN VALIDATE @company_siret FORMAT SIRET # Multiple conditions with AND/OR IF @a > 0 AND @b > 0 THEN ASSERT @c > 0 IF @type == 'A' OR @type == 'B' THEN REQUIRED @code ``` ### WARN IF - Non-blocking Warnings Warnings alert on potential issues without failing validation. The `all_passed` result is `True` even when warnings are triggered: ``` # Standard condition-based warnings WARN IF @capitaux_propres < 0 WARN IF @debt > @equity WARN IF @balance < 0 AND @status == 'active' # Warning based on assertion failure WARN IF NOT ASSERT @expected == @actual WITHIN 10 WARN IF NOT ASSERT @count > 0 ``` --- ### HoloLang fields URL: https://app.holofin.ai/docs/api/v1.1/hololang/fields/ HoloLang / Sealed Fields ======================== v1.1 Verdict fields published by FORMAT validators, and the 2D-Doc sealed fields available on each document type. #### Derived Verdict Fields A **2D-Doc** is the Data Matrix printed on French administrative and commercial documents: avis d'impôt, justificatifs de domicile, bulletins de salaire, relevés de compte. It is a *cachet électronique visible*: the issuer's own copy of the document's key data, signed with a certificate accredited by ANTS (France Titres), so the page can be checked against what its issuer actually emitted. See [2D-Doc seals](https://app.holofin.ai/docs/api/v1.1/extraction/#two-d-doc) for what Holofin verifies, and the [ANTS 2D-Doc page](https://ants.gouv.fr/nos-missions/les-solutions-numeriques/2d-doc) for the specification itself. Looked-up data always hangs off **the field it came from**. A schema field declared with the `2D-Doc` type (say `cachet`) publishes the decoded seal as `@enriched.cachet.*`; `VALIDATE @adresse FORMAT ADDRESS_FR` publishes `@enriched.adresse.*`. Naming the field is what lets one document carry two addresses (a billing one and a delivery one), each with its own verdict. **A 2D-Doc field needs no rule to be decoded and verified.** The seal is read on every run, so its paths are available from the first line of a rule and you write a rule only to pin a sealed value against an extracted one. That pin names the exact sealed field it must equal, which is what catches two values swapped between fields. ``` # No VALIDATE line: `cachet` is a 2D-Doc schema field, decoded on every run. # Always gate a pin on the signature - an unsealed document has nothing to # compare, and an ungated pin would fail it. IF @enriched.cachet.signature_valid == 'true' THEN ASSERT @enriched.cachet.fiscal.declarant_1 == @declarant1 IF @enriched.cachet.signature_valid == 'true' THEN ASSERT @enriched.cachet.adresse_domicile.code_postal == @postcode # Require the seal to belong to the right kind of document. The same document # is sealed under a new type code each specification version, so accept the # whole family and keep the codes quoted. IF @enriched.cachet.status == 'matched' THEN ASSERT @enriched.cachet.doc_type == '18' OR @enriched.cachet.doc_type == '24' OR @enriched.cachet.doc_type == '27' ``` ##### Sealed fields, by document type Which fields a seal carries depends on the document type it was issued for: an avis d'impôt seals declarants and amounts, a justificatif de domicile seals a postal address, a relevé de compte seals an IBAN. Fields under **Always** are mandatory for that type; a rule pinned to a field under **Sometimes** is valid but checks nothing on the documents that omit it. Each name below is a path under your 2D-Doc field's sealed content: for a field named `cachet`, the name `fiscal.declarant_1` is written `@enriched.cachet.fiscal.declarant_1`. Two consequences for rule writing. A type-04 avis d'impôt seals **no address at all**, so an address check against one can never match. The numéro fiscal is mandatory on type 04 but optional on type 18, so the same rule is a real check on one and inert on the other. Every decoded id is named from the specification's own dictionary, which is global to the perimeter, so sealed content is named for **every** type, and a type the presence matrix does not cover still reports `@enriched.cachet.doc_type` correctly, with `structure_complete` reported as `'unknown'` rather than a misleading `'true'`. ##### Composed convenience fields **One path per fact, whichever way the issuer encoded it.** The specification lets an issuer seal a postal address either as one complete string or as one line per data id, and an identity either as discrete name fields or as a single Qualité/Nom/Prénom composite. Both forms are valid, so Holofin fills the gap: a full address and a split name are always readable at the same path, whichever form the document used. A value the issuer actually sealed always wins; Holofin only composes what is missing. `computed` lists the paths Holofin assembled, comma-separated, and is empty when every value came off the seal itself. * `.complete` is a whole postal address: the specification's own complete-address field when the issuer sealed one, otherwise the group's lines joined in French postal order (lignes 2–5, then code postal and commune). Pin a full extracted address against it, never against a single `ligne4`. * When the signature verifies, the sealed address is also resolved through the national address base and the verdict sits **in the same group**: `.status`, `.normalized`, `.ban_id`, `.granularity` and the other address keys, exactly as on an `address_fr` field. Compare a sealed and an extracted address through them (`@enriched.cachet.adresse_domicile.ban_id == @enriched.adresse.ban_id`, gated on both `status == 'matched'`) rather than by string equality, which punctuation and abbreviations defeat. These keys are Holofin's and appear in `computed`. * Slashed composite identity lines (for example `beneficiaire.ligne1` holding `MME/MARTIN/ANNE`) also resolve at `.qualite`, `.nom` and `.prenom`. A composite without the `/` separator is never split, because the part order would be a guess, and discrete name fields sealed by the issuer always win over the split. ##### Envelope properties Available on every 2D-Doc field whatever the document type, alongside the sealed content: * `status`: `'matched'` (signature verified), `'invalid'` (the seal was altered), `'unchecked'` (no opinion: unknown certificate or trust-list gap), `'absent'` (no seal on the page). * `signature_valid`: the strings `'true'`, `'false'`, `'unresolved'`. * `reason`, `doc_type`, `doc_type_label`, `doc_type_family`, `ca_id`, `cert_id`, `emission_date`, `signature_date`. * `structure_complete` (`'true'` / `'false'` / `'unknown'`) and `missing_required`, a comma-separated string of the ids the specification mandates for that type and the seal omits. A seal can verify perfectly and still be incomplete; that is what a stripped and re-signed payload looks like. * `computed`, a comma-separated string of the paths Holofin assembled rather than read off the seal (see composed fields above). Empty when every value came from the issuer, so `computed == ''` asserts that nothing being pinned was reconstructed. * Pins are exact comparisons: no `WITHIN` tolerance is applied. * A numeric extracted field compares correctly against the sealed text, so `'82269'` equals `82269`. Punctuation is not normalised, so pin identifiers, amounts, postcodes and dates exactly; pin a full address via `.complete` and a name via the split `.nom` / `.prenom` fields. * Some ids seal several values at once (the list of forenames is one field holding `CLARA/RACHEL/ELSA`), so use `IN` there rather than `==`, which could never pass. * **Always gate a pin** on `signature_valid == 'true'` or `status == 'matched'`. A document with no seal, or one whose certificate cannot be resolved, has no sealed values, and an ungated pin would reject it for a mismatch it could not have. * A valid signature proves the barcode was not altered since the issuer signed it. It does not prove the document is the most recent one on file. #### Photo Capture Fields A document uploaded as a **photo** rather than a PDF carries EXIF metadata: when it was taken, by what, and where. Holofin reads it from the original bytes at upload, before the image is converted, and publishes it as flat `exif_*` fields. **The same names are available at both rule levels**, deliberately, so a rule reads the same wherever it is written. A document's own validation rules, on a custom extractor, address them bare (`@exif_gps_present`). Case rules address them through the document slot (`@justificatif_domicile.exif_gps_present`), because at that level they are properties of one upload among several. Multi-page uploads are merged into one namespace by consensus: a field is published when every source image that has a value agrees on it. On disagreement the field is dropped and `exif_divergent` is set, which is itself evidence of several different cameras inside what is presented as one document. Check one asymmetry between the levels before you name a schema field `exif_software`. In a **document's own rules the real extracted field wins**: the capture fields are injected into the section output the rule is written against, and silently replacing a value the extractor really produced would misreport the document. In **case rules the capture field wins** and the collision is reported as a warning, because there it sits beside a whole document's aggregated data rather than inside it. ##### Guard fields (on every document) Published on **every** document at both levels, including PDFs that never had EXIF at all. They are always safe to test, which is what makes them usable as the condition of a guarded rule. | Field | Meaning | Values | | --- | --- | --- | | exif\_present | The upload carried EXIF metadata | 'true' / 'false' | | exif\_gps\_present | A usable GPS position was read | 'true' / 'false' | | exif\_divergent | Source images disagreed on a value | 'true' / 'false' | | exif\_source\_count | Number of source images merged | '0', '1', '2'… | | exif\_address\_comparable | Photo position and address position are both known and precise enough to measure between | 'true' / 'false' | ##### Value fields (only when known) Absent unless the photo actually carried them. A rule that reads one on a document that has none reports a missing field, which fails a blocking rule. This is deliberate: an unguarded rule fails visibly instead of comparing against a default. | Field | Meaning | Present when | | --- | --- | --- | | exif\_latitude | Decimal degrees, south is negative | exif\_gps\_present == 'true' | | exif\_longitude | Decimal degrees, west is negative | exif\_gps\_present == 'true' | | enriched..exif\_distance\_km | Great-circle distance from the photo to *that* address field, in kilometres | Document rules. The field resolved at street level or better, and the photo carried GPS | | exif\_address\_distance\_km | Great-circle distance from the photo to the document's single address, in kilometres | exif\_address\_comparable == 'true' | | exif\_captured\_at | Capture timestamp | The camera recorded one | | exif\_camera\_make | Manufacturer | The camera recorded one | | exif\_camera\_model | Model | The camera recorded one | | exif\_software | Software that last wrote the file | The file recorded one | ##### Always guard, and always on a guard field A case rule fails when a field it references is missing, and that check covers the field inside an `IF` condition too. So a guard is only worth writing on a field that is *always there*: guarding on a value field is undecidable on exactly the documents the guard was meant to spare. ``` # WRONG - exif_camera_make is absent on a PDF, so the condition cannot be # answered and the rule fails the dossier on every PDF in it IF @case.documents.justif.exif_camera_make == 'Apple' THEN ASSERT @case.documents.justif.exif_latitude > 41 # RIGHT - exif_gps_present is on every document, so the guard always answers IF @case.documents.justif.exif_gps_present == 'true' THEN ASSERT @case.documents.justif.exif_latitude > 41 # doc_category is injected on every document too, and is the guard to reach # for when a slot mixes photos and PDFs IF @case.documents.justif.doc_category == 'justificatif_domicile' THEN ASSERT @case.documents.justif.exif_divergent == 'false' # The same rule inside the extractor that produced the document, where the # fields are addressed bare rather than through a slot IF @exif_gps_present == 'true' THEN ASSERT @exif_latitude > 41 ``` The guarded form dictates the rule type it can be written in. `IF … THEN ASSERT` is a whole rule rather than a condition, so it belongs in a **consistency rule**, at blocking severity. A validity rule or a warning rule wraps its condition in a keyword that takes an expression, and rejects the guarded form outright. ##### Was the photo taken at the address? When a document is both a geotagged photo and carries an address that resolved to coordinates, Holofin measures the great-circle distance between the two and publishes it. There are two forms of the answer, and the one to reach for first names the address field the photo is measured against. **In a document's own validation rules, name the field.** Every schema property typed *Address (FR)* is resolved against the French national address database during extraction, and the result is attached as the [enrichment sidecar](https://app.holofin.ai/docs/api/v1.1/extraction/#address-enrichment) that rules already read as `@enriched..`. Where that field was placed precisely enough and the photo carried GPS, its entry also carries `exif_distance_km`: ``` ASSERT @enriched.customerAddress.exif_distance_km < 50 ``` Two photos of the same electricity bill, showing `55 RUE DU FAUBOURG SAINT HONORE, 75008 PARIS` and differing only in the GPS position embedded in the image, give the rule above: ``` # photo shot at the address exif_distance_km = 0.044 -> rule passes # same bill, photo shot in Marseille exif_distance_km = 662.887 -> rule fails "Assertion failed: 662.887 < 50.0" ``` Naming the field *is* the disambiguation. A payslip schema carrying both a home address and an employer address gets one honest number for each, and the rule says which one it means: ``` ASSERT @enriched.domicile.exif_distance_km < 5 WARN IF @enriched.employeur.exif_distance_km > 200 ``` Two properties of this form worth stating: * **Nothing has to be validated first.** The sidecar fires on the schema type alone, so no `VALIDATE … FORMAT ADDRESS_FR` line is needed and there is no way for the rule to sit silently inert because the workflow lacks one. * **The key is omitted, never null.** It is published only on an entry that the geocoder placed at house-number or street level *and* that has a photo GPS position to measure from. A commune-level hit is the town-centre point, so a distance from it would be misleading rather than merely imprecise, and no number is published. A rule reading an absent key reports a missing field and fails loudly, rather than comparing against a fabricated zero. `status` is on every sidecar entry and is the natural thing to gate on, but a `'matched'` commune-level entry still carries no distance. ##### The single-address shortcut For the ordinary schema that declares one address, the flat `exif_address_distance_km` saves naming it, and it is the only form available to **case rules**, which see a document's aggregate rather than its schema fields: ``` IF @case.documents.justif.exif_address_comparable == 'true' THEN ASSERT @case.documents.justif.exif_address_distance_km < 50 ``` What the guard is protecting you from: * **A commune-level address match is refused.** When the reference dataset can only confirm the town, the coordinates it returns are the town centre, not the address. The distance measured from them says how far the photo is from the mairie. That is misleading, not merely imprecise, so no distance is published at all. Only house-number and street matches are comparable. * **The distance exists only when the guard is `'true'`.** There is no second way to read a distance nobody vouched for: no coordinates, no address, a PDF upload or a commune-level match all give `'false'` and no value. * **A schema with several addresses publishes nothing here.** This key is published only when the locatable addresses on the document agree on one point, because otherwise « the » distance is not defined: picking one field would make the rule's meaning depend on invisible schema ordering, and taking the smallest would answer a weaker question than the one written. Name the field instead. Two fields holding the same address are not a conflict. * **In a case rule, the address must actually have been looked up.** At that level the coordinates come from the [`@.enriched..*` paths](https://app.holofin.ai/docs/api/v1.1/hololang/rules/#address-coordinates), which exist only on documents whose extraction runs `VALIDATE @ FORMAT ADDRESS_FR`. Without that rule in the workflow there is nothing to compare against, so the guard is permanently `'false'` and the rule silently never fires. Document rules do not have this trap: they measure against the enrichment sidecar, which needs no rule. `exif_address_comparable` answers for this key alone. The per-field distances are independent of it, and are published on a multi-address schema where it is `'false'`. Distances are rounded to the metre. Both endpoints are approximate (consumer GPS is accurate to a few metres at best, and a geocoded address is a building-level point), so thresholds belong at the scale of the question being asked (a few hundred metres for « at this address », tens of kilometres for « in this region »), never at the metre. A large distance is not proof of fraud on its own: photographs of a document are legitimately taken away from the address printed on it. Run this at warning severity unless the product genuinely requires the photo to be taken on site. --- ### HoloLang examples URL: https://app.holofin.ai/docs/api/v1.1/hololang/examples/ HoloLang / Examples & Behavior ============================== v1.1 A complete CERFA validation example, evaluation behaviors, and the error messages rules produce. [Complete Example](#complete-example) ------------------------------------- A complete example validating a CERFA 2033-A balance sheet: ### CERFA 2033-A Validation Rules ``` # ═══════════════════════════════════════════════════════════════ # CERFA 2033-A - Balance Sheet Validation Rules # ═══════════════════════════════════════════════════════════════ # Metadata validation REQUIRED @company_name REQUIRED @company_siret VALIDATE @company_siret FORMAT SIRET VALIDATE @fiscal_year_end_n FORMAT DATE # Balance calculations (pure assignments) @f110_brut = @f010_brut + @f014_brut + @f028_brut @f180 = @f142 + @f176 # Balance check - fundamental accounting equation ASSERT @f110_net_n == @f180 WITHIN 0.01 # Conditional presence rules IF @company_type == 'SARL' THEN REQUIRED @f120 IF @f040_brut > 0 THEN REQUIRED @f042 IF @f136 > 0 THEN REQUIRED @f134 # Conditional assertions IF @has_debt == 'yes' THEN ASSERT @f156 > 0 # Warnings (non-blocking) WARN IF @f136 < 0 WARN IF @f142 < 0 WARN IF NOT ASSERT @f136 == @f134 WITHIN 100 ``` [Key Behaviors](#key-behaviors) ------------------------------- * Case insensitive keywords:`IF`, `if`, `If` all work * Nested field access: Use dot notation for objects, e.g., `@salary.amount`, `@employee.address.city` * Tolerance: Default tolerance is 10.0 for floating-point comparisons, customizable per engine * Null handling: Missing/null fields are treated as 0 (numbers) or "" (strings) * Warnings don't fail:`all_passed` is True even with warnings * Conditional skip: If condition is false, rule passes automatically * Comments: Use `#` for comments in rule files [Error Messages](#error-messages) --------------------------------- When validation fails, HoloLang returns descriptive messages: | Rule Type | Example Message | | --- | --- | | Formula | "Difference of 150.00 (expected: 1000.00, actual: 850.00)" | | Presence | "Field @company\_name is required" | | Format | "Field @company\_siret does not match format SIRET" | | Assertion (==) | "Assertion failed: difference of 100.00 exceeds tolerance 10" | | Assertion (>) | "Assertion failed: -50 > 0" | | Warning | "Warning condition triggered" | --- ## Dossiers ### Dossiers URL: https://app.holofin.ai/docs/api/v1.1/cases/ Dossiers ======== Create a dossier from a case configuration, send the typed values it declares (loan amount, product, participants) and read back where the dossier stands. Documents are sent separately through [workflow runs](https://app.holofin.ai/docs/api/v1.1/workflow-runs/); both write to the same dossier. [Overview](#overview) --------------------- Three steps: pick a configuration with [`GET /case-configs/`](https://app.holofin.ai/docs/api/v1.1/case-configs/), create the dossier with that `config_id`, then send documents with the dossier's `case_id` through [workflow runs](https://app.holofin.ai/docs/api/v1.1/workflow-runs/). The configuration decides which fields exist, which documents are expected and which checks run. Later calls address the dossier by `case_id` alone. `case_id` identifies the dossier. Omit it and Holofin generates one (`case_`), returned in the response. Or send your own reference (a loan number, a CRM id): the first call creates the dossier, later calls update it. Replaying a call is safe. Values **merge**: only the keys you send are touched, an explicit `null` clears a key, and re-sending identical values changes nothing. Values are stored typed: send `250000`, not `"250000"` (a numeric string is accepted and converted). Read the exact payload your configuration expects from [`GET /case-configs/{config_id}`](https://app.holofin.ai/docs/api/v1.1/case-configs/), or open *Show input schema* in the dossier configuration screen for a copyable example. The configuration is attached when the dossier is created and cannot be changed: sending the same `config_id` again is a no-op, a different one is refused with `CONFIG_CONFLICT`. To run the same case under another configuration, create another dossier. A dossier with no configuration (deleted, or created before `config_id` existed) adopts the one you name. [Create or update a dossier](#create-or-update) ----------------------------------------------- `POST /api/v1.1/cases` ### Push dossier data ``` curl -X POST https://app.holofin.ai/api/v1.1/cases \ -H "X-API-Key: $HOLOFIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "config_id": "e5b1c9a0-4f2d-4b8e-9c3a-7d6f1e0a8b42", "case_fields": { "montant": 250000, "emprunteur": { "party_type": "PM", "firstname": "Marie", "lastname": "Durand", "siren": "552100554" } } }' ``` | Parameter | Type | Required | Description | | --- | --- | --- | --- | | case\_id | string | No | Omitted, Holofin generates one and returns it; that call always creates a new dossier. Sent, your own reference (at most 255 characters): creates the dossier on first use and identifies it afterwards. The same `case_id` on `/workflow-runs/` names the same dossier. | | config\_id | string (uuid) | On creation | The case configuration the dossier runs under, from [`GET /case-configs/`](https://app.holofin.ai/docs/api/v1.1/case-configs/). Required when the call creates the dossier (`MISSING_CONFIG_ID`), including when Holofin generates the `case_id`. Optional on an existing dossier: the same id passes, a different one is refused with `CONFIG_CONFLICT`. | | case\_fields | object | No | Values for the fields the configuration declares, as `{"key": value}`. A key naming a declared participant holds that participant's object instead (see below). An undeclared key is rejected (`UNKNOWN_INPUT_FIELD`). Omit it when the configuration declares nothing to send: `config_id` alone opens the dossier. | ### Participants A participant is a key of `case_fields` named after its role, holding that participant's values. A role holding several participants takes a list of such objects. The configuration declares a type of participant and how many the dossier expects: «borrower» one or two people, «guarantor» exactly one. The count is a completeness requirement, not a request validation: fewer participants than the minimum is accepted and the dossier stays `incomplete` until the rest arrive. The bounds are published under `x-participant-cardinality` in the configuration's schema. * Values merge into the participant under that key; it is created in the same call when it does not exist yet. Replaying a call is idempotent. * `party_ref` - Optional. Your own reference for the participant, the one `party_refs` on `/workflow-runs/` names when a document is filed against them. Defaults to the field key. * `party_ref` is required when one key holds a list of several participants (`FIELDS_WITHOUT_PARTY`). * A participant type capped at one takes an object; a list under that key is rejected (`INVALID_INPUT_FIELD`). * `party_type` - Optional. The participant's legal nature: `PM` (personne morale), `PP` (personne physique), `PPNS` (personne physique non salariée) or `BE` (bénéficiaire effectif). Descriptive only: it does not decide which documents are expected. Rules read it as `@case..party_type`. Kept when a later call omits it. * The same `party_ref` twice in one call is rejected (`INVALID_PARTY_REF`). `party_ref`, `role`, `party_refs` and `party_fields` at the top level of the body are refused (`UNSUPPORTED_FIELD`). Participants are declared inside `case_fields`; `party_refs` belongs on `/workflow-runs/`, where it names the participants a document belongs to. ### Sending documents Send each document as its own [`POST /workflow-runs/`](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run) carrying the dossier's `case_id`. The dossier must exist first: a run never opens one. Holofin then works out which participant the document belongs to: * A dossier with a single participant files every document against that participant, so `case_id` alone is enough. * With several participants, a document is filed against the one its **extracted identity** designates, when the name read off it matches exactly one participant's declared identity. * A bundle the pipeline splits is attributed **part by part**, so one file can hold documents of several participants. Send `party_refs` on the run when you know whose the file is. One reference files it against that participant; several declare a jointly held document, such as a joint account statement, that counts for each of them. On a bundle, `party_refs` names the file's owner: parts whose own content designates another participant still go to that one, and the rest fall back to your declaration. A document nothing attributes (no usable name, or a name two participants share) counts toward no requirement. [`GET /cases/{case_id}`](#read) lists it under `unassigned_documents[]` with `reason` set to `awaiting_attribution`. ### Response The response is the dossier as your call left it, re-evaluated against the configuration: the same body [`GET /cases/{case_id}`](#read) returns, documented under [The dossier body](#dossier-body). `case_id` is the identifier you sent, or the one Holofin generated. A first call with no `case_fields` already answers the full requirement list, so one call tells you what to collect. Read `case_status` and the `fields` whose `state` is `missing` to know what to send next. [Read a dossier](#read) ----------------------- `GET /api/v1.1/cases/{case_id}` returns the dossier: where it stands, what it still needs, and the verdict of every configured check. Instead of polling, subscribe to the `case.updated` and `case.completed`[webhook events](https://app.holofin.ai/docs/api/v1.1/webhooks/#dossier-events): the same body is pushed on every status or coherence change. ``` curl https://app.holofin.ai/api/v1.1/cases/case_9f3k2m81xq7c \ -H "X-API-Key: $HOLOFIN_API_KEY" ``` ### The dossier body The body mirrors the configuration: the dossier's own `fields` and `requirements` at the root, `participants` keyed by role with each person's own `fields` and `requirements`, the configured checks in one flat `rules` list, and the documents counting toward nothing in `unassigned_documents`. Every document appears in exactly one place. ``` { "case_id": "case_9f3k2m81xq7c", "config_id": "6dc962d3-14df-4d41-b858-75f2b24b648f", "config_name": "Crédit professionnel", "case_status": "incomplete", "decision": "none", "coherence": "ko", "created_at": "2026-08-10T07:59:02Z", "evaluated_at": "2026-08-10T08:04:07.804604Z", "fields": { "montant": {"label": "Montant du prêt", "value": 250000, "state": "provided", "required": true}, "duree": {"label": "Durée (mois)", "value": null, "state": "missing", "required": true} }, "requirements": [ {"key": "kbis", "label": "KBIS", "state": "satisfied", "min": 1, "max": null, "provided": 1, "accepted_categories": ["kbis"], "conditional": false, "activation": null, "documents": [ {"document_id": "doc_k2VgTmN4pQ7cWxYz", "part_key": "", "state": "processed", "name": "kbis.pdf", "run_id": "wr_8sK2mQ4tXcV1nRy7", "category": "kbis", "pages": [1, 2], "owners": []} ]}, {"key": "hote_identite", "label": "Pièce d'identité de l'hôte", "state": "not_required", "min": 1, "max": null, "provided": 0, "accepted_categories": ["piece_identite"], "conditional": true, "activation": {"active": false, "condition": "ASSERT @case.documents.justificatif_domicile.doc_category == 'attestation_hebergement'"}, "documents": []} ], "participants": { "emprunteur": { "label": "Emprunteur", "state": "missing", "min": 2, "max": 2, "provided": 1, "parties": [ {"party_ref": "REF-MARIE", "party_type": "PP", "fields": { "firstname": {"label": "Prénom", "value": "Marie", "state": "provided", "required": true}, "lastname": {"label": "Nom", "value": null, "state": "missing", "required": true} }, "requirements": [ {"key": "releve_bancaire", "label": "Bank statement", "description": "Last two months, same account as the RIB", "state": "missing", "min": 2, "max": null, "provided": 1, "invalid": 1, "accepted_categories": ["releve_bancaire"], "conditional": false, "activation": null, "documents": [ {"document_id": "doc_5rT9wLnB2xQjPmVd", "part_key": "", "state": "processed", "name": "releve_juin.pdf", "run_id": "wr_3fW7kZq9YtM5cHx2", "category": "releve_bancaire", "pages": [1], "owners": [{"party_ref": "REF-MARIE", "source": "declared"}]}, {"document_id": "doc_8fRsLdH6wB3nJvQa", "part_key": "", "state": "invalid", "name": "releve_juillet.pdf", "run_id": "wr_6bN4pDs8RvK1mTy3", "category": "releve_bancaire", "pages": [1], "owners": [{"party_ref": "REF-MARIE", "source": "inferred"}]} ]} ]} ] } }, "rules": [ {"code": "case_rule_a3f19c04d2", "label": "Capital social suffisant", "kind": "consistency", "severity": "blocking", "status": "ko", "reason": null, "depends_on": [ {"type": "requirement", "key": "kbis", "party_ref": null, "state": "ready"}, {"type": "dossier", "key": "dossier", "party_ref": null, "state": "ready"}], "documents": [], "values": [ {"field": "kbis.capital", "value": 5000, "document_id": "doc_k2VgTmN4pQ7cWxYz"}, {"field": "dossier.montant", "value": 250000}], "provisional": false, "rule": "ASSERT @case.documents.kbis.capital >= @case.montant", "description": null}, {"code": "case_rule_9d81b2c7e4", "label": "Relevé de moins de 3 mois", "kind": "validity", "severity": "blocking", "status": "ko", "reason": null, "depends_on": [ {"type": "requirement", "key": "releve_bancaire", "party_ref": "REF-MARIE", "state": "ready"}], "documents": ["doc_8fRsLdH6wB3nJvQa"], "values": [], "provisional": false, "rule": "@releve_date > TODAY - 3 MONTH", "description": null}, {"code": "case_rule_5c40aa91f8", "label": null, "kind": "consistency", "severity": "warning", "status": "skipped", "reason": "slot_inactive", "depends_on": [ {"type": "requirement", "key": "hote_identite", "party_ref": null, "state": "inactive"}, {"type": "requirement", "key": "kbis", "party_ref": null, "state": "ready"}], "documents": [], "values": [], "provisional": false, "rule": "WARN IF @case.documents.hote_identite.nom != @case.documents.kbis.dirigeant", "description": null} ], "unassigned_documents": [ {"document_id": "doc_5tXcPeW9mK4dYbRu", "part_key": "", "name": "note_interne.pdf", "run_id": "wr_2hJ6gFp4LwS9qXc8", "category": "autre", "pages": [1], "owners": [], "reason": "category_not_accepted"}, {"document_id": "doc_3wQhBnT7yL2xMcVe", "part_key": "segment_1", "name": "liasse.pdf", "run_id": "wr_7mV3cRt6NbD2kZw5", "category": "kbis", "pages": [1, 2], "owners": [], "reason": "requirement_full"}, {"document_id": "doc_3wQhBnT7yL2xMcVe", "part_key": "segment_2", "name": "liasse.pdf", "run_id": "wr_7mV3cRt6NbD2kZw5", "category": "facture", "pages": [3], "owners": [], "reason": "category_not_accepted"} ] } ``` #### Dossier The root object. Each nested object is described in its own table below. | Field | Type | Description | | --- | --- | --- | | case\_id | string | The dossier's reference: the one you sent, or the one Holofin generated. | | config\_id, config\_name | string | The configuration the dossier runs under, as published by `GET /case-configs/`. `null` only on a dossier created before configurations were required. | | case\_status | string | `incomplete` (values, documents or participants missing, or a failed check), `pending_processing`, `pending_review`, `rules_failed` (a configured check could not run: a configuration problem, not something a document fixes), `complete`, or `no_config` (no configuration attached). | | decision | string | The reviewer's decision, separate from `case_status`: `none`, `in_review`, `validated` or `rejected`. Set in the Holofin app, read-only here. | | coherence | string | The overall verdict of the configured checks: `ok`, `ko` or `warning`. `null` when there is no verdict (no rules configured, or nothing read yet). Read-only. | | created\_at, evaluated\_at | string | ISO 8601 UTC. `evaluated_at` is the last evaluation, with microseconds. | | fields | object | One [field](#body-field) per dossier-level field the configuration declares, keyed by field key. The keys are the ones `case_fields` accepts, so the `missing` entries are what to send next. | | participants | object | One [role](#body-role) per participant type the configuration declares, keyed by role, present even when nobody fills it yet. | | requirements | array | The dossier-level expected documents, one [requirement](#body-requirement) each. Participant-owned requirements sit under their party instead. | | rules | array | One flat list of the configured checks and their current verdicts, one [rule](#body-rule) each, wherever they attach. | | unassigned\_documents | array | The units counting toward nothing: [document rows](#body-document) with a `reason`. An empty list means everything you sent counts. | #### Field fields. | Field | Type | Description | | --- | --- | --- | | label | string | The configured name. `null` when the configuration no longer declares the field. | | value | any | The current merged value, coerced to its declared type. `null` when absent. | | state | string | `provided`, `missing` (required and absent: the dossier stays `incomplete` until it arrives) or `empty` (not required and absent). | | required | boolean | Whether the configuration demands the field as of the last evaluation. A conditionally required field may flip as the dossier fills in. | #### Role participants. | Field | Type | Description | | --- | --- | --- | | label | string | The configured name of the participant type. | | state | string | `ok`, `missing` (below the minimum) or `exceeded` (above the maximum). | | min, max | integer | How many people the dossier expects. `max` is `null` when uncapped. | | provided | integer | How many people currently fill the role. | | parties | array | Always a list, one [party](#body-party) per person. | #### Party parties[] | Field | Type | Description | | --- | --- | --- | | party\_ref | string | Your reference for the person, the one `party_refs` on `/workflow-runs/` names. `null` when none was sent. | | party\_type | string | The legal nature (`PM`, `PP`, `PPNS`, `BE`), or `null`. | | fields | object | This person's values, same shape as the dossier's [fields](#body-field). | | requirements | array | The [requirements](#body-requirement) this person owns. A participant-owned requirement appears only here. | #### Requirement requirements[] A requirement owned by a participant the dossier does not hold yet has no row; it appears under the person once they are added. | Field | Type | Description | | --- | --- | --- | | key, label | string | The requirement's key and configured name. | | description | string | The configuration's own explanation of the requirement. Omitted when none. | | state | string | `satisfied`, `pending` (enough documents received, some still processing), `missing`, `exceeded` (more counting documents than `max`) or `not_required` (a conditional requirement whose condition is not met). | | min, max | integer | How many counting documents are expected. `max` is `null` when uncapped; `min: 0` is an optional requirement. | | provided | integer | Documents processed and valid. | | invalid, in\_flight, failed | integer | Documents that did not hold up, are still processing, or failed. Omitted when zero. | | accepted\_categories | array | The document types that satisfy the requirement. | | group | string | Present only when the configuration sets one. | | conditional | boolean | `true` for a requirement that only applies when a condition holds. | | activation | object | `null` for a plain requirement. Otherwise `{"active", "condition"}`: `active` says whether the condition currently holds, `condition` is the configured rule text (several lines mean any one of them activates), `null` unless rule details are enabled for your organization. | | documents | array | The [document rows](#body-document) counting under this requirement. There is no separate flat list. | #### Document row documents[] · unassigned\_documents[] | Field | Type | Description | | --- | --- | --- | | document\_id, part\_key | string | The unit's identity. `part_key` is `""` for a whole file; a bundle the pipeline split has one row per part, materialized when processing completes. The pair joins to the [workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/)'s `checks`. | | state | string | `processed`, `in_flight`, `failed`, or `invalid` (failed the requirement's validity rules; the row stays listed). | | name | string | The filename, repeated on every part of a split bundle. | | category | string | The document type the unit is filed as, in the `accepted_categories` vocabulary. `null` until classified. | | pages | array | The unit's page numbers in the file, 1-based, as the workflow run numbers them. `null` until the file is paged. | | run\_id | string | The workflow run that produced the document, joining to `GET /workflow-runs/{run_id}` for the full extraction and document-level check verdicts. `null` when unknown. | | owners | array | `[{"party_ref", "source"}]`, empty for a dossier-level document. `source` is `declared` (your `party_refs` on the upload), `manual` (a reviewer's choice, which outranks a declaration) or `inferred` (Holofin matched the document's content to a participant). A joint document lists several owners. | | shared | boolean | Present when a shareable requirement counts the unit without consuming it; the unit also appears under the requirement that seats it. Omitted otherwise. | | duplicate\_of | string | The document this one was deduplicated against. Omitted otherwise. | | reason | string | `unassigned_documents` only. `not_yet_classified` (no category yet, usually still processing), `category_not_accepted` (no requirement accepts its category), `awaiting_attribution` (a participant-owned requirement accepts its category, but the dossier holds several participants and nothing says whose document it is: send `party_refs` on the [workflow run](https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run); never reported on a single-participant dossier) or `requirement_full` (capacity reached, wrong participant, or failed extraction; a document belonging to one participant reads this against another participant's requirement, even one still `missing`). | #### Rule rules[] Every key is always present. | Field | Type | Description | | --- | --- | --- | | code | string | Stable identifier derived from the rule's text; track a check by it, not by matching the text. Editing the rule changes its code. | | label | string | The configured name, as shown in the Holofin app. `null` for a rule written without one. | | kind | string | `validity` (judges one requirement's documents) or `consistency` (compares values across documents and declared data). Conditional-requirement activation is not a rule entry; it sits on the requirement's `activation`. | | severity | string | `blocking` or `warning`, as configured. A blocking failure holds the dossier (`coherence: "ko"`, `case_status: "incomplete"`); a warning blocks nothing. | | status | string | `ok`, `ko` or `skipped`. A failing warning reads `ko` with `severity: "warning"`. `skipped` means the check never ran (nothing yet in the requirements it reads) and is excluded from `coherence`. | | reason | string | `field_missing` on a `ko` whose referenced value is absent; on a skip, `no_instances`, `slot_inactive`, `no_applicable_parties` or `aggregate_empty`. `null` otherwise. | | depends\_on | array | What the check reads, each as `{"type", "key", "party_ref", "state"}`. `type` is `requirement`, `participant` or `dossier`. `party_ref` names the person for a participant-scoped entry, `null` at dossier level; a check reading a participant is evaluated once per person, under the same `code`. `state` is `ready`, `pending` (a document still processing), `missing` (no document counted, or nobody fills the participant) or `inactive` (a conditional requirement not activated). A `validity` check lists the requirement it judged first. | | documents | array | For a `validity` check, the documents it rejected (the ones reading `state: "invalid"` under their requirement). Empty otherwise. | | values | array | The observed values the verdict was decided on: `field`, `value` and, when read from a document, `document_id`. An aggregate carries its function under `aggregate`; an array field is reported by length (`list: true`); a value that could not be read carries `missing: true`. Present on passing checks too. | | provisional | boolean | `true` on a `ko` that only waits for a registry lookup; the dossier re-evaluates by itself when it lands. | | rule, description | string | The rule's text, and what the check verifies in plain language. Both `null` unless rule details are enabled for your organization. | [Dossier activity](#activities) ------------------------------- `GET /api/v1.1/cases/{case_id}/activities` returns the dossier's change trail: every recorded change, newest first, including what a reviewer changed after you sent it. The trail is not embedded in the dossier body. ### Read the trail ``` curl "https://app.holofin.ai/api/v1.1/cases/case_9f3k2m81xq7c/activities?since=2026-09-01T00:00:00Z" \ -H "X-API-Key: $HOLOFIN_API_KEY" ``` ``` { "case_id": "case_9f3k2m81xq7c", "activities": [ {"id": "f3887775-2a65-4f60-a8df-d3388fc3148e", "at": "2026-09-01T20:33:33Z", "type": "extraction.corrected", "actor": {"kind": "user", "name": "Charles Prime"}, "participant": {"key": "emprunteur", "party_ref": "REF-MARIE"}, "document": {"document_id": "doc_8fRsLdH6wB3nJvQa", "part_key": "", "name": "releve_juillet.pdf", "run_id": "wr_6bN4pDs8RvK1mTy3", "page": 1}, "summary": "Field \"titulaire\" corrected", "changes": [{"field": "titulaire", "before": "DUPONT M.", "after": "DUPONT Marie"}], "note": null, "operation": "update"}, {"id": "0c2d7e4a-9b1f-4c63-8e5a-2f7d1b9c4e10", "at": "2026-09-01T20:31:05Z", "type": "owner.changed", "actor": {"kind": "system", "name": null}, "participant": {"key": "emprunteur", "party_ref": "REF-MARIE"}, "document": {"document_id": "doc_3wQhBnT7yL2xMcVe", "part_key": "segment_2", "name": "liasse.pdf", "run_id": "wr_7mV3cRt6NbD2kZw5"}, "summary": "Owner attributed", "changes": [{"field": "owners", "before": [], "after": [{"party_ref": "REF-MARIE", "source": "inferred"}]}], "note": "Matched on the document's identity fields."}, {"id": "6a1e0b3d-4f52-4c8b-9d7e-1b2c3d4e5f60", "at": "2026-09-01T20:30:58Z", "type": "status.changed", "actor": {"kind": "system", "name": null}, "participant": null, "document": null, "summary": "Dossier went from pending_processing to incomplete", "changes": [{"field": "case_status", "before": "pending_processing", "after": "incomplete"}], "note": "aggregator_terminal"}, {"id": "9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "at": "2026-09-01T20:29:41Z", "type": "field.changed", "actor": {"kind": "api", "name": null}, "participant": {"key": "emprunteur", "party_ref": "REF-MARIE"}, "document": null, "summary": "2 fields changed", "changes": [{"field": "firstname", "before": null, "after": "Marie"}, {"field": "lastname", "before": null, "after": "DUPONT"}], "note": null} ], "pagination": {"page_size": 100, "has_next": false} } ``` #### Activity entry | Field | Type | Description | | --- | --- | --- | | id | string | Stable per entry; deduplicate on it between polls. | | at | string | ISO 8601 UTC. The ordering key. | | type | string | The kind of change, `object.verb`. See the table below. | | actor | object | `{"kind", "name"}`. `kind` is `user` (a person in the Holofin app, by display name, never an email), `api` (your integration; no person is named) or `system` (the pipeline). `null` when nothing recorded who. | | participant | object | `{"key", "party_ref"}` when the change is about one person; `null` for a dossier-level change or a jointly owned document. | | document | object | `{"document_id", "part_key", "name", "run_id"}` when the change is on a document, plus `page` on an extraction correction; `null` otherwise. Same identity as the [document rows](#body-document) of the dossier body. | | summary | string | An English sentence for a log line. May be reworded. | | changes | array | `{"field", "before", "after"}` entries, empty when there is nothing to diff. `before` is the value just before this change: on an extraction, the previous correction's value, not the pipeline's original reading. On `owner.changed` the values are owner lists in the body's `owners` shape; on `status.changed` and `decision.*` they are the status words the body uses. | | note | string | The comment's text, the decision's reason, the attribution's justification or the evaluation trigger; `null` otherwise. | | operation | string | `extraction.corrected` only: `update`, `add`, `delete` or `restore`. | #### Activity types | Type | Description | | --- | --- | | field.changed | A declared value set or edited, on the dossier or on a participant. `actor.kind: "user"` means a reviewer changed what you sent. | | party.created | A participant added. | | document.added | A document added. | | extraction.corrected | A reviewer edited a value read from a document. | | owner.changed | A document part's owners: attributed by the pipeline, overridden by a reviewer, reset or removed. | | decision.taken | A reviewer took a decision. | | decision.reopened | A decision voided. When a re-evaluation does it, the actor is the pipeline and `note` says what moved. | | comment.added | A note on the dossier or on a document; `note` holds the text. | | status.changed | A `case_status` transition, with the trigger in `note` (`link_added`, `extraction_revalidated`, `config_saved`…). | A document unlinked or a participant deleted is not recorded as such; only the `status.changed` entry of its re-evaluation appears. ### Query parameters | Parameter | Type | Description | | --- | --- | --- | | type | string | Optional. Comma-separated activity types to keep, e.g. `field.changed,extraction.corrected`. An unknown type is refused with `INVALID_FILTER`. | | since | string | Optional. ISO 8601 datetime. Keeps entries stamped at or after that instant (inclusive), so a poller re-sends the newest `at` it saw and deduplicates on `id`. | | limit | integer | Optional. Page size, 1 to 100. Defaults to 100; larger values are clamped. | | next\_token | string | Optional. The `pagination.next_token` of the previous response, to fetch the next page. | Pagination is the same cursor block as the [dossier listing](#list): `page_size`, `has_next`, and `next_token` when there is a next page. Send it back as `next_token` with the same filters until `has_next` is `false`. A token that does not decode is refused with `INVALID_FILTER`; an unknown query parameter with `UNKNOWN_PARAMETER`. [List dossiers](#list) ---------------------- `GET /api/v1.1/cases` returns one row per dossier of your organization, with no terminal-state restriction: a dossier still being assembled is listed like any other. Use it to reconcile your own records and to find what changed since your last pass. Rows carry the status columns only. Requirements, documents and rule verdicts come from [`GET /cases/{case_id}`](#read). A dossier with no `case_id` is not listed. ### List dossiers ``` curl "https://app.holofin.ai/api/v1.1/cases?updated_after=2026-08-20T00:00:00Z" \ -H "X-API-Key: $HOLOFIN_API_KEY" ``` ``` { "cases": [ {"case_id": "L-2026-0417", "case_status": "complete", "completion": true, "coherence": "ok", "decision": "validated", "created_at": "2026-08-21T09:12:44.318921Z", "evaluated_at": "2026-08-22T16:03:11.207845Z"}, {"case_id": "case_9f3k2m81xq7c", "case_status": "incomplete", "completion": false, "coherence": "ko", "decision": "in_review", "created_at": "2026-08-20T14:51:02.664310Z", "evaluated_at": "2026-08-20T14:51:02.981204Z"} ], "pagination": { "page_size": 100, "has_next": true, "next_token": "MjAyNi0wOC0yMFQxNDo1MTowMi42NjQzMTArMDA6MDB8YzFkNGY2ODAtOWEyMy00ZTdmLThiMzUtMmY0YTZkOTBjMTdl" } } ``` #### Listing row | Field | Type | Description | | --- | --- | --- | | case\_id | string | The dossier's reference, the one you send to `GET /cases/{case_id}` and to `/workflow-runs/`. | | case\_status, coherence, decision | string | The same values as the [dossier body](#read). `coherence` is `null` when there is no verdict. | | completion | boolean | `true` exactly when `case_status` is `complete`. | | created\_at | string | ISO 8601 UTC. The ordering key. | | evaluated\_at | string | When the last evaluation ran; `null` for a dossier never evaluated. | Filters combine. An unsupported query parameter is refused (`UNKNOWN_PARAMETER`). | Parameter | Type | Description | | --- | --- | --- | | case\_status | string | Optional. One of `no_config`, `incomplete`, `pending_processing`, `pending_review`, `rules_failed`, `complete`. Case-insensitive. | | decision | string | Optional. One of `none`, `in_review`, `validated`, `rejected`. Case-insensitive. | | coherence | string | Optional. One of `ok`, `ko`, `warning`, `review`, `none`. Case-insensitive. `review` is reserved and currently matches nothing. `none` selects the dossiers whose row answers `"coherence": null`. | | updated\_after | string | Optional. ISO 8601 datetime. Only dossiers last modified at or after it. Filters on the last modification, not `created_at`, so a dossier created before your last pass whose status has moved since is included. | | updated\_before | string | Optional. ISO 8601 datetime. Only dossiers last modified at or before it. Both bounds are inclusive. | | limit | integer | Optional. Rows per page. Default 100, also the maximum; a value outside 1 to 100 is clamped. A non-integer is refused. | | next\_token | string | Optional. The `pagination.next_token` of the previous response, to fetch the next page. | Pagination is cursor-based. `pagination` carries `page_size` (after clamping), `has_next`, and `next_token` when there is a next page. Send it back as `next_token` with the same filters until `has_next` is `false`. The token is opaque. Rows come newest first by creation, so page boundaries stay stable while dossiers are updated. A token that does not decode is refused with `INVALID_FILTER`, and so is a token issued by the workflow runs listing. This is stricter than the workflow runs listing, which starts over from the first page on a broken token. [Errors](#errors) ----------------- On `POST /cases` everything is validated before anything is written; a rejected call never leaves a half-populated dossier behind. | Code | Status | Meaning | | --- | --- | --- | | INVALID\_CASE\_ID | 400 | A `case_id` was sent but is not a non-empty string of at most 255 characters. Omit it to have one generated. | | MISSING\_CONFIG\_ID | 400 | The call creates the dossier and carries no `config_id`. List your configurations with [`GET /case-configs/`](https://app.holofin.ai/docs/api/v1.1/case-configs/). | | UNKNOWN\_CONFIG | 400 | The `config_id` names no configuration of your organization (a missing configuration, a non-UUID id and another organization's configuration answer the same way). | | CONFIG\_CONFLICT | 409 | The `config_id` names a different configuration than the dossier already runs under. Create another dossier to use another configuration. | | UNSUPPORTED\_FIELD | 400 | A top-level `party_ref`, `role`, `party_refs` or `party_fields`. Send participants inside `case_fields`. | | UNKNOWN\_INPUT\_FIELD | 400 | A key is not declared by the case configuration. The message lists the declared keys. | | INVALID\_INPUT\_FIELD | 400 | A value does not match its declared type: a non-numeric amount, a date that is not `YYYY-MM-DD`, a value outside an enum's list, a SIREN failing its checksum, or a list under a participant key capped at one. | | FIELDS\_WITHOUT\_PARTY | 400 | One key holds several participants and one of them carries no `party_ref`. | | INVALID\_PARTY\_REF | 400 | A `party_ref` is malformed, or the same reference appears twice in one call. | | INVALID\_PARTY\_TYPE | 400 | A `party_type` outside `PM`, `PP`, `PPNS`, `BE`. | | UNKNOWN\_ROLE | 400 | A participant key is not defined by the case configuration. The message lists the allowed roles. | | UNKNOWN\_PARAMETER | 400 | On the [listing](#list) and [activities](#activities), a query parameter the endpoint does not support. The message names it and lists what is supported. | | INVALID\_FILTER | 400 | On the [listing](#list) and [activities](#activities), a supported parameter with an unusable value: a status outside its list, a datetime that is not ISO 8601, a non-integer `limit`, an unknown activity `type`, or a `next_token` that is not a valid cursor. | | APPLICATIONS\_DISABLED | 400 | The Dossiers feature is not enabled for your organization. | | NOT\_FOUND | 404 | No dossier with that `case_id` in your organization. | [Back to Overview](https://app.holofin.ai/docs/api/v1.1/) --- ### Push dossier data - POST URL: https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update (A section of the page it links to, included above) ### Read a dossier - GET URL: https://app.holofin.ai/docs/api/v1.1/cases/#read (A section of the page it links to, included above) ### Dossier activity - GET URL: https://app.holofin.ai/docs/api/v1.1/cases/#activities (A section of the page it links to, included above) ### List dossiers - GET URL: https://app.holofin.ai/docs/api/v1.1/cases/#list (A section of the page it links to, included above) ### Case configurations URL: https://app.holofin.ai/docs/api/v1.1/case-configs/ Case configurations =================== List your organization's case configurations and read what each one expects: the fields it collects, the documents it requires and who provides them, its participants and checks, and the JSON Schema of the [dossier](https://app.holofin.ai/docs/api/v1.1/cases/) payload. Use it to build your upload flow before any dossier exists. [Overview](#overview) --------------------- Both endpoints are read-only and free. The catalogue is the machine-readable version of the *Show input schema* modal on the dossier configuration screen, so your integration reads what a colleague sees there. The detail is the **template of the dossier**: it uses the same nesting as [`GET /cases/{case_id}`](https://app.holofin.ai/docs/api/v1.1/cases/#read) does, with the same keys, and each row is the dossier's row minus its current state. The configuration's **checks** are listed under `rules` with the same stable `code` their verdicts carry on the dossier. Neither endpoint takes query parameters. An unknown parameter is **refused** (`UNKNOWN_PARAMETER`) rather than ignored. [List case configurations](#list) --------------------------------- `GET /api/v1.1/case-configs` returns every configuration of your organization, ordered by name. `config_id` is the one thing you need from this call: send it as `config_id` on [`POST /cases`](https://app.holofin.ai/docs/api/v1.1/cases/#create-or-update) to create a dossier under that configuration. ### List case configurations ``` curl https://app.holofin.ai/api/v1.1/case-configs \ -H "X-API-Key: $HOLOFIN_API_KEY" ``` ``` { "configs": [ {"config_id": "0b7e2d9c-6a41-4f3b-8e5d-1c9a7f2b6d03", "name": "Bail habitation"}, {"config_id": "e5b1c9a0-4f2d-4b8e-9c3a-7d6f1e0a8b42", "name": "Crédit pro"} ] } ``` [Read one configuration](#detail) --------------------------------- `GET /api/v1.1/case-configs/{config_id}` returns the configuration's full contract in the shape of the dossier it produces: the `fields` it collects, the `requirements` it expects at dossier level, the `participants` it declares with the fields and requirements each of them carries, the `rules` it checks, and the JSON Schema of the `POST /api/v1.1/cases` body. Every key of a row is always present, `null` or `false` when the configuration does not set it. ``` curl https://app.holofin.ai/api/v1.1/case-configs/e5b1c9a0-4f2d-4b8e-9c3a-7d6f1e0a8b42 \ -H "X-API-Key: $HOLOFIN_API_KEY" ``` The example below is read with rule details enabled; without them `required_if`, `activation.condition`, `rule` and `description` answer `null`. ``` { "config_id": "e5b1c9a0-4f2d-4b8e-9c3a-7d6f1e0a8b42", "name": "Crédit pro", "fields": { "montant": {"label": "Montant", "type": "number", "required": true, "conditional": false, "required_if": null, "choices": null}, "produit": {"label": "Produit", "type": "enum", "required": false, "conditional": false, "required_if": null, "choices": ["pret_pro", "credit_bail"]}, "apport": {"label": "Apport", "type": "number", "required": false, "conditional": true, "required_if": "@dossier.montant > 100000", "choices": null} }, "requirements": [ {"key": "releve_bancaire", "label": "Bank statement", "description": null, "accepted_categories": ["relevé_bancaire"], "min": 2, "max": null, "conditional": false, "activation": null, "shareable": false, "presence_only": false, "group": null}, {"key": "kbis", "label": "KBIS", "description": "Registration extract of the financed company", "accepted_categories": ["kbis"], "min": 1, "max": null, "conditional": true, "activation": {"condition": "@emprunteur.party_type == 'PM'"}, "shareable": false, "presence_only": false, "group": null} ], "participants": { "emprunteur": { "label": "Emprunteur", "min": 1, "max": 2, "fields": { "firstname": {"label": "First name", "type": "text", "required": true, "conditional": false, "required_if": null, "choices": null}, "siren": {"label": "SIREN", "type": "siren", "required": false, "conditional": false, "required_if": null, "choices": null} }, "requirements": [ {"key": "justificatif_identite", "label": "Pièce d'identité", "description": "Identity proof of the borrower", "accepted_categories": ["pièce_identité"], "min": 1, "max": 2, "conditional": false, "activation": null, "shareable": true, "presence_only": false, "group": null} ] } }, "rules": [ {"code": "case_rule_9d81b2c7e4", "label": "Relevé de moins de 3 mois", "kind": "validity", "severity": "blocking", "depends_on": [{"type": "requirement", "key": "releve_bancaire"}], "agent_review": false, "rule": "@releve_date > TODAY - 3 MONTH", "description": null}, {"code": "case_rule_a3f19c04d2", "label": "Capital social suffisant", "kind": "consistency", "severity": "blocking", "depends_on": [{"type": "requirement", "key": "kbis"}, {"type": "dossier", "key": "dossier"}], "agent_review": true, "rule": "ASSERT @case.documents.kbis.capital >= @case.montant ON KO REVIEW WITH AGENT \"…\"", "description": "Checks that the company's share capital covers the amount requested."}, {"code": "case_rule_5c40aa91f8", "label": "Titulaire du relevé", "kind": "consistency", "severity": "warning", "depends_on": [{"type": "requirement", "key": "releve_bancaire"}, {"type": "participant", "key": "emprunteur"}], "agent_review": false, "rule": "WARN IF @case.emprunteur.documents.releve_bancaire.titulaire != @case.emprunteur.lastname", "description": null} ], "payload_schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Crédit pro", "type": "object", "additionalProperties": true, "properties": { "case_id": { "type": "string", "description": "Your dossier reference. Omit it and Holofin generates one." }, "case_fields": { "type": "object", "additionalProperties": false, "properties": { "montant": {"type": ["number", "null"], "description": "Montant"}, "produit": {"enum": ["pret_pro", "credit_bail", null], "description": "Produit"}, "apport": {"type": ["number", "null"], "description": "Apport — conditional"}, "emprunteur": { "anyOf": [{"$ref": "#/$defs/emprunteur"}, {"type": "array", "items": {"$ref": "#/$defs/emprunteur"}}], "description": "Between 1 and 2 participants.", "x-participant-cardinality": {"min": 1, "max": 2} } }, "x-required-for-completeness": ["montant", "apport"] } }, "$defs": { "emprunteur": { "title": "Emprunteur", "type": "object", "additionalProperties": false, "properties": { "party_ref": {"type": "string", "description": "Optional: your own reference for this participant, defaults to the field key"}, "party_type": {"enum": ["PP", "PPNS", "PM", "BE", null], "description": "Optional: the participant's legal nature. Rules read it as @.party_type"}, "firstname": {"type": ["string", "null"], "description": "First name"}, "siren": {"type": ["string", "null"], "pattern": "^\\d{9}$", "description": "SIREN"} }, "x-required-for-completeness": ["firstname"] } } } } ``` Read it as the **template** of `GET /api/v1.1/cases/{case_id}`. On the dossier, under the same keys: * a field entry becomes `{value, state, required}`; * a requirement row gains its `state`, counts and `documents`; * a participant wrapper gains its `parties`, each carrying an instance of the `fields` and `requirements` listed here; * a rule gains its verdict under the same `code`. Nothing is listed twice: a requirement provided by a participant appears under that participant and nowhere else. ### Fields Declared input fields, keyed by the key you send: `fields` at the root holds the direct keys of `case_fields`, and each participant's `fields` holds the keys of that participant's object. Same keys, same nesting as the dossier's `fields`. | Key | Type | Description | | --- | --- | --- | | label | string | Display label. | | type | string | The declared type the value is validated against: `text`, `number`, `boolean`, `date`, `enum`, `siren`, `address` or `iso_country`. | | required | boolean | Whether the dossier always needs this value to be complete. It never means one call must carry it: values merge across calls. On the dossier, `required` is this flag resolved as of the last evaluation, conditions included. | | conditional / required\_if | boolean / string | `conditional` says the field becomes required when a configured condition holds; `required_if` is that condition as written, `null` on a plain field and `null` unless your organization has rule details enabled. | | choices | array | The allowed values of an `enum` field, `null` for every other type. Sending anything else is a 400 (`INVALID_INPUT_FIELD`). | ### Requirements One row per expected document, listed where its instances will appear on the dossier: at the root for a dossier-level requirement, under the participant that provides it otherwise. The row is the dossier's requirement row without its state: `key`, `label`, `accepted_categories`, `min`, `max`, `conditional` and `activation` read the same on both. | Key | Type | Description | | --- | --- | --- | | key | string | The requirement's identifier, the same one the dossier response uses. | | label | string | Display label. When the configuration authors none, it falls back to the accepted categories' human names. | | description | string | The configuration's own explanation of why the document is required; `null` when none is authored. | | accepted\_categories | array | The document categories that satisfy the requirement, the vocabulary a document row's `category` answers in. | | min / max | integer | How many documents are required, and the cap; `max` is `null` when uncapped. | | conditional / activation | boolean / object | A plain requirement reads `conditional: false` and `activation: null`. A conditional one applies only when a configured condition holds and carries `activation: {"condition"}`, the condition as written (several lines mean any one of them activates); `condition` is `null` unless your organization has rule details enabled. The dossier's row adds `active`, whether the condition currently holds. | | shareable | boolean | A document may satisfy this requirement even when already counted under another; on the dossier its row then appears in both places, `shared: true` on the copy that consumes nothing. | | presence\_only | boolean | Documents count once classified, whatever their extraction state. | | group | string | The name several requirements share when a rule reads them together, `null` otherwise. It is what a rule's `depends_on` may name instead of the requirement's own key. | ### Participants Keyed by participant key, the key of `case_fields` that carries this participant's values on `POST /cases` and the key the dossier's `participants` uses. Each wrapper declares a **type of participant and how many of it** the dossier expects, never one named person: «emprunteur» may be one or two people. The count is checked for completeness, not when the request is validated: a dossier holding fewer stays `INCOMPLETE` until the rest arrive. | Key | Type | Description | | --- | --- | --- | | label | string | Display label. | | min / max | integer | How many participants of this type the dossier expects. `max` is `null` when uncapped. The dossier's wrapper adds `provided` and its `state`. | | fields | object | The fields each participant of this type carries, same entry shape as the root `fields`. On the dossier they appear under each party in `parties[]`. | | requirements | array | The documents each participant of this type provides, same row shape as the root `requirements`. The dossier holds one instance per party. | ### Rules One flat list of the configuration's checks, wherever they attach: the same rows the dossier's `rules` answers, minus the verdict. Join a verdict to its definition on `code`: it is derived from the rule itself and stays the same across dossiers and evaluations, and changes only when the rule is edited. | Key | Type | Description | | --- | --- | --- | | code | string | The check's stable identifier, the one its verdict carries on every dossier under this configuration. | | label | string | The name the configuration gives the check, the one shown in the Holofin app; `null` for a rule written without one. | | kind | string | `validity` (judges one requirement's documents; a document failing it reads `state: "invalid"` under that requirement) or `consistency` (compares values across documents and declared data), the two sections of the configuration. A conditional requirement's activation is not a rule entry: it lives on the requirement's own `activation`. | | severity | string | `blocking` or `warning`. A blocking failure holds the dossier; a warning is reported and blocks nothing. | | depends\_on | array | What the check reads, each as `{"type": "requirement" | "participant" | "dossier", "key"}`. A validity check names the requirement it judges first. A check reading a participant runs once per person filling it on the dossier. The dossier's entries add `party_ref` and `state`. | | agent\_review | boolean | Whether a failure is adjudicated by Holofin's review agent before it stands, so a `ko` on the dossier may be overturned with a rationale. | | rule / description | string | The rule's text, and what the check verifies in plain language on the rules whose configuration asks for one. Both `null` unless your organization has rule details enabled. | ### Payload schema `payload_schema` is a JSON Schema (draft 2020-12) for the `POST /api/v1.1/cases` body, the same document the configuration page's *Show input schema* modal prints. Feed it to your own validator or a form generator. The sections above and the schema are built from the same declarations, so they cannot disagree. The schema **never enforces counts**: no required field keys, no `minItems` or `maxItems`, because values merge across calls and a legal partial call must not fail your validator. The completeness contract rides in `x-required-for-completeness`, and participant bounds in `x-participant-cardinality`. Every field type also accepts `null`, which clears the key. [Errors](#errors) ----------------- | Code | Status | Meaning | | --- | --- | --- | | APPLICATIONS\_DISABLED | 400 | The Dossiers feature is not enabled for your organization. Same code and status the [dossier endpoints](https://app.holofin.ai/docs/api/v1.1/cases/#errors) answer. | | UNKNOWN\_PARAMETER | 400 | A query parameter was sent. These endpoints take none, and refuse rather than ignore. | | NOT\_FOUND | 404 | No case configuration with that `config_id` in your organization. A configuration belonging to another organization and one that does not exist answer identically, and a malformed id is the same plain 404. | [Dossiers API](https://app.holofin.ai/docs/api/v1.1/cases/)[Workflow runs](https://app.holofin.ai/docs/api/v1.1/workflow-runs/)[Back to Overview](https://app.holofin.ai/docs/api/v1.1/) --- ### List case configurations - GET URL: https://app.holofin.ai/docs/api/v1.1/case-configs/#list (A section of the page it links to, included above) ### Read one configuration - GET URL: https://app.holofin.ai/docs/api/v1.1/case-configs/#detail (A section of the page it links to, included above) ## Workflow Runs ### Workflow runs URL: https://app.holofin.ai/docs/api/v1.1/workflow-runs/ Workflow runs ============= v1.1 Run a workflow on a document, follow it, read its results and PDFs, and cancel it. [Create a workflow run](#create-workflow-run) --------------------------------------------- POST`/api/v1.1/workflow-runs/` Sends one document through a workflow. Returns at once with the run in `pending`; follow it with [webhooks](https://app.holofin.ai/docs/api/v1.1/webhooks/) or by polling the run. A call refused with a 4xx creates no run and uses no credits. ### Body * `workflow_id`stringrequired The workflow to run. * `document_b64`string The file, base64-encoded: a PDF, an office document (`.xlsx`, `.xls`, `.docx`, `.pptx`) or one image (PNG, JPG, TIFF). Images become a PDF. Send this or `document_pages_b64`. * `document_pages_b64`array of string Base64 images merged, in order, into one PDF. * `document_name`string The extension is set from the file's real type. Default `"document"`. * `metadata`object Your own keys, returned on the run and in every webhook. * `webhook_url`string Send this run's webhook events to this URL only, instead of your configured endpoints. Signed with your endpoints' secret, or unsigned (with a warning) when you have none. * `case_id`string The dossier this document joins. Create it first with [`POST /cases/`](https://app.holofin.ai/docs/api/v1.1/cases/); a run never opens one. * `party_refs`array of string The participants the document belongs to, by `party_ref`. Several declare a jointly held document. Needs `case_id`. See [Sending documents](https://app.holofin.ai/docs/api/v1.1/cases/#documents). ### Errors * 400`MISSING_WORKFLOW_ID``workflow_id` is missing. * 400`MISSING_DOCUMENT`Neither `document_b64` nor `document_pages_b64` was sent. * 400`CONFLICTING_INPUT`Both `document_b64` and `document_pages_b64` were sent. * 400`INVALID_DOCUMENT`The content is not valid base64, or not a PDF, office document or image Holofin reads. * 400`PAGE_LIMIT_EXCEEDED`The document has more than 200 pages. * 400`INVALID_WEBHOOK_URL``webhook_url` is malformed, not `http(s)`, or resolves to a private address. * 400`UNSUPPORTED_FIELD`The body carries a dossier key (`case_fields`, `party_fields`, `config_id`, `party_ref`, `role`, `party_type`). Send it to `POST /cases/`. * 400`APPLICATIONS_DISABLED``case_id` was sent but Dossiers are not enabled. * 400`INVALID_CASE_ID``case_id` is empty or longer than 255 characters. * 400`UNKNOWN_CASE_ID``case_id` names no dossier. Create it with `POST /cases/` first. * 400`PARTY_WITHOUT_CASE``party_refs` was sent without `case_id`. * 400`INVALID_PARTY_REF`A participant reference is not a distinct, non-empty string of at most 255 characters. * 400`UNKNOWN_PARTY_REF`A reference names no participant of the dossier; the message lists them. * 402`INSUFFICIENT_CREDITS`No credits remaining. * 402`NO_ACTIVE_SUBSCRIPTION`The organization has no active plan. * 402`PLAN_NOT_PAID`The plan is not active yet. * 404`WORKFLOW_NOT_FOUND`No workflow with this ID in your organization. * 429`SANDBOX_LIMIT_EXCEEDED`The document needs more sandbox credits than remain. Backing off does not help. To file the document under a dossier, add its `case_id`, and `party_refs` when the dossier holds several participants: ``` { "workflow_id": "wf_abc123xyz", "document_b64": "JVBERi0xLjQKJeLjz9MKNCAwIG9i...", "case_id": "L-2026-0417", "party_refs": ["REF-MARIE"] } ``` Dossier field values go to [`POST /cases/`](https://app.holofin.ai/docs/api/v1.1/cases/), not here. ``` curl -X POST "https://app.holofin.ai/api/v1.1/workflow-runs/" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_abc123xyz", "document_b64": "JVBERi0xLjQKJeLjz9MKNCAwIG9i...", "document_name": "invoice-2024-001.pdf", "metadata": { "client_reference": "invoice-2024-001" } }' ``` 202 · Accepted ``` { "run_id": "wr_abc123xyz789", "workflow_id": "wf_abc123xyz", "workflow_name": "Invoice Processing", "document_id": "doc_def456uvw", "extraction_id": null, "document_name": "invoice-2024-001.pdf", "status": "pending", "created_at": "2024-01-15T10:30:00Z", "metadata": { "client_reference": "invoice-2024-001" } } ``` [Get a workflow run](#get-workflow-run) --------------------------------------- GET`/api/v1.1/workflow-runs/{run_id}` The run's status and results. Results fill in while it runs: `extraction_result` gains a part as each extractor finishes. ### Path parameters * `run_id`stringrequired The run's ID, from the create call. ### Response * `run_id`string The run's ID. * `workflow_id`string The workflow that ran. * `workflow_name`string The workflow's display name. * `document_id`string The uploaded file. * `document_name`string * `extraction_id`string | null The extraction's ID (`wea_...`), null until the extraction node has run. The same value as `extraction_id` in the Partner SSO token and the extraction JSON export. * `status`string `pending` until it starts, `running`, `paused` at a human review node, then `completed`, `failed` or `cancelled`. * `created_at`string * `metadata`object The `metadata` sent on creation, returned as is. * `document_pdf_url`string The [run document PDF](#get-workflow-run-pdf) endpoint. Needs the `X-API-Key` header. * `progress`object `current_step`, `total_steps` and `percentage`. * `started_at`string | null * `completed_at`string | null * `duration_ms`integer | null * `classification_result`object | null `class`, `confidence` and `reasoning`. Null without a classifier node. * `segmentation_result`object | null `sections`, one entry per document found (`section_id`, `pages`, `partition_key`), and `pages`, the same split page by page. Null without a segmenter node. * `extraction_result`object | null `document_parts`, one entry per document found, each with its `data`, `checks`, `validation_status` and a `pdf_url` (the [part PDF](#get-part-pdf)). `duplicated_pages` lists pages left out as duplicates. Fills in as each extractor finishes. See [Schema part types](https://app.holofin.ai/docs/api/v1.1/schema-part-types/). * `fraud_analysis_result`object | null `risk_level`, `risk_score`, `risk_factors` and `trust_factors`. Each factor's `sources` says where its evidence comes from: `forensic`, `template`, `content` or `seal`. `status` is `skipped`, with a `skip_reason`, when the organization's fraud filters left the document out. Null without a fraud node. See [Fraud analysis](https://app.holofin.ai/docs/api/v1.1/fraud-analysis/). * `case_id`string | null The dossier the document is filed under, if any. * `no_route_matched`boolean True when the run ended without extraction because no route handles the document's type. The run did not fail: add a route for the type, or send the document to another workflow. * `unmatched_classifications`array of string | null The types that had no route, when `no_route_matched` is true. * `error`object | null On a failed run, `code` (`NODE_EXECUTION_FAILED` or `WORKFLOW_FAILED`) and `message`, plus `node_id` and `node_type` for a node failure. ### Errors * 404`RUN_NOT_FOUND`No run with this ID in your organization. ``` curl "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789" \ -H "X-API-Key: $HOLOFIN_API_KEY" ``` 200 ``` { "run_id": "wr_abc123xyz789", "workflow_id": "wf_abc123xyz", "workflow_name": "Bank Statement Processing", "document_id": "doc_def456uvw", "extraction_id": "wea_abc123xyz789", "document_name": "statement_january_2024.pdf", "document_pdf_url": "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/pdf", "status": "completed", "progress": { "current_step": 4, "total_steps": 4, "percentage": 100 }, "started_at": "2024-01-15T10:30:01Z", "completed_at": "2024-01-15T10:30:15Z", "duration_ms": 14000, "classification_result": { "class": "bank_statement", "confidence": 0.98, "reasoning": "Bank logo, account number and a transaction list." }, "segmentation_result": { "sections": [ { "section_id": "bank_statement", "pages": [ 1, 2 ], "partition_key": "FR7630004020360001026054720" } ], "pages": [ { "page": 1, "section": "bank_statement", "partition": "FR7630004020360001026054720" }, { "page": 2, "section": "bank_statement", "partition": "FR7630004020360001026054720" } ] }, "extraction_result": { "document_parts": [ { "part_type": "bank-statement", "key": "segment_1", "label": "FR7630004020360001026054720", "validation_status": "OK", "validation_codes": [], "checks": [ { "type": "extraction", "code": "BALANCE_EQUATION", "label": "Balance equation", "status": "passed", "severity": "error" } ], "pages": [ 1, 2 ], "pdf_url": "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/parts/segment_1/pdf", "data": { "bank_name": "BNP Paribas", "currency": "EUR", "account_number": "FR7630004020360001026054720", "client_name": "ACME Corporation SARL", "start_balance": 2450.75, "end_balance": 5825.25, "start_date": "2024-01-01", "end_date": "2024-01-31", "transactions": [ { "transaction_date": "2024-01-02", "description": "VIREMENT SALAIRE", "amount": 3500.0, "page": 1, "row": 1 } ] } } ] }, "fraud_analysis_result": { "status": "completed", "risk_level": "warning", "risk_score": 42, "risk_factors": [ { "id": "suspicious_timestamp", "label": "Suspicious Timestamp", "severity": "medium", "details": "Unusual gap between creation and modification times.", "sources": [ "forensic" ] } ], "trust_factors": [] }, "case_id": null, "no_route_matched": false, "unmatched_classifications": null, "error": null, "metadata": { "client_reference": "statement-2024-001" } } ``` [Download the run's document](#get-workflow-run-pdf) ---------------------------------------------------- GET`/api/v1.1/workflow-runs/{run_id}/pdf` The file sent on creation, available as soon as the run exists. This is the run's `document_pdf_url`. Redirects (302) to a signed link valid for about 10 minutes; any client that follows redirects receives the PDF. With `format=json`, returns the link instead. ### Path parameters * `run_id`stringrequired The run's ID, from the create call. ### Query parameters * `format`string `json` returns the signed link in a JSON body instead of redirecting. ### Response * `url`string Anyone with the link can download the file until it expires. * `expires_at`string ### Errors * 404`RUN_NOT_FOUND`No run with this ID in your organization. * 404`DOCUMENT_FILE_UNAVAILABLE`The document file is no longer stored, for example after its retention period. * 415`UNSUPPORTED_DOCUMENT_TYPE`A PDF was requested for a source document that is not a PDF, such as a spreadsheet. * 500`PDF_RESOLUTION_FAILED`The run or part PDF could not be produced. The signed `url` needs no API key: anyone holding it can download the file until `expires_at`. ``` curl -L "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/pdf" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ -o document.pdf ``` 200 · With format=json ``` { "url": "https://storage.holofin.ai/documents/doc_def456uvw.pdf?signature=...", "expires_at": "2026-08-01T10:40:00Z" } ``` [Download a document part](#get-part-pdf) ----------------------------------------- GET`/api/v1.1/workflow-runs/{run_id}/parts/{part_key}/pdf` The pages of one part as a single PDF, non-contiguous pages included. Each entry of `document_parts` carries this URL as `pdf_url`. Same redirect and `format=json` behaviour as the run's document. ### Path parameters * `run_id`stringrequired The run's ID, from the create call. * `part_key`stringrequired The part's `key` in `document_parts`. ### Query parameters * `format`string `json` returns the signed link in a JSON body instead of redirecting. ### Response * `url`string Anyone with the link can download the file until it expires. * `expires_at`string ### Errors * 404`RUN_NOT_FOUND`No run with this ID in your organization. * 404`PART_NOT_FOUND`No part with this key on the run. * 404`DOCUMENT_FILE_UNAVAILABLE`The document file is no longer stored, for example after its retention period. * 409`RUN_NOT_TERMINAL`Extraction has not finished, so the parts do not exist yet. * 415`UNSUPPORTED_DOCUMENT_TYPE`A PDF was requested for a source document that is not a PDF, such as a spreadsheet. * 500`PDF_RESOLUTION_FAILED`The run or part PDF could not be produced. ``` curl -L "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/parts/segment_1/pdf" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ -o segment_1.pdf ``` 200 · With format=json ``` { "url": "https://storage.holofin.ai/documents/doc_def456uvw_segment_1.pdf?signature=...", "expires_at": "2026-08-01T10:40:00Z" } ``` [List workflow runs](#list-workflow-runs) ----------------------------------------- GET`/api/v1.1/workflow-runs/` Runs that have finished (`completed`, `failed` or `cancelled`), newest first. Items carry no extraction data: fetch a run by ID for it. ### Query parameters * `workflow_id`string Only this workflow's runs. * `status`string One of `completed`, `failed`, `cancelled`. * `case_id`string Only runs whose document is in this dossier. * `created_after`string * `created_before`string * `limit`integer Page size, clamped to 1-100. Default `100`. * `next_token`string Opaque cursor from the previous page. Omit for the first page. ### Response * `runs`array of object * `pagination`object `page_size`, `has_next`, and `next_token` while `has_next` is true. Pass `next_token` back for the next page. ### Errors * 400`INVALID_FILTER``created_after` or `created_before` is not ISO 8601. ``` curl -G "https://app.holofin.ai/api/v1.1/workflow-runs/" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ --data-urlencode "workflow_id=wf_abc123xyz" \ --data-urlencode "limit=20" ``` 200 ``` { "runs": [ { "run_id": "wr_abc123xyz789", "workflow_id": "wf_abc123xyz", "workflow_name": "Bank Statement Processing", "document_id": "doc_def456uvw", "extraction_id": "wea_abc123xyz789", "case_id": "L-2026-0417", "document_name": "statement_january_2024.pdf", "status": "completed", "created_at": "2024-01-15T10:30:00Z", "completed_at": "2024-01-15T10:30:15Z", "duration_ms": 14000, "validation_status": "OK" } ], "pagination": { "page_size": 20, "has_next": true, "next_token": "MjAyNC0wMS0xNVQxMDozMDoxNVp8d3JfYWJj" } } ``` [List a run's node executions](#list-node-runs) ----------------------------------------------- GET`/api/v1.1/workflow-runs/{run_id}/node-runs/` Each node's configuration, status and output. Extractor output is not here: read it from the run. Start and end nodes are left out. ### Path parameters * `run_id`stringrequired The run's ID, from the create call. ### Response * `run_id`string * `node_runs`array of object * `total_count`integer ### Errors * 404`RUN_NOT_FOUND`No run with this ID in your organization. ### What each node returns | Node | `config` | `results` | | --- | --- | --- | | `classifier` | `classifier_name`, `classes` (each `name`, `description`) | `class`, `confidence` (0 to 1), `reasoning` | | `segmenter` | `segmenter_name`, `custom_rules`, `prompt`, `sections` (each `section_id`, `section_description`, `partition_key`, `partition_description`) | `confidence`, `reasoning`, `sections` and `pages`, the same views as the run's `segmentation_result` | | `extractor` | `extractor_name`, `extraction_type` | Not here: read `extraction_result` on the run. | ``` curl "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/node-runs/" \ -H "X-API-Key: $HOLOFIN_API_KEY" ``` 200 ``` { "run_id": "wr_abc123xyz789", "node_runs": [ { "node_type": "classifier", "status": "completed", "started_at": "2024-01-15T10:30:01Z", "completed_at": "2024-01-15T10:30:04Z", "duration_ms": 3120, "agent_id": "ca_EACHWbXq3cRtPvGQ", "is_global_agent": false, "config": { "classifier_name": "Financial documents", "classes": [ { "name": "bank_statement", "description": "..." } ] }, "results": { "class": "bank_statement", "confidence": 0.98, "reasoning": "Bank logo, account number, transactions." } } ], "total_count": 1 } ``` [Cancel a workflow run](#cancel-workflow-run) --------------------------------------------- POST`/api/v1.1/workflow-runs/{run_id}/cancel` Stops a `pending` or `running` run before its next node starts. A node already running finishes first. ### Path parameters * `run_id`stringrequired The run's ID, from the create call. ### Response * `run_id`string * `status`string One of `cancelled`. * `cancelled_at`string | null ### Errors * 404`RUN_NOT_FOUND`No run with this ID in your organization. * 409`CANNOT_CANCEL`The run is already `completed`, `failed` or `cancelled`. Treat it as success. ``` curl -X POST "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/cancel" \ -H "X-API-Key: $HOLOFIN_API_KEY" ``` 200 ``` { "run_id": "wr_abc123xyz789", "status": "cancelled", "cancelled_at": "2024-01-15T10:30:05Z" } ``` --- ### Create a workflow run - POST URL: https://app.holofin.ai/docs/api/v1.1/workflow-runs/#create-workflow-run (A section of the page it links to, included above) ### Get a workflow run - GET URL: https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run (A section of the page it links to, included above) ### Download the run PDF - GET URL: https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-workflow-run-pdf (A section of the page it links to, included above) ### Download a part PDF - GET URL: https://app.holofin.ai/docs/api/v1.1/workflow-runs/#get-part-pdf (A section of the page it links to, included above) ### List workflow runs - GET URL: https://app.holofin.ai/docs/api/v1.1/workflow-runs/#list-workflow-runs (A section of the page it links to, included above) ### List node runs - GET URL: https://app.holofin.ai/docs/api/v1.1/workflow-runs/#list-node-runs (A section of the page it links to, included above) ### Cancel a workflow run - POST URL: https://app.holofin.ai/docs/api/v1.1/workflow-runs/#cancel-workflow-run (A section of the page it links to, included above) ## Organization ### Usage - GET URL: https://app.holofin.ai/docs/api/v1.1/usage/ Usage ===== v1.1 How many credits your organization has consumed, over the last 30 days and since the start of the month. [Get usage](#get-usage-stats) ----------------------------- GET`/api/v1.1/usage` Credits consumed over the last 30 days and since the start of the month. Extraction costs 1 credit per page, classification 1 per document, segmentation 1 per operation. Counts update as work completes. ### Response * `last_30_days`object `total`, `extraction`, `classification` and `segmentation` credits. * `since_month_start`object The same breakdown since the 1st, 00:00 UTC. * `period_start`string The 1st of the month, 00:00 UTC. * `period_end`string When the figures were computed. * `organization`string * `current_plan`object | null `type` (`SANDBOX`, `PAY_PER_PAGE`, `STARTER`, `PRO`, `ENTERPRISE`), `name` and `included_credits`. Null without an active plan. * `sandbox`object Sandbox organizations only: `credits_limit`, `credits_used`, `credits_remaining` and `usage_percentage` (0 to 100). The limit applies at once. For a sandbox organization the response also carries `sandbox`: ``` "sandbox": { "credits_limit": 3500, "credits_used": 245, "credits_remaining": 3255, "usage_percentage": 7.0 } ``` ``` curl "https://app.holofin.ai/api/v1.1/usage" \ -H "X-API-Key: $HOLOFIN_API_KEY" ``` 200 ``` { "last_30_days": { "total": 2534, "extraction": 2456, "classification": 45, "segmentation": 33 }, "since_month_start": { "total": 948, "extraction": 892, "classification": 32, "segmentation": 24 }, "period_start": "2025-10-01T00:00:00+00:00", "period_end": "2025-10-15T14:32:45.123456+00:00", "organization": "Acme Corp", "current_plan": { "type": "PRO", "name": "Pro", "included_credits": 100000 } } ``` --- ### Processing metrics - GET URL: https://app.holofin.ai/docs/api/v1.1/metrics/ Metrics ======= v1.1 Volumes and processing performance for your organization: documents and pages by type and over time, runs by outcome and channel. Built for dashboards and BI pulls. Every endpoint takes an optional window, `from` and `to`; without it, the last 30 days. Figures cover the organization that owns the key, update as work completes, and leave out the internal page-split children of a segmented bundle, so document totals are real submissions. An invalid `granularity` or `include` answers 400. Whether Holofin can take work right now is [Health](https://app.holofin.ai/docs/api/v1.1/health/). [Get all metrics](#get-metrics) ------------------------------- GET`/api/v1.1/metrics` Volumes and throughput for one window, in one call: the one to use for a scheduled BI pull. The `volumes` and `throughput` objects hold the fields of the two endpoints below. ### Query parameters * `from`string ISO 8601 date or datetime. Default: 30 days ago. * `to`string ISO 8601 date or datetime. Default: now. * `granularity`string The size of each `series` bucket. One of `day`, `week`, `month`. Default `"day"`. * `include`string `volumes`, `throughput`, or both comma-separated. Default `"volumes,throughput"`. ### Response * `organization`string * `period_start`string * `period_end`string * `granularity`string * `volumes`object As [volume metrics](#get-volumes). * `throughput`object As [throughput metrics](#get-throughput). ``` curl -G "https://app.holofin.ai/api/v1.1/metrics" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ --data-urlencode "from=2026-01-01" \ --data-urlencode "granularity=month" ``` 200 ``` { "organization": "Acme Corp", "period_start": "2026-01-01T00:00:00+00:00", "period_end": "2026-09-01T08:14:22.481930+00:00", "granularity": "month", "volumes": { "totals": { "documents": 70, "pages": 678 }, "by_type": [ { "document_category": "relevé_bancaire", "documents": 57, "pages": 536 } ], "by_type_families": [ { "family": "bank_statement", "label": "Bank statement", "documents": 58, "pages": 537 } ], "series": [ { "period": "2026-07-01", "documents": 12, "pages": 140 } ] }, "throughput": { "totals": { "processed": 64, "completed": 60, "failed": 1 }, "success_rate": 0.9836, "avg_duration_ms": 41250, "by_channel": [ { "channel": "api", "runs": 46 } ], "by_status": [ { "status": "completed", "runs": 60 } ] } } ``` [Get volume metrics](#get-volumes) ---------------------------------- GET`/api/v1.1/metrics/volumes` Documents and pages processed, by type and over time. ### Query parameters * `from`string ISO 8601 date or datetime. Default: 30 days ago. * `to`string ISO 8601 date or datetime. Default: now. * `granularity`string The size of each `series` bucket. One of `day`, `week`, `month`. Default `"day"`. ### Response * `organization`string * `period_start`string * `period_end`string * `granularity`string * `totals`object `documents` and `pages`. * `by_type`array of object Per raw classifier code (`document_category`, `documents`, `pages`). Stable keys for data pipelines. * `by_type_families`array of object Codes folded into business families (`family`, `label`, `documents`, `pages`), as on the in-product dashboard. * `series`array of object One bucket per period: `period`, `documents`, `pages`. Use `by_type` in data pipelines, its raw codes are stable; use `by_type_families` for dashboards people read. ``` curl -G "https://app.holofin.ai/api/v1.1/metrics/volumes" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ --data-urlencode "from=2026-01-01" \ --data-urlencode "granularity=month" ``` 200 ``` { "organization": "Acme Corp", "period_start": "2025-01-01T00:00:00+00:00", "period_end": "2026-07-19T14:32:45.123456+00:00", "granularity": "month", "totals": { "documents": 70, "pages": 678 }, "by_type": [ { "document_category": "relevé_bancaire", "documents": 57, "pages": 536 }, { "document_category": null, "documents": 5, "pages": 29 } ], "by_type_families": [ { "family": "bank_statement", "label": "Bank statement", "documents": 58, "pages": 537 }, { "family": "__unclassified__", "label": "Unclassified", "documents": 5, "pages": 29 } ], "series": [ { "period": "2026-07-01", "documents": 12, "pages": 140 } ] } ``` [Get throughput metrics](#get-throughput) ----------------------------------------- GET`/api/v1.1/metrics/throughput` Workflow runs by outcome and by ingestion channel, with success rate and average processing time. ### Query parameters * `from`string ISO 8601 date or datetime. Default: 30 days ago. * `to`string ISO 8601 date or datetime. Default: now. ### Response * `organization`string * `period_start`string * `period_end`string * `totals`object `processed` (every run), `completed`, `failed`. * `success_rate`number | null Completed over completed plus failed; runs still in flight are left out. Null when nothing has finished. * `avg_duration_ms`integer | null * `by_channel`array of object `channel` and `runs`. * `by_status`array of object `status` and `runs`. ``` curl -G "https://app.holofin.ai/api/v1.1/metrics/throughput" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ --data-urlencode "from=2026-01-01" ``` 200 ``` { "organization": "Acme Corp", "period_start": "2025-01-01T00:00:00+00:00", "period_end": "2026-07-19T14:32:45.123456+00:00", "totals": { "processed": 64, "completed": 60, "failed": 1 }, "success_rate": 0.9836, "avg_duration_ms": 1258816, "by_channel": [ { "channel": "webhook", "runs": 46 }, { "channel": "manual", "runs": 18 } ], "by_status": [ { "status": "completed", "runs": 60 }, { "status": "running", "runs": 3 }, { "status": "failed", "runs": 1 } ] } ``` --- ### Health - GET URL: https://app.holofin.ai/docs/api/v1.1/health/ Health ====== v1.1 One endpoint your monitoring can poll to know whether Holofin can accept and process work right now. It reports the **platform**, not your account. For what happened to documents you already sent, use [Metrics](https://app.holofin.ai/docs/api/v1.1/metrics/). [Get service health](#get-health) --------------------------------- GET`/api/v1.1/health` Whether Holofin can accept and process work right now. No API key, never rate limited. Answers **200** when `operational` or `degraded`, **503** on `outage`: alert on the status code. ### Response * `status`string One of `operational`, `degraded`, `outage`. * `services`object * `time`string `status` is the worst of the four `services`: `api` (requests and authentication), `document_intake` (accepting uploads), `processing` (classification, segmentation, extraction, analysis) and `webhooks` (event delivery). Each is `operational`, `degraded` (slower, or notifications late; nothing is lost) or `outage` (hold submissions and retry). `time` is when the state was measured; it refreshes at most every 10 seconds. ``` curl "https://app.holofin.ai/api/v1.1/health" ``` 200 · Operational or degraded ``` { "status": "operational", "services": { "api": "operational", "document_intake": "operational", "processing": "operational", "webhooks": "operational" }, "time": "2026-09-01T08:14:22.481930+00:00" } ``` Monitoring ---------- * **Poll every 30 to 60 seconds.** The state refreshes at most every 10 seconds, so polling more often buys nothing. * **Alert on the status code, not the body.** 503 is unavailable, 200 is available. Read `services` to see which capability is affected. * **Require three consecutive failures.** One failed probe is usually a network hiccup, not an incident. * **Do not gate submissions on it.** A failed submission explains itself; see [Errors](https://app.holofin.ai/docs/api/v1.1/errors/) and [Reliability and limits](https://app.holofin.ai/docs/api/v1.1/reliability/). Notes ----- * Where a contract sets an availability commitment, it is computed from the share of non-503 responses on this endpoint, excluding announced maintenance. * `/api/v1/health` and `/api/v1.1/health` return the same payload, with or without a trailing slash. * `degraded` returns 200 on purpose, so a slow period does not wake your on-call team. --- ### Sub-organizations URL: https://app.holofin.ai/docs/api/v1.1/sub-organizations/ Sub-Organizations ================= Create and manage isolated tenant organizations for each of your end clients, all from a single API key. [Overview](#overview) --------------------- Partner accounts can programmatically create **sub-organizations**: fully isolated tenants, one per end client. Each sub-organization has its own documents, workflow runs, webhooks, and usage statistics, while billing is consolidated on your main organization's plan and credits. Sub-organizations are headless: they have no users and no dashboard access. You operate them entirely through the API using your existing API key. Sub-organization management is a partner capability that must be enabled on your account. Contact your Holofin representative to activate it. Once enabled, your API keys are also exempt from the standard per-key rate limit. [Acting on Behalf of a Sub-Organization](#acting-as) ---------------------------------------------------- Every existing API endpoint (workflow runs, webhooks, usage, metrics) can target a sub-organization by adding the `X-Organization-Id` header to the request. Without the header, requests target your main organization. ``` curl -X POST "https://app.holofin.ai/api/v1.1/workflow-runs/" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ -H "X-Organization-Id: org_01h2xcejqtf2nbrexx3vqjhp41" \ -H "Content-Type: application/json" \ -d '{"workflow_id": "wf_...", "document_b64": "..."}' ``` The header accepts the sub-organization id in either form: the `org_...` identifier returned at creation, or the raw UUID. Targeting an organization that is not one of your sub-organizations (or one that has been deactivated) returns `404`. Workflow definitions are shared with your sub-organizations automatically: a run created on behalf of a sub-organization can reference any workflow defined on your main organization, in addition to the tenant's own (see [Cloned Workflows](#cloned-workflows)). The run, its documents, and its usage are recorded on the sub-organization. [Create a Sub-Organization](#create) ------------------------------------ POST`/api/v1.1/organizations` Creates a new sub-organization. The new tenant is active immediately and inherits your organization's processing configuration (fraud detection, data retention period, and related settings). ### Request Body | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | Yes | Display name of the sub-organization (1-255 characters). | | `external_ref` | string | No | Your own identifier for this client (max 255 characters). Unique among your sub-organizations and used for idempotency: re-sending a request with a known `external_ref` returns the existing sub-organization with status `200` instead of creating a duplicate. | | `clone_workflows` | boolean | No | Defaults to `true`. Copies your published workflows into the new sub-organization so it owns its own definitions from the start. Set to `false` to create an empty tenant that only runs the workflows defined on your main organization. See [Cloned Workflows](#cloned-workflows). | ### Example ``` curl -X POST "https://app.holofin.ai/api/v1.1/organizations" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Acme Insurance", "external_ref": "crm-4821"}' ``` Response `201 Created` (or `200 OK` on an idempotent replay): ``` { "id": "org_01h2xcejqtf2nbrexx3vqjhp41", "name": "Acme Insurance", "external_ref": "crm-4821", "status": "active", "created_at": "2026-07-25T09:14:02.481930+00:00" } ``` [Cloned Workflows](#cloned-workflows) ------------------------------------- By default, creating a sub-organization copies every **published** workflow from your main organization into the new tenant. The copies are independent: each sub-organization owns its own workflow ids, and editing one never affects your main organization or your other tenants, so each client can be tuned separately. The copy is complete. Any classifier, segmenter, or extractor that belongs to your organization is copied alongside the workflow and re-linked to the tenant's version, so a sub-organization never depends on a definition it does not own. Shared Holofin agents are referenced as-is. Cloned workflows keep the name and published version of the original but start with email ingestion disabled. Draft workflows are not copied. Pass `"clone_workflows": false` to skip this and create an empty tenant. Runs created on its behalf still reference the workflows defined on your main organization, as described in [Acting on Behalf of a Sub-Organization](#acting-as). ``` curl -X POST "https://app.holofin.ai/api/v1.1/organizations" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Acme Insurance", "external_ref": "crm-4821", "clone_workflows": false}' ``` Cloning never blocks provisioning. If the copy cannot be completed, the sub-organization is still created and returned as usual, and the tenant runs your main organization's workflows in the meantime. Contact support if a tenant's workflow list is unexpectedly empty. [List Sub-Organizations](#list) ------------------------------- GET`/api/v1.1/organizations` Returns all of your sub-organizations (active and inactive), oldest first. ``` { "organizations": [ { "id": "org_01h2xcejqtf2nbrexx3vqjhp41", "name": "Acme Insurance", "external_ref": "crm-4821", "status": "active", "created_at": "2026-07-25T09:14:02.481930+00:00" } ] } ``` [Get a Sub-Organization](#get) ------------------------------ GET`/api/v1.1/organizations/{id}` Fetches a single sub-organization by its `org_...` identifier or raw UUID. [Update a Sub-Organization](#update) ------------------------------------ PATCH`/api/v1.1/organizations/{id}` Updates the `name` and/or `status` of a sub-organization. Setting `status` to `"inactive"` immediately cuts API access to that tenant: subsequent requests using its `X-Organization-Id` return `404`. Re-activate by setting `status` back to `"active"`. ``` curl -X PATCH "https://app.holofin.ai/api/v1.1/organizations/org_01h2xcejqtf2nbrexx3vqjhp41" \ -H "X-API-Key: $HOLOFIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"status": "inactive"}' ``` Sub-organizations cannot be deleted through the API. Deactivate them instead; document retention and deletion follow the sub-organization's data retention settings. [Billing and Usage](#billing) ----------------------------- * Consolidated billing Sub-organizations have no plan of their own. All processing draws on your main organization's subscription and credit balance, and appears on your invoice. * Per-tenant usage tracking Usage is recorded per sub-organization. Call `/api/v1.1/usage` or `/api/v1.1/metrics/volumes` with the `X-Organization-Id` header to get per-tenant figures; without the header you get your main organization's own usage. * Webhooks Webhook endpoints are registered per organization. Register endpoints for a sub-organization via `/api/v1.1/webhooks` with the `X-Organization-Id` header. Workflow-level events are delivered to the endpoints of the organization that owns the workflow definition. [Error Codes](#errors) ---------------------- | Status | Code | Meaning | | --- | --- | --- | | `400` | `INVALID_NAME` / `INVALID_EXTERNAL_REF` / `INVALID_CLONE_WORKFLOWS` / `INVALID_STATUS` | A field in the request body failed validation. | | `403` | `NOT_ALLOWED` | Your organization does not have the sub-organization capability enabled. | | `404` | `NOT_FOUND` | The id does not match one of your sub-organizations. | | `409` | `LIMIT_REACHED` | Your sub-organization limit has been reached. Contact Holofin to raise it. | --- ## Resources ### Document Categories URL: https://app.holofin.ai/docs/api/v1.1/document-categories/ Document Categories =================== The Holofin API classifies every uploaded document into one of the categories below. This page lists each category and whether it supports extraction. Overview -------- When you submit a document for extraction, the API determines its type and returns it in the `document_category` field. Not all document categories support data extraction. The table below shows which categories have extraction support enabled. | Category | Description | Extraction | | --- | --- | --- | | BANK\_STATEMENT | Bank account statements showing transactions and balances | ✅ | | FINANCIAL\_STATEMENT | Financial statements including balance sheets and income statements | ✅ | | FISCAL\_PACKAGE | French fiscal packages (liasses fiscales) - CERFA forms 2033, 2050, 2051, 2052, 2053 | ✅ | | RIB | French bank details document (Relevé d'Identité Bancaire) | | | KBIS | French company registration document (Extrait Kbis) | | | IDENTITY\_DOCUMENT | Identity documents (passports, ID cards, driver's licenses) | | | TAX\_NOTICE | Tax notices and assessment documents (avis d'imposition) | | | TAX\_RETURN | Tax return documents (déclarations de revenus) | | | PAYSLIP | Salary slips and payroll documents (bulletins de salaire) | | | INVOICE | Commercial invoices and bills | | | OTHER | Other document types not fitting the above categories | | | UNCATEGORIZED | Document classification is in progress or could not be determined | | Special Values -------------- | Value | Meaning | | --- | --- | | null | Document has not been created or classified yet. This typically occurs in the early stages of job processing. | --- ### Schema Part Types URL: https://app.holofin.ai/docs/api/v1.1/schema-part-types/ Schema Part Types ================= When extracting documents, the API returns results in a `document_parts` array. Each part has a `part_type` field that identifies its schema structure. Overview -------- The `part_type` field is a string identifier that tells you what kind of data to expect in that document part. Use this field to determine how to parse and display the extraction results. Available Part Types -------------------- The table below shows all supported part types and their corresponding schemas. | part\_type | Description | | --- | --- | | bank-statement | Bank account statement with transactions, balances, and account details. [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#bank-statement) | | utility\_bill | Utility bill (electricity, gas, water) with consumption periods, meter readings, and delivery point details. [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#utility-bill) | | bilan-actif | Generic French balance sheet - Assets (Bilan Actif). [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#fr-bilan-actif) | | bilan-passif | Generic French balance sheet - Liabilities (Bilan Passif). [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#fr-bilan-passif) | | CERFA-2033-A | Simplified balance sheet (Bilan Simplifié) - Regime Simplifie d'Imposition. [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#cerfa-2033a) | | CERFA-2033-B | Simplified income statement (Compte de Resultat Simplifie). [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#cerfa-2033b) | | CERFA-2033-C | Fixed assets and depreciation (Immobilisations et Amortissements). [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#cerfa-2033c) | | CERFA-2033-D | Provisions, accelerated depreciation and loss carryforwards (Relevé des provisions). [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#cerfa-2033d) | | CERFA-2033-E | Added value determination for CVAE (Détermination de la valeur ajoutée produite). [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#cerfa-2033e) | | CERFA-2050 | Balance sheet - Assets (Bilan Actif) - Regime Reel Normal. [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#cerfa-2050) | | CERFA-2051 | Balance sheet - Liabilities (Bilan Passif) - Regime Reel Normal. [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#cerfa-2051) | | CERFA-2052 | Income statement (Compte de Resultat) - Regime Reel Normal. [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#cerfa-2052) | | CERFA-2053 | Income statement continued (Compte de Resultat suite) - Regime Reel Normal. [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#cerfa-2053) | | CERFA-2059-G | Subsidiaries and equity holdings (Filiales et Participations). [View schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#cerfa-2059g) | Document Part Fields -------------------- Each item in the `document_parts` array contains the following common fields: | Field | Type | Description | | --- | --- | --- | | key | string | Unique segment identifier for this document part (e.g., `segment_1`, `segment_2`). Useful to correlate parts across multi-segment documents. | | part\_type | string | Schema identifier for this document part. See the table above for available values. | | label | string | null | Human-readable label identifying this part (e.g., account number for bank statements, form name for CERFA). May be `null` for some part types. | | validation\_status | string | null | Data validation result: `OK` (all consistency checks passed), `WARNING` (soft issues detected), or `ERROR` (validation errors detected, such as balance mismatches). | | validation\_codes | array of strings | Stable upper-snake tokens describing decisions taken during validation. Always present, may be empty. Includes errors that fired (e.g. `BALANCE_MISMATCH`), warnings, and neutral notes such as the savings-account waiver (`SAVING_ACCOUNT_WO_START_BALANCE_IS_EXPECTED`) or the securities / compte-titres waiver (`SECURITIES_ACCOUNT_WO_START_BALANCE_IS_EXPECTED`). Parts whose schema declares a field typed `siren` additionally carry the company-registry outcome: `SIREN_RESOLVED` when the registry recognises the number, `SIREN_NOT_FOUND` when it does not or the printed value is not a usable SIREN / SIRET, and `SIREN_ENRICHMENT_UNAVAILABLE` when the lookup could not be performed. The last two are errors. Branch on codes; rely on `validation_status` for the rolled-up verdict. | | checks | array of objects | The full rule inventory for this part: one flat array of every check that ran: passed, failed and skipped alike. Each entry carries `type` (`extraction`, `business` or `id_verification`), `code`, `label`, `description`, `status` (`passed` / `failed` / `skipped`), `severity`, `message` and `reason`. **Extraction** checks judge whether the document was read correctly; **business** checks judge your own acceptance rules; **id\_verification** checks carry the identity-verification provider's verdict on identity documents. The `code` is the same token used in `validation_codes`, so the two join directly. A category that ran no checks contributes no entries. Organizations with rule details enabled additionally receive `formula`, `values`, `expected`, `actual` and `tolerance` per entry where available; absent keys uniformly mean the data is not available. | | pages | array of integers | Page numbers from the original document where this part appears. | | version | string | null | Form version year (e.g., "2024"). Only present for CERFA forms. | | data | object | The extracted data. Structure varies by `part_type`. See individual schema documentation for details. | Usage Example ------------- Here's how to use the `part_type` field to process document parts: ``` { "extraction_id": "wea_iMaUbZDZoLfWZcKh", "document_name": "testhabib4.pdf", "category": "BANK_STATEMENT", "extraction_date": "2025-12-17T14:22:21.249137+00:00", "review_status": "NOT_REVIEWED", "reviewed_by_user": null, "reviewed_at": null, "rejected_by_user": null, "rejected_at": null, "document_parts": [ { "key": "segment_1", "part_type": "bank-statement", "label": "Qonto - My Corp", "validation_status": "OK", "validation_codes": [], "checks": [ { "type": "extraction", "code": "BALANCE_EQUATION", "label": "Balance equation", "description": null, "status": "passed", "severity": "error", "message": null, "reason": null } ], "pages": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 ], "data": { "bank_name": "Qonto", "currency": "EUR", "account_type": "current", "account_name": "My Corp", "client_name": "My Corp SARL", "client_names": ["My Corp SARL"], "account_number": "FR7630004020360001026054720", "advisor_name": "Jean Dupont", "branch_name": "Paris Opéra", "start_balance": 2450.75, "end_balance": 5825.25, "start_date": "2024-01-01", "end_date": "2024-01-31", "transactions": [ // ... ] } }, { "key": "segment_2", "part_type": "CERFA-2033-A", "validation_status": "OK", "validation_codes": [], "pages": [1, 2], "version": "2024", "data": { // ... } }, { "key": "segment_3", "part_type": "bank-statement", "label": "Livret A", "validation_status": "OK", "validation_codes": ["SAVING_ACCOUNT_WO_START_BALANCE_IS_EXPECTED"], "checks": [ { "type": "extraction", "code": "BALANCE_EQUATION", "label": "Balance equation", "description": null, "status": "passed", "severity": "error", "message": null, "reason": null } ], "pages": [20, 21], "data": { "account_type": "savings", "end_balance": 8214.36, // start_balance was not declared on the document. Acceptable for // savings accounts; surfaced via the validation_codes entry. } } ] } ``` --- ### Supported Document Types URL: https://app.holofin.ai/docs/api/v1.1/supported-document-types/ Supported Document Types ======================== This page lists every document type Holofin supports for KYC, lending and asset-financing workflows, and whether its extraction output comes from a prebuilt schema or a custom extractor. Coverage model -------------- Document types are served through one of two mechanisms: * **Prebuilt schema**: the document has a stable, versioned JSON schema with a fixed `part_type`. The output contract is documented on the [Document Schemas](https://app.holofin.ai/docs/api/v1.1/document-schemas/) page and is identical for every customer. * **Custom extractor**: the document is processed with a customer-defined extraction schema. You define the fields you need (or start from a generated schema) and the API returns your structure. See the [Extraction](https://app.holofin.ai/docs/api/v1.1/extraction/) guide to define a schema. Every type below is production-supported: classification, segmentation, extraction and fraud signals apply to both mechanisms. Identity & KYC -------------- | Document type | Description | Delivery | | --- | --- | --- | | National ID card (CNI) | French identity card, both legacy and 2021 credit-card formats, with MRZ reading and consistency checks | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/identity/#cni) | | Passport | Holder page with ICAO MRZ reading and check-digit validation | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/identity/#passport) | | Driving licence | EU-format licence with holder, categories and validity dates | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/identity/#permis-conduire) | | Residence permit | Titre de séjour with permit type, mention and validity window | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/identity/#titre-sejour) | | Resident card | 10-year carte de résident with MRZ and validity checks | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/identity/#carte-resident) | | Residence-permit renewal attestation | Prefecture decision letter confirming a favourable renewal | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/identity/#renouvellement-titre-sejour) | Proof of Address ---------------- | Document type | Description | Delivery | | --- | --- | --- | | Utility bill (energy) | Electricity, gas and water invoices with consumption periods | [Prebuilt schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#utility-bill) | | Telecom bill | Mobile, landline and internet invoices | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/proof-of-address/#facture-telecom) | | Rent receipt | Quittance de loyer with tenant, landlord, period and amounts | Custom extractor | | Accommodation certificate | Attestation d'hébergement, including handwritten letters | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/proof-of-address/#attestation-hebergement) | | Property tax notice | Taxe foncière notice with owner, property and cadastral references | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/proof-of-address/#taxe-fonciere) | | Residence tax notice | Taxe d'habitation notice with occupant and dwelling details | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/proof-of-address/#taxe-habitation) | | Notarial deed | Acte notarié with deed type, parties and property references | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/proof-of-address/#acte-notarie) | Income & Employment ------------------- | Document type | Description | Delivery | | --- | --- | --- | | Pay slip | Bulletin de paie with gross/net pay and social contributions | Custom extractor | | Employment contract | CDI / CDD contracts, clause location across multi-page documents | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/income-employment/#contrat-travail) | | Secondment contract | Contrat de détachement with host entity, country and duration | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/income-employment/#contrat-detachement) | | Employment certificate | Attestation de travail issued by the employer | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/income-employment/#attestation-travail) | | France Travail payment statement | Unemployment allowance statement (ARE and related) | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/income-employment/#attestation-france-travail) | | Pension payment statement | Attestation de paiement de retraite from pension funds | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/income-employment/#attestation-retraite) | | Tax notice | Avis d'imposition with reference income and household details | Custom extractor | | Income tax return (2042) | Main French income declaration, plus 2042 C PRO for the self-employed | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/income-employment/#declaration-2042) | | Property income return (2044) | Rental income declaration with receipts, charges and net result | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/income-employment/#declaration-2044) | | BIC return (2031-SD) | Industrial and commercial profit declaration for sole traders | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/income-employment/#declaration-2031-bic) | Banking & Wealth ---------------- | Document type | Description | Delivery | | --- | --- | --- | | Bank statement | Account statements with transactions, balances and consistency checks | [Prebuilt schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#bank-statement) | | RIB / bank identity statement | IBAN, BIC and holder with IBAN validation | Custom extractor | | Bank certificate | Attestation bancaire (domiciliation, balance, account existence) | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/banking-wealth/#attestation-bancaire) | | Transfer order | Ordre de virement with payer, beneficiary and amount | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/banking-wealth/#ordre-virement) | | Amortization schedule | Extrait d'échéancier with instalments and outstanding balance | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/banking-wealth/#extrait-echeancier) | | Wealth statement | Relevé de patrimoine with assets, liabilities and net worth | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/banking-wealth/#releve-patrimoine) | | Wealth profile sheet | Fiche patrimoniale with income, charges and debt ratio | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/banking-wealth/#fiche-patrimoniale) | Financial Statements & Accounting --------------------------------- | Document type | Description | Delivery | | --- | --- | --- | | Liasse fiscale (CERFA 2050–2059) | French fiscal return package: 2033 A/B/C, 2050, 2051, 2052, 2053, 2059 G | [Prebuilt schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#cerfa-2050) | | Bilan actif / passif | French balance sheet, assets and liabilities | [Prebuilt schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#fr-bilan-actif) | | Compte de résultat | French income statement | [Prebuilt schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#fr-compte-resultat) | | Belgian internal annual accounts | Interne jaarrekening: bilan actif, bilan passif, compte de résultat | [Prebuilt schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#be-interne-jaarrekening-bilan-actif) | | German SuSa | Summen- und Saldenliste trial balance | [Prebuilt schema](https://app.holofin.ai/docs/api/v1.1/document-schemas/#de-susa) | | Audited annual report | Financial statements with the auditor's opinion | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/business-financial/#rapport-annuel) | | Business plan | Forecast revenue, results and financing plan | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/business-financial/#business-plan) | | Accountant's certificates | Director income and company activity certified by a chartered accountant | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/business-financial/#attestation-comptable-dirigeant) | Legal & KYB ----------- | Document type | Description | Delivery | | --- | --- | --- | | KBIS extract | Commercial register extract with company identity and directors | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/business-financial/#extrait-kbis) | | RCS extract | Registre du Commerce et des Sociétés registration extract | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/business-financial/#extrait-rcs) | | SIRENE situation notice | INSEE company identity sheet with SIREN / SIRET | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/business-financial/#avis-sirene) | | Beneficial owners (CERFA 16062) | French beneficial-owner declaration with ownership percentages | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/business-financial/#cerfa-16062) | | Beneficial owners (Monaco RBE) | Monaco beneficial-owner register extract | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/business-financial/#rbe-monaco) | | General meeting minutes | Procès-verbal d'assemblée générale with resolutions and votes | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/business-financial/#pv-assemblee-generale) | Vehicle & Financed Asset ------------------------ | Document type | Description | Delivery | | --- | --- | --- | | Vehicle registration (carte grise) | All lettered fields with SIV plate and VIN validation | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/vehicle-asset/#carte-grise) | | Temporary registration (CPI) | Provisional WW registration pending the final carte grise | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/vehicle-asset/#cpi) | | Non-pledge certificate | Certificat de situation administrative with pledge and opposition status | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/vehicle-asset/#certificat-non-gage) | | Vehicle / asset invoice | Invoice for the financed good with seller, asset and amounts | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/vehicle-asset/#facture-vehicule) | | Purchase order | Bon de commande with goods, amounts and deposit | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/vehicle-asset/#bon-commande) | After-Sales ----------- | Document type | Description | Delivery | | --- | --- | --- | | Payment deferral request | Customer letter requesting an instalment deferral, including handwritten | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/after-sales/#report-echeance) | | Guarantee waiver | Déclaration de désistement de garantie with waiver statement | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/after-sales/#desistement-garantie) | | Third-party sale agreement | Client agreement to sell the financed vehicle to a third party | Custom extractor[Schema](https://app.holofin.ai/docs/api/v1.1/schemas/after-sales/#accord-vente-tiers) | Health ------ | Document type | Description | Delivery | | --- | --- | --- | | Carte Vitale / health coverage | Carte Vitale and attestation de droits with coverage details | Custom extractor | Defining a schema for a custom-extractor type --------------------------------------------- For any type marked **Custom extractor**, define the output structure once and reuse it across your workflows: * Create an extractor with your JSON schema (nested objects and arrays supported), or generate one automatically from a sample document. * The API then returns your structure for every document of that type, with bounding-box traceability per field. * Validation and fraud signals run on every extraction and are surfaced as graded alerts. See the [Extraction](https://app.holofin.ai/docs/api/v1.1/extraction/) documentation for schema definition, and [Document Schemas](https://app.holofin.ai/docs/api/v1.1/document-schemas/) for the prebuilt contracts. --- ### Document Schemas URL: https://app.holofin.ai/docs/api/v1.1/document-schemas/ Document Schemas ================ The Holofin API extracts structured data from French financial statements (liasse fiscale) including CERFA forms for the simplified and normal tax regimes. This page describes the response for balance sheets, income statements and supporting documents. Jump to a document type Complete API Response Format ---------------------------- The financial statement API returns extraction results in a structured format with document parts for each document type detected (CERFA forms, balance sheets, income statements, German SuSa, etc.): ``` { "extraction_id": "wea_E58wA3KMSneeh85z", "document_name": "liasse_fiscale_2024.pdf", "extraction_date": "2025-06-10T17:12:45.109937+00:00", "document_parts": [ { "key": "segment_1", "part_type": "CERFA-2033-A", "validation_status": "OK", "validation_codes": [], "checks": [ { "type": "extraction", "code": "ACTIF_TOTAL_BRUT", "label": "Total actif brut", "description": null, "status": "passed", "severity": "error", "message": null, "reason": null } ], "pages": [1, 2], "version": "2024", "data": { "028": { "value": 60571, "label": "Immobilisations corporelles (Brut)" }, "030": { "value": 34551, "label": "Immobilisations corporelles (Amortissements)" }, "028_net_n": { "value": 26024, "label": "Immobilisations corporelles (Net N)" } } }, { "key": "segment_2", "part_type": "CERFA-2033-B", "validation_status": "ERROR", "validation_codes": [], "pages": [3, 4], "version": "2024", "data": { "210": { "value": 500000, "label": "Chiffre d'affaires" } } } ] } ``` ### Root Level Fields | Field | Type | Description | | --- | --- | --- | | extraction\_id | string | Unique document identifier | | document\_name | string | Original document filename | | extraction\_date | string | ISO 8601 timestamp of extraction | | document\_parts | array | Array of extracted document parts, each identified by type (e.g., CERFA-2033-A, FR-BILAN-ACTIF, DE-SUSA) | ### Document Part Structure Each item in the `document_parts` array contains a single document part with its extracted data: | Field | Type | Description | | --- | --- | --- | | validation\_status | string | null | Validation result: `OK` (all consistency checks passed), `WARNING` (soft issues detected), or `ERROR` (validation errors detected) | | validation\_codes | array of strings | Stable upper-snake tokens describing the decisions taken during validation. Always present, may be empty. Codes appear here whether they fired an error, a warning, or a neutral note (for example a savings-account waiver). Branch on codes for behavior; rely on `validation_status` for the rolled-up verdict. See the [code registry](#validation-codes) below. | | checks | array of objects | The full rule inventory for this part: one flat array of every check that ran: passed, failed and skipped alike. Each entry carries `type` (`extraction`, `business` or `id_verification`), `code`, `label`, `description`, `status` (`passed` / `failed` / `skipped`), `severity`, `message` and `reason`. **Extraction** checks judge whether the document was read correctly; **business** checks judge your own acceptance rules; **id\_verification** checks carry the identity-verification provider's verdict on identity documents. The `code` is the same token used in `validation_codes`, so the two join directly. A category that ran no checks contributes no entries. Organizations with rule details enabled additionally receive `formula`, `values`, `expected`, `actual` and `tolerance` per entry where available; absent keys uniformly mean the data is not available. | | pages | array of integers | Page numbers where this document part appears in the original document | | version | string | null | Document version/year when applicable (e.g., `2024` for SD2024 CERFA forms) | | data | object | Extracted field values keyed by field codes. Each field contains `value`, `label`, and (for numeric form cells) `column`: a machine-readable token naming the form column the value belongs to (e.g. `net_n`, `net_n_1`, `total_n`, `n_1`, `value_brut`) | ### Validation code registry Each `validation_codes` entry is a stable upper-snake token. Branch on the code; the `validation_status` column shows whether the code, when fired, contributes to the rolled-up verdict. Bank statement codes have [their own table](#bank-statement-validation-codes). | Code | When emitted | Affects validation\_status | Part types | | --- | --- | --- | --- | | \_NOT\_PERFORMED | A financial-statement consistency rule could not be evaluated because the document does not carry the data it needs: the target KPI line (for example a P&L without a résultat d'exploitation line) or every component line of the formula is absent. The check is traced as not performed instead of failing. The prefix is the upper-snake form of the check identifier, e.g. `TOTAL_RESULTAT_D_EXPLOITATION_NET_N_NOT_PERFORMED`. | No (status OK) | financial statements | | SIREN\_RESOLVED | The French company registry recognises the number carried by a field typed `siren` in the schema. Informational, surfaced for audit. | No (status OK) | custom schemas | | SIREN\_NOT\_FOUND | The registry has no such SIREN, or the value printed on the document is not a usable 9-digit SIREN / 14-digit SIRET so nothing could be looked up. | Yes (ERROR) | custom schemas | | SIREN\_ENRICHMENT\_UNAVAILABLE | The registry lookup could not be performed (outage or timeout), so we have no opinion on the company. Never means the company does not exist. | Yes (ERROR) | custom schemas | --- Bank Statement JSON Schema ========================== This page documents the JSON returned for a bank statement: document properties, transactions, extraction metadata and consistency check results. part\_type:`bank-statement` Schema Overview --------------- The bank statement extraction result contains the following main components: * **Document Properties**: Bank name, currency, start/end balances * **Transactions**: Detailed transaction records with dates, descriptions, and amounts * **Metadata**: Extraction information and model details * **Consistency Checks**: Validation results and balance verification Complete JSON Response ---------------------- The API returns extraction results in a simplified format without OCR positioning data: ``` { "bank_name": "Qonto", "currency": "EUR", "account_type": "current", "usage_type": "business", "account_name": null, "client_names": ["Starflight Dynamics GmbH"], "account_number": "DE15100101232339317943", "advisor_name": null, "branch_name": null, "start_balance": 3071.69, "end_balance": 3030.39, "start_date": "2025-05-01", "end_date": "2025-05-31", "validation_status": "OK", "validation_codes": [], "checks": [ { "type": "extraction", "code": "BALANCE_EQUATION", "label": "Balance equation", "description": null, "status": "passed", "severity": "error", "message": null, "reason": null } ], "pages": [1, 2, 3], "annotations": [ // optional, omitted when none detected {"page": 1, "content": "Checked OK"}, {"page": 2, "content": "See invoice #4521"} ], "flagged_deferred_card_payments": [ // optional, omitted when no deferred-card tables are detected { "card_type": "Visa Business", "card_number": "**** **** **** 1234", "card_holder": "Jane Doe", "debit_date": "2025-06-02", "total_amount": -842.31, "page": 3, "transactions": [ { "transaction_date": "2025-05-12", "label": "AMAZON EU SARL", "amount": -42.18, "page": 3, "row": 1 } ] } ], "ocr_confidence": { // opt-in, omitted unless enabled for your organization "bank_name": {"min": 0.71, "mean": 0.85, "engine": "engine_a"}, "end_balance": {"min": 0.98, "mean": 0.99, "engine": "engine_a"} }, "transactions": [ { "transaction_date": "2025-05-02", "value_date": "2025-05-02", "amount": -963.9, "description": "Schmittlein Kloster Arbeitsrecht", "credit": null, "debit": 963.9, "page": 1, "row": 1 }, { "transaction_date": "2025-05-15", "value_date": "2025-05-15", "amount": 922.6, "description": "Virement Client SARL Dupont", "credit": 922.6, "debit": null, "page": 1, "row": 2, "field_annotations": { // optional, omitted when none detected "description": { "content": ["bank"] }, "credit": { "is_highlighted": true } }, "ocr_confidence": { // opt-in, omitted unless enabled for your organization "description": {"min": 0.62, "mean": 0.8, "engine": "engine_a"} } } ] } ``` Response Payload ---------------- | Name | Type | Description | | --- | --- | --- | | bank\_name | string | null | Bank or financial institution name | | currency | string | null | Currency code (e.g., `EUR`, `USD`) | | account\_type | string | null | Account type (`current`, `savings`, `securities` (compte titres / PEA / CTO / brokerage), or `loans`) | | usage\_type | string | null | Usage type (`business` or `personal`) | | account\_name | string | null | Account name from the statement | | client\_names | array of strings | Client/account holder names | | account\_number | string | null | Account number from the statement | | advisor\_name | string | null | Personal banker / relationship manager name (FR: *conseiller*). `null` when the statement does not surface it. | | branch\_name | string | null | Branch label or code (FR: *agence*). `null` when the statement does not surface it. | | start\_balance | number | null | Starting balance of the statement period | | end\_balance | number | null | Ending balance of the statement period | | start\_date | string | null | Start date of the statement period (YYYY-MM-DD) | | end\_date | string | null | End date of the statement period (YYYY-MM-DD) | | validation\_status | string | null | Result of data validation. Possible values: `OK` (all checks passed), `WARNING` (soft issues detected), or `ERROR` (validation errors detected, such as balance mismatches) | | validation\_codes | array of strings | Stable tokens explaining the decisions taken during validation. Always present, may be empty. Branch on these for behavior; `validation_status` remains the rolled-up verdict. See [Validation codes](#bank-statement-validation-codes). | | checks | object | Per-category breakdown of `validation_status` (extraction quality vs your own business rules). See the Document Parts page for the full contract. Bank-statement balance checks are extraction checks. | | pages | array of integers | Page numbers where the bank statement appears in the original document (null for bank statements in v1.1) | | annotations | array of objects | optional Handwritten annotations detected on the document pages (e.g., margin notes, stamps, reviewer comments). Each object contains `page` (integer, page number) and `content` (string, annotation text). This field is omitted entirely when no annotations are detected. | | flagged\_deferred\_card\_payments | array of objects | optional Deferred-debit card payment tables detected on the statement (FR: *cartes à débit différé*). Each entry represents one card and contains its own list of transactions billed on a future debit date. Field is omitted entirely when no deferred-card tables are detected. See [Deferred Card Fields](#deferred-card-fields). | | ocr\_confidence | object | opt-in Per-property OCR confidence scores, keyed by property name (e.g. `bank_name`, `end_balance`). Only included when OCR confidence is enabled for your organization; properties without an OCR reading are omitted. See [OCR Confidence Scores](#ocr-confidence). | | transactions | array | List of transaction records | ### Transaction Fields | Field | Type | Description | | --- | --- | --- | | transaction\_date | string | null | Date when transaction occurred (YYYY-MM-DD format) | | value\_date | string | null | Date when transaction value was applied | | description | string | null | Transaction description or label | | amount | number | null | Transaction amount (positive for credits, negative for debits) | | debit | number | null | Debit amount (null if credit transaction) | | credit | number | null | Credit amount (null if debit transaction) | | page | integer | PDF page number where transaction appears | | row | integer | Row number within the page | | transaction\_id | string | opt-in Stable transaction identifier (`txn_…`), assigned at extraction time and preserved across reviewer edits. Only included when transaction ids are enabled for your organization. | | field\_annotations | object | optional Per-field annotation metadata indicating handwritten marks or highlighting on specific transaction fields. Keys are field names (`transaction_date`, `value_date`, `description`, `debit`, `credit`). Each value may contain `content` (array of annotation labels, e.g. `["bank"]`) and/or `is_highlighted` (boolean). This field is omitted entirely when no annotations are detected on the transaction. | | ocr\_confidence | object | opt-in Per-field OCR confidence scores. Keys are field names (`transaction_date`, `value_date`, `description`, `debit`, `credit`). Only included when OCR confidence is enabled for your organization; fields without an OCR reading are omitted. See [OCR Confidence Scores](#ocr-confidence). | ### OCR Confidence Scores opt-in When enabled for your organization, extraction responses and webhook payloads include OCR confidence scores for document properties (in the part-level `ocr_confidence` object) and for each transaction field (in the transaction-level `ocr_confidence` object). Contact us to enable it for your account. Each entry has the same shape: | Field | Type | Description | | --- | --- | --- | | min | number | Lowest OCR confidence among the text regions that produced the value, between 0 and 1 (rounded to 4 decimals). Use this as the pessimistic signal: a low `min` means at least part of the value was hard to read. | | mean | number | null | Average OCR confidence across those text regions, between 0 and 1 (rounded to 4 decimals). | | engine | string | null | Opaque identifier of the OCR engine that produced the scores. Confidence scales are **not comparable across engines**. Interpret `min` and `mean` relative to the engine, and calibrate any thresholds per engine. | A field or property is omitted from `ocr_confidence` when it has no OCR reading, typically for digitally-born (native) PDFs where the text is read directly without OCR, or for values corrected by a human reviewer. The `ocr_confidence` object itself is omitted when no field carries a score, so treat a missing entry as "no signal", not as low confidence. ### Deferred Card Fields Each entry in `flagged_deferred_card_payments` represents a deferred-debit card and its pending transactions, billed together on the card's debit date. | Field | Type | Description | | --- | --- | --- | | card\_type | string | null | Card brand or product name (e.g., `Visa Business`) | | card\_number | string | null | Masked card number as shown on the statement | | card\_holder | string | optional Cardholder name when present on the statement | | debit\_date | string | optional Future date on which the aggregated total will be debited (YYYY-MM-DD) | | total\_amount | number | optional Aggregated amount that will be debited on `debit_date` (negative for debits) | | page | integer | optional PDF page number where the card table appears | | transactions | array | Card transactions covered by this deferred debit. Each item contains `transaction_date`, `label`, `amount`, `page`, and `row`. | Validation codes ---------------- Each part reports its codes in `validation_codes`, grouped here by what to do about them. The status column is the code's effect on `validation_status`: `null` means the part has no verdict, and "unchanged" means the status still follows the balance check. ### Needs attention The reading may be wrong or incomplete. Review the part before relying on it. | Code | Status | Meaning | | --- | --- | --- | | BALANCE\_MISMATCH | ERROR | Opening balance plus transactions differs from the closing balance by more than €0.02. | | SECTION\_MIXED\_CURRENCIES | ERROR | The transactions of one part use several currencies, so the part holds more than one account and its totals add them together. | | MISSING\_START\_BALANCE | null | The statement declares no opening balance, and the product is not one that omits it by design. | | MISSING\_END\_BALANCE | null | The statement declares no closing balance. | | INVALID\_BALANCE\_FORMAT | null | A declared balance could not be parsed as a number. | | TRANSACTION\_DATE\_OUT\_OF\_SEQUENCE | unchanged | A block of rows is dated weeks away from the rows around it, and swapping day and month would restore the order. The date is kept and the message gives the one that fits. | | CLOSING\_BALANCE\_CONTRADICTED\_BY\_SIBLING | unchanged | One statement's closing balance is the exact negative of the next one's opening balance, so one read lost its debit sign. Neither is changed. | | INVALID\_IBAN\_CHECKSUM | unchanged | The IBAN failed its MOD-97 check and could not be corrected. The value is kept as read. | | STATEMENT\_LIKELY\_MISSING\_PAGE | unchanged | A current account declared no opening balance, so a page is probably absent. Always alongside MISSING\_START\_BALANCE. | ### Expected, no action Normal for this kind of product or transaction. | Code | Status | Meaning | | --- | --- | --- | | SAVING\_ACCOUNT\_WO\_START\_BALANCE\_IS\_EXPECTED | OK | A savings product shows a closing balance and no opening balance. | | SECURITIES\_ACCOUNT\_WO\_START\_BALANCE\_IS\_EXPECTED | OK | A securities product (compte titres, PEA, CTO) shows a closing valuation and no opening balance. | | LOAN\_ACCOUNT\_WO\_BALANCE\_IS\_EXPECTED | OK | A loan statement is missing a balance. Loan statements carry no balance equation. | | CARD\_ACCOUNT\_WO\_BALANCE\_IS\_EXPECTED | OK | A card statement is missing a balance. Card statements often omit the opening one. | | TRANSACTION\_DATE\_OUTSIDE\_PERIOD | unchanged | A transaction is dated more than a few days outside the statement period, usually a card operation carried over. | ### Not scored The part is not a statement to reconcile, so it has no verdict. Leave it out of your own totals. | Code | Status | Meaning | | --- | --- | --- | | DUPLICATE\_STATEMENT\_SEGMENT\_EXCLUDED | null | Another part of the document carries this statement in full, usually a rescan. This copy's transactions are excluded. | | ACCOUNT\_SUMMARY\_SEGMENT\_NOT\_SCORED | null | The part recaps a period covering other statements of the account and has no transactions. | | BALANCE\_ONLY\_SEGMENT\_NOT\_SCORED | null | The part declares one balance and no transactions, usually a line of an account summary table. | | ACCOUNT\_WITHOUT\_STATEMENT | null | The part names an account but has no balance and no transactions. | ### Corrected for you The reading was fixed during validation. The code is there for your audit trail. | Code | Status | Meaning | | --- | --- | --- | | TRANSACTION\_DATE\_CORRECTED\_FROM\_PAGE\_IMAGE | unchanged | A misread transaction date was corrected. | | DUPLICATE\_PAGE\_TRANSACTIONS\_EXCLUDED | unchanged | A page repeats an earlier page row for row and the balance only reconciles without it. Its transactions are excluded. | | ACCOUNT\_NUMBER\_CORRECTED\_FROM\_SIBLING | unchanged | An IBAN that failed its MOD-97 check was corrected. | | ACCOUNT\_NUMBER\_CORRECTED\_FROM\_SEGMENTER | unchanged | An IBAN that failed its MOD-97 check was corrected. | | ACCOUNT\_NUMBER\_CORRECTED\_FROM\_PAGE\_IMAGE | unchanged | An IBAN that failed its MOD-97 check was corrected. | | ACCOUNT\_NUMBER\_COMPLETED\_FROM\_SIBLING | unchanged | The part reported a domestic account number while other statements of the account reported the IBAN containing it. The part takes the IBAN. | --- Utility Bill JSON Schema ======================== This page documents the JSON returned for a utility bill (electricity, gas, water). Each meter or delivery point produces a separate document part with consumption periods and optional totals. part\_type:`utility_bill` Schema Overview --------------- The utility bill extraction result contains the following main components: * **Document Properties**: Delivery point reference, invoice ID, invoice date, fluid type, supplier * **Consumptions**: Individual consumption periods with dates, units, and kWh or liter conversion * **Total**: Aggregate consumption for the meter (when available on the document) * **Consistency Checks**: Unit conversion validation, reasonableness checks, total consistency Complete JSON Response ---------------------- The API returns extraction results in a simplified format: ``` { "dp_ref": "PDL-30001234567890", "invoice_id": "EDF-2024-00341", "invoice_date": "2024-03-15", "fluid": "ELECTRICITY", "supplier": "EDF", "consumptions": [ { "consumption_type": "hc", "start_date": "2024-01-01", "end_date": "2024-03-15", "consumption_kwh": "4820", "consumption_l": null, "original_unit": "kWh", "original_consumption": "4820", "description": "Heures Creuses" }, { "consumption_type": "hp", "start_date": "2024-01-01", "end_date": "2024-03-15", "consumption_kwh": "6340", "consumption_l": null, "original_unit": "kWh", "original_consumption": "6340", "description": "Heures Pleines" } ], "total": { "consumption_type": null, "start_date": "2024-01-01", "end_date": "2024-03-15", "consumption_kwh": "11160", "consumption_l": null, "original_unit": "kWh", "original_consumption": "11160", "description": "Total" } } ``` Response Payload ---------------- | Name | Type | Description | | --- | --- | --- | | dp\_ref | string | null | Delivery point reference (e.g., PDL, PRM, PCE for France; MPAN, MPRN for UK; meter/account number for US) | | invoice\_id | string | null | Invoice or bill reference number | | invoice\_date | string | null | Invoice date (YYYY-MM-DD) | | fluid | string | null | Fluid type: `ELECTRICITY`, `GAS`, or `WATER` | | supplier | string | null | Utility supplier or provider name | | consumptions | array | List of consumption period records | | total | object | optional Aggregate consumption total for the meter. Uses the same fields as individual consumption records. Omitted entirely when the document does not print a total. | ### Consumption Fields | Field | Type | Description | | --- | --- | --- | | consumption\_type | string | null | Tariff tier code: `base`, `peak`, `off_peak`, `hp`, `hc`, `hph`, `hpe`, `hch`, `hce`, `f1`, `f2`, `f3`, `ops`, `mps`, `ofps`, `opw`, `mpw`, `ofpw`, `total`, `other` | | start\_date | string | null | Consumption period start date (YYYY-MM-DD) | | end\_date | string | null | Consumption period end date (YYYY-MM-DD) | | consumption\_kwh | string | null | Consumption converted to kWh. Automatically computed from `original_consumption` and `original_unit` when possible. `null` for water meters or unknown units. | | consumption\_l | string | null | Water consumption converted to liters. Automatically computed from `original_consumption` and `original_unit` for water meters using `m3`, `ccf`, `L`, or `Gal`. `null` for electricity and gas meters. | | original\_unit | string | null | Canonical unit. One of `kWh`, `MWh`, `GWh`, `Wh`, `therms`, `ccf`, `m3`, `L`, or `Gal`. | | original\_consumption | string | null | Consumption value in the original unit as printed on the document | | description | string | null | Description or label for this consumption line (e.g., "Heures Creuses", "Off-Peak") | Consistency Checks ------------------ The API runs these consistency checks on every extracted utility bill: * **Unit Conversion**: Verifies that `original_unit` is a recognized unit and records the kWh conversion for electricity/gas or liter conversion for water * **Reasonableness**: Flags negative or zero consumption values * **Total Consistency**: Verifies that the sum of individual consumption lines matches the total (when present and using the same unit) --- FR Bilan Actif - Generic Balance Sheet (Assets) =============================================== The **FR Bilan Actif** is a generic French balance sheet assets template that can extract from various balance sheet formats. This form presents all assets owned or controlled by the company, organized by liquidity and type. part\_type:`bilan-actif` **Note:** This is a generic schema that adapts to various French balance sheet formats. It extracts the asset side (Actif) of the balance sheet with granular detail. ### Document Structure The FR Bilan Actif is organized into the following major sections: * **Actif Immobilisé**: Fixed assets (long-term investments) + Immobilisations incorporelles (intangible assets) + Immobilisations corporelles (tangible assets) + Immobilisations financières (financial assets) * **Actif Circulant**: Current assets (short-term, liquid assets) + Stocks et en-cours (inventory and work in progress) + Créances (receivables) + Valeurs mobilières de placement (marketable securities) + Disponibilités (cash and cash equivalents) * **Comptes de Régularisation**: Accruals and deferrals ### FR Bilan Actif Data Structure Within the `document_parts` array, the fr\_bilan\_actif `data` object contains the following structure: ``` { "AI13": { "value": 150000, "label": "Fonds commercial - Brut", "column": "value_brut" }, "AI14": { "value": 0, "label": "Fonds commercial - Amort/Prov", "column": "value_amort_prov" }, "AI15": { "value": 150000, "label": "Fonds commercial - Net N", "column": "net_n" }, "AI16": { "value": 150000, "label": "Fonds commercial - Net N-1", "column": "net_n_1" }, "AC01": { "value": 250000, "label": "Terrains - Brut", "column": "value_brut" }, "AC02": { "value": 0, "label": "Terrains - Amort/Prov", "column": "value_amort_prov" }, "AC03": { "value": 250000, "label": "Terrains - Net N", "column": "net_n" }, "AC05": { "value": 500000, "label": "Constructions - Brut", "column": "value_brut" }, "AC06": { "value": 120000, "label": "Constructions - Amort/Prov", "column": "value_amort_prov" }, "AC07": { "value": 380000, "label": "Constructions - Net N", "column": "net_n" }, "AS09": { "value": 85000, "label": "Marchandises - Brut", "column": "value_brut" }, "AS11": { "value": 85000, "label": "Marchandises - Net N", "column": "net_n" }, "ACR05": { "value": 125000, "label": "Créances clients et comptes rattachés - Brut", "column": "value_brut" }, "ACR06": { "value": 5000, "label": "Créances clients et comptes rattachés - Amort/Prov", "column": "value_amort_prov" }, "ACR07": { "value": 120000, "label": "Créances clients et comptes rattachés - Net N", "column": "net_n" }, "AD01": { "value": 45000, "label": "Disponibilités - Brut", "column": "value_brut" }, "AD03": { "value": 45000, "label": "Disponibilités - Net N", "column": "net_n" }, "AI30": [ { "label": { "value": "Logiciels informatiques", "markdown_ids": ["mk_123"] }, "value_brut": { "value": 50000, "markdown_ids": ["mk_124"] }, "value_amort_prov": { "value": 30000, "markdown_ids": ["mk_125"] }, "net_n": { "value": 20000, "markdown_ids": ["mk_126"] }, "net_n_1": { "value": 25000, "markdown_ids": ["mk_127"] } }, { "label": { "value": "Licences", "markdown_ids": ["mk_128"] }, "value_brut": { "value": 15000, "markdown_ids": ["mk_129"] }, "value_amort_prov": { "value": 10000, "markdown_ids": ["mk_130"] }, "net_n": { "value": 5000, "markdown_ids": ["mk_131"] }, "net_n_1": { "value": 7000, "markdown_ids": ["mk_132"] } } ], "ATOT01": { "value": 1250000, "label": "TOTAL GENERAL ACTIF - Brut", "column": "value_brut" }, "ATOT02": { "value": 125000, "label": "TOTAL GENERAL ACTIF - Amort/Prov", "column": "value_amort_prov" }, "ATOT03": { "value": 1125000, "label": "TOTAL GENERAL ACTIF - Net N", "column": "net_n" }, "ATOT04": { "value": 1050000, "label": "TOTAL GENERAL ACTIF - Net N-1", "column": "net_n_1" }, "company_name": "SAS EXEMPLE COMMERCE", "siret": "12345678901234", "current_year_end": "31/12/2023", "previous_year_end": "31/12/2022" } ``` ### Field ID Naming Convention Field IDs follow a systematic pattern based on asset category and column type: | Prefix | Category | Example Fields | | --- | --- | --- | | AI | Immobilisations Incorporelles | AI01-AI24, AI30 (Intangible assets) | | AC | Immobilisations Corporelles | AC01-AC24, AC30 (Tangible assets) | | AF | Immobilisations Financières | AF01-AF16, AF30 (Financial assets) | | AS | Stocks et En-cours | AS01-AS16, AS30 (Inventory) | | ACR | Créances | ACR01-ACR16, ACR30 (Receivables) | | AV | Valeurs Mobilières | AV01-AV04, AV30 (Securities) | | AD | Disponibilités | AD01-AD04, AD30 (Cash) | | AR | Comptes de Régularisation | AR01-AR16, AR30 (Accruals) | | ATOT | Total Général Actif | ATOT01-ATOT04 (Grand total) | ### Column Structure Each asset line item has up to 4 columns representing different valuations. Every scalar field carries a machine-readable `column` property naming the form column it belongs to (use it instead of parsing the label): * **`value_brut`** (Brut): Gross value (original cost or acquisition value), current fiscal year * **`value_amort_prov`** (Amort/Prov): Depreciation and provisions (accumulated over time), current fiscal year * **`net_n`** (Net N): Net value for current fiscal year (Brut - Amort/Prov) * **`net_n_1`** (Net N-1): Net value for previous fiscal year (for comparison) Dynamic "Autres" arrays (e.g. `AI30`) encode the same columns as object keys (`value_brut`, `value_amort_prov`, `net_n`, `net_n_1`) on each item, so the vocabulary is identical across both shapes. Metadata fields (`company_name`, dates, ...) have no `column` property. **Note on Subtotals:** Section subtotals (e.g., Total Immobilisations incorporelles, Total Actif Immobilisé, Total Actif Circulant) are computed automatically and are not included in the API response. Only individual line items and the grand total (ATOT01-ATOT04) are extracted. ### Key Asset Categories Extracted | Section | Key Fields | Item Label | | --- | --- | --- | | Immobilisations Incorporelles | AI01-AI04 | Frais d'établissement (Start-up costs) | | AI05-AI08 | Frais de développement (R&D costs) | | AI09-AI12 | Concessions, brevets (Patents, licenses) | | AI13-AI16 | Fonds commercial (Goodwill) | | AI17-AI20 | Autres immobilisations incorporelles | | AI21-AI24 | Avances et acomptes | | Immobilisations Corporelles | AC01-AC04 | Terrains (Land) | | AC05-AC08 | Constructions (Buildings) | | AC09-AC12 | Installations techniques, matériel (Equipment) | | AC13-AC16 | Immobilisations en cours (Assets under construction) | | AC17-AC20 | Autres immobilisations corporelles | | AC21-AC24 | Avances et acomptes | | Immobilisations Financières | AF01-AF04 | Participations (Equity investments) | | AF05-AF08 | Autres participations | | AF09-AF12 | Créances rattachées à des participations | | AF13-AF16 | Autres titres immobilisés | | Stocks et En-cours | AS01-AS04 | Matières premières (Raw materials) | | AS05-AS08 | En-cours de production (Work in progress) | | AS09-AS12 | Marchandises (Goods for resale) | | AS13-AS16 | Produits finis (Finished goods) | | Créances | ACR01-ACR04 | Avances et acomptes versés | | ACR05-ACR08 | Créances clients (Customer receivables) | | ACR09-ACR12 | Autres créances (Other receivables) | | ACR13-ACR16 | Capital souscrit appelé, non versé | | Valeurs Mobilières de Placement | AV01-AV04 | Valeurs mobilières de placement (Marketable securities) | | Disponibilités | AD01-AD04 | Disponibilités (Cash and cash equivalents) | | Comptes de Régularisation | AR01-AR04 | Charges constatées d'avance (Prepaid expenses) | | AR05-AR08 | Frais d'émission d'emprunt à étaler (Bond issue costs) | | AR09-AR12 | Primes de remboursement des obligations (Bond redemption premiums) | | AR13-AR16 | Écart de conversion actif (Foreign exchange gains) | ### Dynamic "Autres" Fields The schema includes dynamic "Autres" (Other) fields that can contain multiple line items. These fields are identified by their `"items": []` property in the schema and are returned as arrays of objects. **Array items are form rows, not cells:** each item carries one value per column, so the column tokens appear as the object keys (`value_brut`, `value_amort_prov`, `net_n`, `net_n_1`) instead of a `column` property. The tokens are the same ones scalar fields expose in `column`. Each array item contains the following properties with the standard `{value, markdown_ids}` structure: * **label**: Text description of the line item (object with value and markdown\_ids) * **value\_brut**: Gross value (object with value and markdown\_ids) * **value\_amort\_prov**: Amortization/Provisions (object with value and markdown\_ids) * **net\_n**: Net value for current fiscal year (object with value and markdown\_ids) * **net\_n\_1**: Net value for previous fiscal year (object with value and markdown\_ids) Example array structure: ``` "AI30": [ { "label": { "value": "Logiciels informatiques", "markdown_ids": ["mk_123"] }, "value_brut": { "value": 50000, "markdown_ids": ["mk_124"] }, "value_amort_prov": { "value": 30000, "markdown_ids": ["mk_125"] }, "net_n": { "value": 20000, "markdown_ids": ["mk_126"] }, "net_n_1": { "value": 25000, "markdown_ids": ["mk_127"] } }, { "label": { "value": "Licences", "markdown_ids": ["mk_128"] }, "value_brut": { "value": 15000, "markdown_ids": ["mk_129"] }, "value_amort_prov": { "value": 10000, "markdown_ids": ["mk_130"] }, "net_n": { "value": 5000, "markdown_ids": ["mk_131"] }, "net_n_1": { "value": 7000, "markdown_ids": ["mk_132"] } } ] ``` Array fields in the Bilan Actif schema include: * `AI30` - Autres immobilisations incorporelles * `AC30` - Autres immobilisations corporelles * `AF30` - Autres immobilisations financières * `AS30` - Autres stocks * `ACR30` - Autres créances * `AV30` - Autres valeurs mobilières * `AD30` - Autres disponibilités * `AR30` - Autres comptes de régularisation ### Understanding Asset Classification French accounting organizes assets by liquidity and intended use: * **Actif Immobilisé**: Long-term assets intended to be held for more than one year (e.g., buildings, equipment, long-term investments) * **Actif Circulant**: Short-term assets that are expected to be converted to cash or consumed within one year (e.g., inventory, receivables, cash) * **Comptes de Régularisation**: Timing differences between accounting periods (e.g., prepaid expenses, deferred charges) ### Metadata Fields The following metadata fields are also extracted: * **company\_name**: Legal name of the company * **company\_siret**: French business identification number (14 digits) * **company\_address**: Registered address of the company * **fiscal\_year\_end\_n**: End date of current fiscal year (DD/MM/YYYY) * **fiscal\_year\_end\_n1**: End date of previous fiscal year (DD/MM/YYYY) --- FR Bilan Passif - Generic Balance Sheet (Liabilities & Equity) ============================================================== The **FR Bilan Passif** is a generic French balance sheet liabilities and equity template that can extract from various balance sheet formats. This form presents all liabilities and equity (sources of financing), organized by permanence and type. part\_type:`bilan-passif` **Note:** This is a generic schema that adapts to various French balance sheet formats. It extracts the liabilities and equity side (Passif) of the balance sheet with granular detail. ### Document Structure The FR Bilan Passif is organized into the following major sections: * **Capitaux Propres**: Shareholders' equity (permanent capital) + Capital social (share capital) + Réserves (reserves) + Report à nouveau (retained earnings) + Résultat de l'exercice (current year profit/loss) + Subventions d'investissement (investment grants) * **Autres Fonds Propres**: Other equity instruments + Produits des émissions de titres participatifs (participatory securities) + Avances conditionnées (conditional advances) * **Provisions pour Risques et Charges**: Provisions for risks and charges * **Dettes**: Liabilities (debts and payables) + Dettes financières (financial debts) + Dettes d'exploitation (operating liabilities) + Dettes diverses (other liabilities) + Comptes de régularisation (accruals and deferrals) ### FR Bilan Passif Data Structure Within the `document_parts` array, the fr\_bilan\_passif `data` object contains the following structure: ``` { "C01": { "value": 500000, "label": "Capital social - Net N", "column": "net_n" }, "C02": { "value": 500000, "label": "Capital social - Net N-1", "column": "net_n_1" }, "C03": { "value": 100000, "label": "Primes d'émission, de fusion, d'apport - Net N", "column": "net_n" }, "C04": { "value": 95000, "label": "Primes d'émission, de fusion, d'apport - Net N-1", "column": "net_n_1" }, "R07": { "value": 50000, "label": "Réserve légale - Net N", "column": "net_n" }, "R08": { "value": 45000, "label": "Réserve légale - Net N-1", "column": "net_n_1" }, "R11": { "value": 150000, "label": "Autres réserves - Net N", "column": "net_n" }, "R12": { "value": 140000, "label": "Autres réserves - Net N-1", "column": "net_n_1" }, "N11": { "value": 75000, "label": "Report à nouveau - Net N", "column": "net_n" }, "N12": { "value": 65000, "label": "Report à nouveau - Net N-1", "column": "net_n_1" }, "R13": { "value": 125000, "label": "Résultat de l'exercice - Net N", "column": "net_n" }, "R14": { "value": 105000, "label": "Résultat de l'exercice - Net N-1", "column": "net_n_1" }, "P05": { "value": 35000, "label": "Provisions pour risques - Net N", "column": "net_n" }, "P06": { "value": 30000, "label": "Provisions pour risques - Net N-1", "column": "net_n_1" }, "D01": { "value": 200000, "label": "Emprunts obligataires convertibles - Net N", "column": "net_n" }, "D02": { "value": 220000, "label": "Emprunts obligataires convertibles - Net N-1", "column": "net_n_1" }, "E01": { "value": 85000, "label": "Dettes fournisseurs et comptes rattachés - Net N", "column": "net_n" }, "E02": { "value": 80000, "label": "Dettes fournisseurs et comptes rattachés - Net N-1", "column": "net_n_1" }, "E30": { "value": [ { "label": "Dettes sociales", "net_n": 25000, "net_n_1": 22000 }, { "label": "Dettes fiscales", "net_n": 18000, "net_n_1": 15000 } ], "label": "Autres dettes d'exploitation" }, "PTOT01": { "value": 1390000, "label": "TOTAL GENERAL PASSIF - Net N", "column": "net_n" }, "PTOT02": { "value": 1300000, "label": "TOTAL GENERAL PASSIF - Net N-1", "column": "net_n_1" }, "company_name": "SAS EXEMPLE COMMERCE", "siret": "12345678901234", "fiscal_year_end_n": "31/12/2023", "fiscal_year_end_n1": "31/12/2022" } ``` ### Field ID Naming Convention Field IDs follow a systematic pattern based on liability/equity category: | Prefix | Category | Example Fields | | --- | --- | --- | | C | Capital Social | C01-C06, C30 (Share capital) | | R | Réserves et Résultat | R07-R14, R30 (Reserves & result) | | N | Report à Nouveau | N11-N12 (Retained earnings) | | A | Autres Capitaux Propres | A15-A18, A30 (Other equity) | | P | Provisions pour Risques et Charges | P05-P08, P30 (Provisions) | | F | Autres Fonds Propres | F21-F24, F30 (Other equity instruments) | | D | Dettes Financières | D01-D14, D30 (Financial debts) | | E | Dettes d'Exploitation | E01-E04, E30 (Operating liabilities) | | Z | Dettes Diverses | Z01-Z04, Z30 (Other liabilities) | | Y | Comptes de Régularisation | Y01-Y04, Y30 (Accruals) | | PTOT | Total Général Passif | PTOT01-PTOT02 (Grand total) | ### Column Structure Each liability/equity line item has 2 columns representing different periods. Every scalar field carries a machine-readable `column` property naming the form column it belongs to (use it instead of parsing the label): * **`net_n`** (Net N): Net value for current fiscal year * **`net_n_1`** (Net N-1): Net value for previous fiscal year (for comparison) Dynamic "Autres" arrays encode the same columns as object keys (`net_n`, `net_n_1`) on each item. Metadata fields (`company_name`, dates, ...) have no `column` property. *Unlike the Actif (assets), the Passif reports only net amounts, with no gross or depreciation values.* ### Dynamic "Autres" Fields The schema includes dynamic "Autres" (Other) fields that can contain multiple line items. These fields are identified by their `"items": []` property in the schema and are returned as arrays with the following structure: **Array items are form rows, not cells:** each item carries one value per column, so the column tokens appear as the object keys (`net_n`, `net_n_1`) instead of a `column` property. The tokens are the same ones scalar fields expose in `column`. | Field Key | Category | Array Structure | | --- | --- | --- | | C30 | Autres Capital | Array of objects: `{item_label, net_n, net_n_1}` | | R30 | Autres Réserves | | A30 | Autres Capitaux Propres | | F30 | Autres Fonds Propres | | P30 | Autres Provisions | | D30 | Autres Dettes Financières | | E30 | Autres Dettes d'Exploitation | | Z30 | Autres Dettes Diverses | | Y30 | Autres Comptes de Régularisation | Each array item contains three properties: * **label**: Text description of the line item (string) * **net\_n**: Value for current fiscal year (numeric or null) * **net\_n\_1**: Value for previous fiscal year (numeric or null) Example array structure: ``` "E30": { "value": [ { "label": "Dettes sociales", "net_n": 25000, "net_n_1": 22000 }, { "label": "Dettes fiscales", "net_n": 18000, "net_n_1": 15000 } ], "label": "Autres dettes d'exploitation" } ``` ### Key Liability & Equity Categories Extracted | Section | Key Fields | Item Label | | --- | --- | --- | | Capital Social | C01-C02 | Capital social ou individuel | | C03-C04 | Primes d'émission, de fusion, d'apport | | C05-C06 | Écarts de réévaluation | | C30 | Autres capital (array) | | Réserves | R07-R08 | Réserve légale | | R09-R10 | Réserve statutaires ou contractuelles | | R11-R12 | Autres réserves | | R30 | Autres réserves (array) | | Report à Nouveau | N11-N12 | Report à nouveau | | Résultat de l'exercice | R13-R14 | Résultat de l'exercice | | Autres Capitaux Propres | A15-A16 | Subventions d'investissement | | A17-A18 | Provisions réglementées | | A30 | Autres capitaux propres (array) | | Autres Fonds Propres | F21-F22 | Produits des émissions de titres participatifs | | F23-F24 | Avances conditionnées | | F30 | Autres fonds propres (array) | | Provisions | P05-P06 | Provisions pour risques | | P07-P08 | Provisions pour charges | | P30 | Autres provisions (array) | | Dettes Financières | D01-D02 | Emprunts obligataires convertibles | | D03-D04 | Autres emprunts obligataires | | D05-D06 | Emprunts auprès d'établissements de crédit | | D07-D08 | Concours bancaires courants | | D09-D10 | Emprunts et dettes financières diverses | | D11-D12 | Avances et acomptes reçus sur commandes en cours | | D13-D14 | Autres dettes financières | | D30 | Autres dettes financières (array) | | Dettes d'Exploitation | E01-E02 | Dettes fournisseurs et comptes rattachés | | E03-E04 | Dettes fiscales et sociales | | E30 | Autres dettes d'exploitation (array) | | Dettes Diverses | Z01-Z02 | Dettes sur immobilisations et comptes rattachés | | Z03-Z04 | Autres dettes | | Z30 | Autres dettes diverses (array) | | Comptes de Régularisation | Y01-Y02 | Produits constatés d'avance | | Y03-Y04 | Écarts de conversion passif | | Y30 | Autres comptes de régularisation (array) | ### Understanding Liability & Equity Classification French accounting organizes the Passif by permanence and nature: * **Capitaux Propres**: Shareholders' equity - permanent capital contributed by owners and retained profits. This represents the company's net worth. * **Autres Fonds Propres**: Hybrid instruments between equity and debt (e.g., participatory securities, conditional advances). * **Provisions**: Reserves set aside for known or anticipated future liabilities and charges. * **Dettes**: Debts and payables - obligations to third parties that must be settled. Organized by type (financial, operating, other) and maturity. ### Metadata Fields The following metadata fields are also extracted: * **company\_name**: Legal name of the company * **company\_siret**: French business identification number (14 digits) * **company\_address**: Registered address of the company * **fiscal\_year\_end\_n**: End date of current fiscal year (DD/MM/YYYY) * **fiscal\_year\_end\_n1**: End date of previous fiscal year (DD/MM/YYYY) --- FR Compte de Résultat - Generic Income Statement ================================================ The **FR Compte de Résultat** is a generic French income statement template that extracts profit and loss data from various income statement formats. This form presents all revenues and expenses, organized by type (operating, financial, exceptional) to arrive at the net result. part\_type:`compte-resultat` **Note:** This is a generic schema that adapts to various French income statement formats. It extracts both current year (N) and previous year (N-1) values for comparison. ### Document Structure The FR Compte de Résultat is organized into the following major sections: * **Produits d'Exploitation**: Operating revenue + Ventes de marchandises (merchandise sales) + Production vendue de biens (goods production sold) + Production vendue de services (services production sold) + Chiffre d'affaires net (net revenue) + Other operating income (subsidies, reversals, etc.) * **Charges d'Exploitation**: Operating expenses + Achats et variation de stocks (purchases and inventory changes) + Charges externes (external charges) + Impôts et taxes (taxes) + Salaires et charges sociales (wages and social charges) + Dotations aux amortissements et provisions * **Résultat d'Exploitation**: Operating result (I - II) * **Produits Financiers**: Financial income * **Charges Financières**: Financial expenses * **Résultat Financier**: Financial result (V - VI) * **Résultat Courant avant Impôts**: Current result before taxes * **Produits Exceptionnels**: Exceptional income * **Charges Exceptionnelles**: Exceptional expenses * **Résultat Exceptionnel**: Exceptional result (VII - VIII) * **Participation et Impôts**: Employee profit-sharing and taxes * **Résultat Net**: Net result (profit or loss) ### FR Compte de Résultat Data Structure Within the `document_parts` array, the fr\_compte\_resultat `data` object contains the following structure: ``` { "PE01": { "value": 150000, "label": "Ventes de marchandises - France", "column": "france" }, "PE02": { "value": 50000, "label": "Ventes de marchandises - Exportation", "column": "export" }, "PE03": { "value": 200000, "label": "Ventes de marchandises - Total N", "column": "total_n" }, "PE04": { "value": 180000, "label": "Ventes de marchandises - N-1", "column": "n_1" }, "PE15": { "value": 500000, "label": "Chiffre d'affaires NET - Total N", "column": "total_n" }, "PE16": { "value": 450000, "label": "Chiffre d'affaires NET - N-1", "column": "n_1" }, "S01": { "value": 120000, "label": "Achats de marchandises - Total N", "column": "total_n" }, "S02": { "value": 110000, "label": "Achats de marchandises - N-1", "column": "n_1" }, "CE13": { "value": 80000, "label": "Salaires et traitements - Total N", "column": "total_n" }, "CE14": { "value": 75000, "label": "Salaires et traitements - N-1", "column": "n_1" }, "RE01": { "value": 45000, "label": "Résultat d'exploitation (I-II) - Total N", "column": "total_n" }, "RE02": { "value": 40000, "label": "Résultat d'exploitation (I-II) - N-1", "column": "n_1" }, "TR05": { "value": 35000, "label": "Bénéfice ou perte - Total N", "column": "total_n" }, "TR06": { "value": 30000, "label": "Bénéfice ou perte - N-1", "column": "n_1" }, "company_name": "SAS EXEMPLE COMMERCE", "company_siret": "12345678901234", "fiscal_year_end_n": "31/12/2023", "fiscal_year_end_n1": "31/12/2022" } ``` Repeatable "Autres" fields (e.g. `CE30`, `PE30`, `S30`, `CX30`) are returned as arrays of line items: ``` "CE30": [ { "label": "Loyer", "total_n": 80000, "n_1": null }, { "label": "Publicité", "total_n": 10000, "n_1": null } ] ``` **Array items are form rows, not cells:** each item carries one value per column, so the column tokens appear as the object keys (`total_n`, `n_1`) instead of a `column` property. The tokens are the same ones scalar fields expose in `column`. ### Field ID Naming Convention Field IDs follow a systematic pattern based on income/expense category: | Prefix | Category | Example Fields | | --- | --- | --- | | PE | Produits d'Exploitation | PE01-PE28 (Operating revenue) | | S | Achats et Stocks | S01-S08 (Purchases and inventory) | | CE | Charges Externes et Personnel | CE09-CE16 (External charges, wages) | | D | Dotations | D17-D22 (Depreciation/provisions) | | AU | Autres Charges | AU23-AU24 (Other charges) | | T | Total Charges d'Exploitation | T25-T26 (Total operating expenses) | | RE | Résultat d'Exploitation | RE01-RE02 (Operating result) | | QP | Quotes-Parts de Résultat | QP01-QP04 (Profit/loss transfers) | | PF | Produits Financiers | PF01-PF14 (Financial income) | | CF | Charges Financières | CF01-CF10 (Financial expenses) | | RF | Résultat Financier | RF01-RF02 (Financial result) | | RC | Résultat Courant | RC01-RC02 (Current result) | | PX | Produits Exceptionnels | PX01-PX08 (Exceptional income) | | CX | Charges Exceptionnelles | CX01-CX08 (Exceptional expenses) | | RX | Résultat Exceptionnel | RX01-RX02 (Exceptional result) | | PI | Participation et Impôts | PI01-PI04 (Profit-sharing, taxes) | | TR | Totaux et Résultats | TR01-TR06 (Totals, net result) | ### Column Structure Unlike the balance sheet which has 4 columns (Brut, Amort, Net N, Net N-1), the income statement has a simpler 2-column structure. Every scalar field carries a machine-readable `column` property naming the form column it belongs to (use it instead of parsing the label): * **`total_n`**: Value for the current fiscal year (Total N column) * **`n_1`**: Value for the previous fiscal year (N-1 column, for comparison) * **`france`** / **`export`**: France / Exportation breakdown of the revenue lines; both belong to the current fiscal year and sum to the matching `total_n` field Repeatable "Autres" rows (e.g. `CE30`, `PE30`) encode the same columns as object keys (`total_n`, `n_1`) on each item, so the vocabulary is identical across both shapes. Metadata fields (`company_name`, `fiscal_year_end_n`, ...) have no `column` property. The actual dates of exercice N and N-1 are given by `fiscal_year_end_n` and `fiscal_year_end_n1`. **Note on Revenue Breakdown:** Each revenue line (PE01-PE16) spans four fields: France (`column: "france"`), Exportation (`column: "export"`), Total for the current year (`column: "total_n"`), and the previous-year comparison (`column: "n_1"`). For example: PE13 (France) + PE14 (Export) = PE15 (Total N), and PE16 is the N-1 value of the same line. ### Key Field Categories Extracted | Section | Key Fields | Item Label | | --- | --- | --- | | Produits d'Exploitation (I) | PE01-PE04 | Ventes de marchandises (Merchandise sales) | | PE05-PE08 | Production vendue de biens (Goods production) | | PE09-PE12 | Production vendue de services (Services) | | PE13-PE16 | Chiffre d'affaires NET (Net revenue) | | PE17-PE18 | Production stockée (Inventory production) | | PE19-PE22 | Production immobilisée, Subventions (Capitalized production, subsidies) | | PE23-PE26 | Reprises, Autres produits (Reversals, other income) | | PE27-PE28 | Total des produits d'exploitation (Total I) | | Charges d'Exploitation (II) | S01-S04 | Achats de marchandises et variation de stock | | S05-S08 | Achats matières premières et variation de stock | | CE09-CE12 | Autres achats, charges externes, impôts et taxes | | CE13-CE16 | Salaires, traitements, charges sociales | | D17-D22 | Dotations aux amortissements et provisions | | AU23-AU24 | Autres charges | | T25-T26 | Total des charges d'exploitation (Total II) | | Résultat d'Exploitation | RE01-RE02 | 1. Résultat d'exploitation (I - II) | | Quotes-Parts | QP01-QP04 | Bénéfice/perte attribué ou transféré (III, IV) | | Produits Financiers (V) | PF01-PF04 | Produits de participations et valeurs mobilières | | PF05-PF08 | Autres intérêts, reprises sur provisions | | PF09-PF12 | Différences de change, cessions VMP | | PF13-PF14 | Total V | | Charges Financières (VI) | CF01-CF04 | Dotations, intérêts et charges assimilées | | CF05-CF08 | Différences de change, charges cessions VMP | | CF09-CF10 | Total VI | | Résultat Financier | RF01-RF02 | 2. Résultat financier (V - VI) | | Résultat Courant | RC01-RC02 | 3. Résultat courant avant impôts (I-II+III-IV+V-VI) | | Produits Exceptionnels (VII) | PX01-PX06 | Produits sur opérations de gestion, capital, reprises | | PX07-PX08 | Total VII | | Charges Exceptionnelles (VIII) | CX01-CX06 | Charges sur opérations de gestion, capital, dotations | | CX07-CX08 | Total VIII | | Résultat Exceptionnel | RX01-RX02 | 4. Résultat exceptionnel (VII - VIII) | | Participation et Impôts | PI01-PI04 | Participation des salariés (IX), Impôts sur bénéfices (X) | | Totaux et Résultats | TR01-TR02 | Total des produits (I+III+V+VII) | | TR03-TR04 | Total des charges (II+IV+VI+VIII+IX+X) | | TR05-TR06 | 5. Bénéfice ou perte (Total produits - Total charges) | ### Metadata Fields The following metadata fields are also extracted: * **company\_name**: Legal name of the company (Raison sociale) * **company\_address**: Registered address of the company (Adresse) * **company\_siret**: French business identification number (SIRET - 14 digits) * **fiscal\_year\_end\_n**: End date of current fiscal year (Exercice N clos le) * **fiscal\_year\_end\_n1**: End date of previous fiscal year (Exercice N-1 clos le) --- CERFA 2033 A - Simplified Balance Sheet ======================================= The **CERFA 2033 A** is a simplified balance sheet used by companies under the simplified tax regime (Régime Simplifié d'Imposition). This form presents the financial position of a company at the end of the fiscal year. part\_type:`CERFA-2033-A` **Versions supported:** SD2025, SD2024, SD2023 [2025 (SD2025) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2025/2033-sd_5015.pdf)[2024 (SD2024) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2024/2033-sd_4596.pdf)[2023 (SD2023) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2023/2033-sd_4223.pdf) ### Document Structure The CERFA 2033 A balance sheet is divided into two main sections: * **Actif (Assets)**: Resources owned or controlled by the company * **Passif (Liabilities & Equity)**: Sources of financing for the assets ### CERFA 2033 A Data Structure Within the `document_parts` array, the CERFA-2033-A `data` object contains the following fields with their official CERFA field IDs: ``` { "028": { "value": 60571, "label": "Immobilisations corporelles (Brut)", "column": "value_brut" }, "030": { "value": 34551, "label": "Immobilisations corporelles (Amortissements)", "column": "value_amort_prov" }, "028_net_n": { "value": 26024, "label": "Immobilisations corporelles (Net N)", "column": "net_n" }, "028_net_n1": { "value": 36551, "label": "Immobilisations corporelles (Net N-1)", "column": "net_n_1" }, "040": { "value": 1850, "label": "Immobilisations financières (Brut)", "column": "value_brut" }, "040_net_n": { "value": 1850, "label": "Immobilisations financières (Net N)", "column": "net_n" }, "044": { "value": 62425, "label": "Total I (Brut)", "column": "value_brut" }, "048": { "value": 34551, "label": "Total I (Amortissements)", "column": "value_amort_prov" }, "044_net_n": { "value": 27874, "label": "Total I (Net N)", "column": "net_n" }, "072": { "value": 1008, "label": "Autres créances (Brut)", "column": "value_brut" }, "072_net_n": { "value": 1008, "label": "Autres créances (Net N)", "column": "net_n" }, "092": { "value": 230, "label": "Charges constatées d'avance (Brut)", "column": "value_brut" }, "096": { "value": 1238, "label": "Total II (Brut)", "column": "value_brut" }, "110": { "value": 63663, "label": "Total général actif (Brut)", "column": "value_brut" }, "110_net_n": { "value": 29112, "label": "Total général actif (Net N)", "column": "net_n" }, "120": { "value": 5000, "label": "Capital social (Net)" }, "120_n1": { "value": 5000, "label": "Capital social (Net N-1)", "column": "net_n_1" }, "126": { "value": 500, "label": "Réserve légale (Net)" }, "136": { "value": -4930, "label": "Résultat de l'exercice (Net)" }, "142": { "value": -345, "label": "Total I capitaux propres (Net)" }, "156": { "value": 8015, "label": "Emprunts (Net)" }, "166": { "value": 3084, "label": "Fournisseurs (Net)" }, "169": { "value": 3250, "label": "TVA" }, "172": { "value": 6014, "label": "Dettes fiscales et sociales (Net)" }, "176": { "value": 29456, "label": "Total III dettes (Net)" }, "180": { "value": 29112, "label": "Total général passif (Net)" }, "company_name": "EURL ATOUS PORT FITNESS", "company_siret": "87924425900013", "fiscal_year_end_n": "31/12/2023", "fiscal_year_end_n1": "31/12/2022" } ``` ### Data Structure Notes Key points about the response structure: * **Wrapper Format**: Each CERFA form is wrapped in the `document_parts` array with document-level metadata (extraction\_id, document\_name, extraction\_date) * **Multiple Forms**: A single liasse fiscale can contain multiple CERFA forms (e.g., 2033-A, 2033-B, 2033-C), each appearing as a separate item in the `document_parts` array * **Validation Status**: Each sub-document has its own `validation_status` indicating whether validation passed (OK), has soft issues (WARNING), or detected errors (ERROR) * **Page Information**: The `pages` array shows which pages of the original PDF contain this CERFA form The CERFA 2033 A data structure includes: * **Flat Field IDs**: Each field uses the official CERFA field ID (e.g., "028", "120", "169") * **Value/Label/Column Objects**: Each field contains `value` (numeric amount), `label` (human-readable description), and `column`, a machine-readable token naming the form column the value belongs to (`value_brut`, `value_amort_prov`, `net_n`, `net_n_1`). Use `column` to tell exercice N from N-1 instead of parsing labels or key suffixes. * **Column Suffixes** (key naming only; prefer the `column` property): + No suffix = Brut (gross) for ACTIF fields, Net N for PASSIF fields + `_net_n` = Net value for current year (ACTIF fields) + `_net_n1` = Net value for previous year (ACTIF fields) + `_n1` = Previous year value (PASSIF fields) + Special field IDs for amortissements (e.g., "030" for field "028") * **Metadata Fields**: Company information stored as flat strings (company\_name, company\_siret, fiscal\_year\_end\_n, etc.) * **Version**: The CERFA form version (e.g., "2024" for SD2024) * **Pages**: Array of page numbers where this form was found in the document ### Key Fields Extracted | Section | Field ID | Description | Columns Available | | --- | --- | --- | --- | | Actif - Immobilisations | 010 | Fonds commercial | brut, amortissements (012), net\_n, net\_n1 | | 014 | Autres immobilisations incorporelles | brut, amortissements (016), net\_n, net\_n1 | | 028 | Immobilisations corporelles | brut, amortissements (030), net\_n, net\_n1 | | 040 | Immobilisations financières | brut, amortissements (042), net\_n, net\_n1 | | Actif - Actif Circulant | 050 | Matières premières | brut, amortissements (052), net\_n, net\_n1 | | 060 | Marchandises | brut, amortissements (062), net\_n, net\_n1 | | 064 | Avances et acomptes versés sur commandes | brut, amortissements (066), net\_n, net\_n1 | | 068 | Clients et comptes rattachés | brut, amortissements (070), net\_n, net\_n1 | | 072 | Autres créances | brut, amortissements (074), net\_n, net\_n1 | | 080 | Valeurs mobilières | brut, amortissements (082), net\_n, net\_n1 | | 084 | Disponibilités | brut, amortissements (086), net\_n, net\_n1 | | Actif - Totaux | 044 | Total I (Immobilisations) | brut, amortissements (048), net\_n, net\_n1 | | 096 | Total II (Actif Circulant) | brut, amortissements (098), net\_n, net\_n1 | | 110 | Total général actif | brut, amortissements (112), net\_n, net\_n1 | | Passif - Capitaux Propres | 120 | Capital social | net, n1 | | 124 | Écarts de réévaluation | net, n1 | | 126 | Réserve légale | net, n1 | | 130 | Réserves réglementées | net, n1 | | 132 | Autres réserves | net, n1 | | 134 | Report à nouveau | net, n1 | | 136 | Résultat de l'exercice | net, n1 | | Passif - Dettes | 156 | Emprunts | net, n1 | | 164 | Avances et acomptes | net, n1 | | 166 | Fournisseurs | net, n1 | | 169 | TVA | amount | | 172 | Dettes fiscales et sociales | net, n1 | | 173 | Compte courant d'associés **Version SD2023 only:** This field was removed in SD2024 and later versions. | net, n1 | | Passif - Totaux | 142 | Total I capitaux propres | net, n1 | | 176 | Total III dettes | net, n1 | | 180 | Total général passif | net, n1 | | Metadata | company\_name | Raison sociale | string | | company\_siret | SIRET | string | | fiscal\_year\_end\_n | Exercice N clos le | string (DD/MM/YYYY) | | fiscal\_year\_end\_n1 | Exercice N-1 clos le | string (DD/MM/YYYY) | ### Validation Rules The extraction applies the following validation rules to CERFA 2033 A data: * **Balance Verification**: Field `110_net_n` (Total général actif) must equal field `180` (Total général passif) * **Net Values for ACTIF**: For each asset field, `field_net_n = field (brut) - amortissements_field`. Example: `028_net_n = 028 - 030` * **Field ID Validation**: All field IDs must match the official CERFA 2033-A field definitions * **Column Type Validation**: Each field can only have specific column types (e.g., ACTIF fields have brut/amortissements/net\_n/net\_n1, PASSIF fields have net/n1) * **Value/Label Structure**: Each field (except metadata) must contain both `value` (number) and `label` (string) ### Field Naming Convention The flat structure uses specific naming patterns based on the field type: | Field Type | Key Format | Example | | --- | --- | --- | | ACTIF - Brut | {field\_id} | "028" | | ACTIF - Amortissements | {field\_id + 2} | "030" (for field 028) | | ACTIF - Net N | {field\_id}\_net\_n | "028\_net\_n" | | ACTIF - Net N-1 | {field\_id}\_net\_n1 | "028\_net\_n1" | | PASSIF - Net N | {field\_id} | "120" | | PASSIF - Net N-1 | {field\_id}\_n1 | "120\_n1" | | Special amounts (e.g., TVA) | {field\_id} | "169" | | Metadata | {field\_name} | "company\_name" | --- CERFA 2033 B - Simplified Income Statement ========================================== The **CERFA 2033 B** is a simplified income statement (Compte de Résultat Simplifié) used by companies under the simplified tax regime (Régime Simplifié d'Imposition). This form presents the operating results and fiscal profit/loss for the fiscal year. part\_type:`CERFA-2033-B` **Versions supported:** SD2025, SD2024, SD2023 [2025 (SD2025) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2025/2033-sd_5015.pdf)[2024 (SD2024) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2024/2033-sd_4596.pdf)[2023 (SD2023) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2023/2033-sd_4223.pdf) ### Document Structure The CERFA 2033 B income statement is divided into main sections: * **Produits d'exploitation**: Operating income (sales, production, subsidies) * **Charges d'exploitation**: Operating expenses (purchases, salaries, depreciation) * **Résultat d'exploitation**: Operating result (profit/loss from operations) * **Produits et charges financiers**: Financial income and expenses * **Produits et charges exceptionnels**: Exceptional income and expenses * **Résultat fiscal**: Tax result with adjustments, add-backs, and deductions ### CERFA 2033 B Data Structure ``` { "218": { "value": 74072, "label": "Production vendue - Services", "column": "amount" }, "226": { "value": 5167, "label": "Subventions d'exploitation reçues", "column": "amount" }, "230": { "value": 1, "label": "Autres produits", "column": "amount" }, "232": { "value": 79240, "label": "Total des produits d'exploitation (I)", "column": "amount" }, "242": { "value": 64599, "label": "Autres charges externes", "column": "amount" }, "244": { "value": 517, "label": "Impôts, taxes et versements assimilés", "column": "amount" }, "250": { "value": 6324, "label": "Rémunérations du personnel", "column": "amount" }, "252": { "value": 1267, "label": "Charges sociales", "column": "amount" }, "254": { "value": 10527, "label": "Dotations aux amortissements", "column": "amount" }, "264": { "value": 83285, "label": "Total des charges d'exploitation (II)", "column": "amount" }, "270": { "value": -4045, "label": "Résultat d'exploitation (I-II)", "column": "amount" }, "310": { "value": -4930, "label": "Bénéfice ou perte", "column": "amount" }, "314": { "value": 4930, "label": "Résultat fiscal - Déficit", "column": "amount" }, "DCM": { "value": 123, "label": "dont crédit-bail mobilier", "column": "amount" }, "DCI": { "value": 0, "label": "dont crédit-bail immobilier", "column": "amount" }, "218_n1": { "value": 55681, "label": "Production vendue - Services (Net N-1)", "column": "n_1" }, "232_n1": { "value": 63112, "label": "Total des produits d'exploitation (I) (Net N-1)", "column": "n_1" }, "company_name": "EURL ATOUSPORT FITNESS", "fiscal_year_end_n": "31122023", "fiscal_year_end_n1": "31122022" } ``` ### Data Structure Notes The CERFA 2033 B response format includes: * **Flat Field IDs**: Each field uses the official CERFA field ID (e.g., "210", "242", "310") * **Letter-Code Fields**: Some fields use 3-letter codes (e.g., "DCM" for crédit-bail mobilier, "DCI" for crédit-bail immobilier) * **Value/Label/Column Objects**: Each field contains `value` (numeric amount), `label` (human-readable description), and `column`, a machine-readable token naming the form column the value belongs to (`amount` = current exercice, `n_1` = previous exercice). Use `column` to tell exercice N from N-1 instead of parsing labels. * **Column Suffixes**: + No suffix = Current year (N) value + `_n1` = Previous year (N-1) value * **Metadata Fields**: Company information stored as flat values (company\_name, fiscal\_year\_end\_n, fiscal\_year\_end\_n1) * **Version**: The CERFA form version (e.g., "2024" for SD2024) * **Pages**: Array of page numbers where this form was found ### Key Fields Extracted | Section | Field ID | Description | Columns Available | | --- | --- | --- | --- | | Produits d'exploitation | 210 | Ventes de marchandises | amount, n1 | | 209 | dont export et livraisons intracommunautaires | amount, n1 | | 214 | Production vendue - Biens | amount, n1 | | 218 | Production vendue - Services | amount, n1 | | 222 | Production stockée | amount, n1 | | 226 | Subventions d'exploitation reçues | amount, n1 | | 230 | Autres produits | amount, n1 | | 232 | Total des produits d'exploitation (I) | amount, n1 | | Charges d'exploitation | 234 | Achats de marchandises | amount, n1 | | 240 | Variation de stock (matières premières) | amount, n1 | | 242 | Autres charges externes | amount, n1 | | DCM | dont crédit-bail mobilier | amount | | DCI | dont crédit-bail immobilier | amount | | 244 | Impôts, taxes et versements assimilés | amount, n1 | | 243 | dont taxe professionnelle CFE et CVAE | amount, n1 | | 250 | Rémunérations du personnel | amount, n1 | | 252 | Charges sociales | amount, n1 | | 254 | Dotations aux amortissements | amount, n1 | | 255 | Dont amortissement du fonds de commerce **Version SD2023 only:** This field was removed in SD2024 and later versions. | amount | | 256 | Dotations aux provisions | amount, n1 | | 262 | Autres charges | amount, n1 | | 264 | Total des charges d'exploitation (II) | amount, n1 | | Résultats | 270 | Résultat d'exploitation (I-II) | amount, n1 | | 280 | Produits financiers (III) | amount, n1 | | 290 | Produits exceptionnels (IV) | amount, n1 | | 294 | Charges financières (V) | amount, n1 | | 300 | Charges exceptionnelles (VI) | amount, n1 | | 306 | Impôts sur les bénéfices (VII) | amount, n1 | | 310 | Bénéfice ou perte | amount, n1 | | Résultat fiscal | 312 | Résultat fiscal - Bénéfice | amount | | 314 | Résultat fiscal - Déficit | amount | | 352 | Résultat avant déficits - Bénéfice | amount | | 370 | Résultat après déficits - Bénéfice | amount | | Metadata | company\_name | Raison sociale | string | | fiscal\_year\_end\_n | Exercice N clos le | string (DDMMYYYY) | | fiscal\_year\_end\_n1 | Exercice N-1 clos le | string (DDMMYYYY) | ### Validation Rules The extraction applies the following validation rules to CERFA 2033 B data: * **PRODUITS D'EXPLOITATION Total**: Field `232` should equal the sum of fields 210, 214, 218, 222, 224, 226, 230 * **CHARGES D'EXPLOITATION Total**: Field `264` should equal the sum of fields 234, 236, 238, 240, 242, 244, 250, 252, 254, 256, 262 * **Résultat d'exploitation**: Field `270` should equal `232 - 264` * **Bénéfice ou perte**: Field `310` should equal `270 + 280 + 290 - 294 - 300 - 306` * **Letter-Code Fields**: Fields DCM, DCI use 3-letter uppercase codes instead of numeric IDs * **Tolerance**: Calculations allow ±2 euro tolerance for rounding differences ### Field Naming Convention The flat structure uses specific naming patterns: | Field Type | Key Format | Example | | --- | --- | --- | | Numeric Field - Current Year (N) | {field\_id} | "210", "242", "310" | | Numeric Field - Previous Year (N-1) | {field\_id}\_n1 | "210\_n1", "242\_n1" | | Letter-Code Field | {3-letter code} | "DCM", "DCI", "DER" | | Metadata | {field\_name} | "company\_name" | --- CERFA 2033 C - Fixed Assets and Depreciation ============================================ The **CERFA 2033 C** (Tableau des immobilisations et des amortissements) tracks the evolution of fixed assets (immobilisations) and their depreciation (amortissements) throughout the fiscal year. This form is used by companies under the simplified tax regime to detail asset acquisitions, disposals, and depreciation calculations. part\_type:`CERFA-2033-C` **Versions supported:** SD2025, SD2024, SD2023 [2025 (SD2025) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2025/2033-sd_5015.pdf)[2024 (SD2024) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2024/2033-sd_4596.pdf)[2023 (SD2023) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2023/2033-sd_4223.pdf) ### Document Structure The CERFA 2033 C has three main sections: * **Immobilisations**: Tracking of fixed assets with 4 columns (Beginning, Increases, Decreases, End of period) * **Amortissements**: Tracking of accumulated depreciation with 4 columns (Beginning, Increases, Decreases, End of period) * **Plus-values et Moins-values**: Capital gains/losses on asset disposals with detailed calculations ### CERFA 2033 C Data Structure Within the `document_parts` array, the CERFA-2033-C `data` object contains the following fields: ``` { "400": { "value": 15000, "label": "Immobilisation incorporelles - Fonds commercial (Début exercice)", "column": "amount" }, "410": { "value": 25000, "label": "Immobilisation incorporelles - Autres (Début exercice)", "column": "amount" }, "430": { "value": 120000, "label": "Constructions (Début exercice)", "column": "amount" }, "432": { "value": 50000, "label": "Constructions (Augmentations)", "column": "amount" }, "436": { "value": 170000, "label": "Constructions (Fin exercice)", "column": "amount" }, "440": { "value": 85000, "label": "Installations techniques, matériel et outillage industriels (Début exercice)", "column": "amount" }, "442": { "value": 12000, "label": "Installations techniques, matériel et outillage industriels (Augmentations)", "column": "amount" }, "446": { "value": 97000, "label": "Installations techniques, matériel et outillage industriels (Fin exercice)", "column": "amount" }, "495": { "value": 5000, "label": "Fonds commercial (Amortissements - Début exercice)", "column": "amount" }, "497": { "value": 1500, "label": "Fonds commercial (Amortissements - Augmentations)", "column": "amount" }, "499": { "value": 6500, "label": "Fonds commercial (Amortissements - Fin exercice)", "column": "amount" }, "520": { "value": 45000, "label": "Constructions (Amortissements - Début exercice)", "column": "amount" }, "522": { "value": 8500, "label": "Constructions (Amortissements - Augmentations)", "column": "amount" }, "526": { "value": 53500, "label": "Constructions (Amortissements - Fin exercice)", "column": "amount" }, "company_name": "SARL EXEMPLE", "siret": "12345678900012", "current_year_end": "31/12/2023", "previous_year_end": "31/12/2022" } ``` ### Key Field Categories The form tracks various asset categories with their movements: | Asset Category | Field ID Range | Columns | | --- | --- | --- | | Fonds commercial | 400, 402, 404, 406 | Début, Augmentations, Diminutions, Fin | | Autres immobilisations incorporelles | 410, 412, 414, 416 | Début, Augmentations, Diminutions, Fin | | Terrains | 420, 422, 424, 426 | Début, Augmentations, Diminutions, Fin | | Constructions | 430, 432, 434, 436 | Début, Augmentations, Diminutions, Fin | | Installations techniques | 440, 442, 444, 446 | Début, Augmentations, Diminutions, Fin | | Matériel de transport | 460, 462, 464, 466 | Début, Augmentations, Diminutions, Fin | ### Column Structure Each asset category has 4 movement columns: * **Début exercice (col 1)**: Asset value at beginning of fiscal year * **Augmentations (col 2)**: Acquisitions during the year * **Diminutions (col 3)**: Disposals during the year * **Fin exercice (col 4)**: Asset value at end of fiscal year (Beginning + Increases - Decreases) The same structure applies to accumulated depreciation (amortissements) tracked separately. Every scalar numeric field carries a machine-readable `column` property naming the form column it belongs to (`amount`: this form reports a single exercice, so every value belongs to the current fiscal year). Use it instead of parsing labels. Metadata fields (company info, dates) have no `column` property. --- CERFA 2033 D - Provisions and Loss Carryforwards ================================================ The **CERFA 2033 D** (Relevé des provisions, amortissements dérogatoires, déficits reportables) details the movement of provisions and accelerated depreciation over the fiscal year, together with tax loss carryforwards and miscellaneous disclosures. It is filed by companies under the simplified tax regime. part\_type:`CERFA-2033-D` **Versions supported:** SD2026, SD2025, SD2024, SD2023 [2025 (SD2025) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2025/2033-sd_5015.pdf)[2024 (SD2024) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2024/2033-sd_4596.pdf)[2023 (SD2023) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2023/2033-sd_4223.pdf) ### Document Structure The CERFA 2033 D has four main sections: * **I.A - Nature des provisions**: Provisions tracked across 4 columns (Début, Augmentations : dotations, Diminutions : reprises, Fin) * **I.B - Mouvements sur amortissements dérogatoires**: Accelerated depreciation movements across 2 columns (Dotations, Reprises) * **I.C / II - Ventilation et déficits reportables**: Provision breakdown and tax loss carryforwards * **III / IV - Divers et travailleurs indépendants**: Miscellaneous disclosures, and (SD2026 onward) the self-employed gross social income section ### CERFA 2033 D Data Structure Within the `document_parts` array, the CERFA-2033-D `data` object contains the following fields: ``` { "600": { "value": 12000, "label": "Amortissements dérogatoires - début", "column": "amount" }, "602": { "value": 3500, "label": "Amortissements dérogatoires - dotations", "column": "amount" }, "604": { "value": 1200, "label": "Amortissements dérogatoires - reprises", "column": "amount" }, "606": { "value": 14300, "label": "Amortissements dérogatoires - fin", "column": "amount" }, "620": { "value": 8000, "label": "Provisions pour risques et charges - début", "column": "amount" }, "650": { "value": 4500, "label": "Dépréciation sur comptes clients - début", "column": "amount" }, "680": { "value": 24500, "label": "TOTAL provisions - début", "column": "amount" }, "760": { "value": 2000, "label": "Amortissements dérogatoires - dotations (mouvements)", "column": "amount" }, "982": { "value": 18000, "label": "Déficits restant à reporter au titre de l'exercice précédent", "column": "amount" }, "company_name": "SARL EXEMPLE" } ``` ### Key Field Categories Each provision line carries a distinct field code per column, so the code alone identifies both the row and the column: | Provision Category | Field IDs | Columns | | --- | --- | --- | | Amortissements dérogatoires | 600, 602, 604, 606 | Début, Dotations, Reprises, Fin | | Autres provisions réglementées | 610, 612, 614, 616 | Début, Dotations, Reprises, Fin | | Provisions pour risques et charges | 620, 622, 624, 626 | Début, Dotations, Reprises, Fin | | Dépréciation sur comptes clients | 650, 652, 654, 656 | Début, Dotations, Reprises, Fin | | TOTAL provisions | 680, 682, 684, 686 | Début, Dotations, Reprises, Fin | | Déficits reportables | 982, 982\_bis, 982\_ter | Single value | | Travailleurs indépendants\* | 690, 691, 692, 693 | Single value | \* The travailleurs indépendants section (revenu brut social) was introduced on the SD2026 edition and is absent from earlier years. ### Consistency Checks The extraction validates each column total of section I.A (fields 680, 682, 684, 686), the roll-forward identity (fin = début + dotations − reprises), and both column totals of section I.B. The "Dont majorations exceptionnelles de 30 %" lines (601, 603, 605, 607) are sub-lines of the amortissements dérogatoires rows and are deliberately excluded from the totals. Field `982_ter` is a count (Nombre d'opérations), not a monetary amount. Every scalar numeric field carries a machine-readable `column` property naming the form column it belongs to (`amount`: each field code is unique per row and column, so a single value column suffices). Use it instead of parsing labels. Metadata fields (company info) have no `column` property. --- CERFA 2033 E - Added Value Determination (CVAE) =============================================== The **CERFA 2033 E** (Détermination de la valeur ajoutée produite) computes the added value produced by the company for CVAE purposes. This form is used by companies under the simplified tax regime and reconciles revenue, other operating income and deductible charges into the taxable added value, along with a workforce declaration and a mono-établissement section. part\_type:`CERFA-2033-E` **Versions supported:** SD2026, SD2025, SD2024, SD2023 [2025 (SD2025) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2025/2033-sd_5015.pdf)[2024 (SD2024) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2024/2033-sd_4596.pdf)[2023 (SD2023) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2033-sd/2023/2033-sd_4223.pdf) ### Document Structure The CERFA 2033 E has five main sections plus a workforce declaration: * **Déclaration des effectifs**: Average workforce headcount (apprentices, disabled workers, artisanal activity) * **I - Chiffre d'affaires de référence CVAE**: Reference revenue (TOTAL 1, field 106) * **II - Autres produits**: Other income to include (TOTAL 2, field 144) * **III - Charges**: Deductible charges (TOTAL 3, field 152) * **IV / V - Valeur ajoutée**: Added value produced (field 137 = TOTAL 1 + TOTAL 2 - TOTAL 3) and the amount subject to CVAE (field 117) * **Mono-établissement**: CVAE single-establishment declaration (checkbox 020, reference revenue 022, headcount 023) ### CERFA 2033 E Data Structure Within the `document_parts` array, the CERFA-2033-E `data` object contains the following fields: ``` { "376": { "value": 25, "label": "Effectif moyen du personnel", "column": "amount" }, "108": { "value": 850000, "label": "Ventes de produits fabriqués, prestations de services et marchandises", "column": "amount" }, "106": { "value": 850000, "label": "TOTAL 1 - Chiffre d'affaires de référence CVAE", "column": "amount" }, "115": { "value": 17000, "label": "Autres produits de gestion courante", "column": "amount" }, "144": { "value": 17000, "label": "TOTAL 2 - Autres produits", "column": "amount" }, "121": { "value": 250000, "label": "Achats", "column": "amount" }, "125": { "value": 110000, "label": "Services extérieurs, à l'exception des loyers et des redevances", "column": "amount" }, "152": { "value": 437500, "label": "TOTAL 3 - Charges", "column": "amount" }, "137": { "value": 429500, "label": "Valeur ajoutée produite (Total 1 + Total 2 - Total 3)", "column": "amount" }, "117": { "value": 429500, "label": "Valeur ajoutée assujettie à la CVAE", "column": "amount" }, "020": { "value": "1", "label": "Mono-établissement au sens de la CVAE (case à cocher)", "column": "amount" }, "company_name": "SARL EXEMPLE", "exercice_ouvert": "01/01/2026", "exercice_clos": "31/12/2026", "nombre_mois": 12 } ``` ### Key Field Categories The form aggregates income and charges into the added value calculation: | Section | Field IDs | Total | | --- | --- | --- | | Déclaration des effectifs | 376, 657, 651, 861 | — | | I - Chiffre d'affaires de référence CVAE | 108, 118, 112 | 106 (TOTAL 1) | | II - Autres produits | 115, 143, 113, 111, 153, 116\* | 144 (TOTAL 2) | | III - Charges | 121, 145, 125, 310, 133, 148, 128, 135, 150 | 152 (TOTAL 3) | | IV/V - Valeur ajoutée | 137, 117 | 137 = 106 + 144 - 152 | | Mono-établissement (CVAE) | 020, 022, 023, 026, 024, 016 | — | \* Field 116 (Transferts de charges déductibles) exists on the SD2023-SD2025 editions only; the SD2026 form removed it. ### Consistency Checks The extraction validates the arithmetic of each section: TOTAL 1 (106), TOTAL 2 (144), TOTAL 3 (152), and the added value produced (137 = 106 + 144 - 152). The added value can legitimately be negative. Every scalar numeric field carries a machine-readable `column` property naming the form column it belongs to (`amount`: this form has a single value column, so every value belongs to the declared exercice). Use it instead of parsing labels. Metadata fields (company info, dates) have no `column` property. --- CERFA 2050 - Balance Sheet Assets (Normal Regime) ================================================= The **CERFA 2050** (Actif du Bilan) is the detailed balance sheet assets form used by companies under the normal tax regime (Régime Réel Normal). It reports every asset line with its gross value, accumulated depreciation/provisions and net book value. part\_type:`CERFA-2050` **Versions supported:** 2025, 2024, 2023 [2025 (5013) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2050-liasse/2025/2050-liasse_5013.pdf)[2024 (4594) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2050-liasse/2024/2050-liasse_4594.pdf)[2023 (4225) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2050-liasse/2023/2050-liasse_4225.pdf) ### Document Structure The CERFA 2050 balance sheet assets section includes: * **Immobilisations incorporelles**: Intangible fixed assets (goodwill, patents, software, etc.) * **Immobilisations corporelles**: Tangible fixed assets (land, buildings, equipment, etc.) * **Immobilisations financières**: Financial fixed assets (investments, loans, securities) * **Stocks**: Inventory (raw materials, work in progress, finished goods, merchandise) * **Créances**: Receivables (customer accounts, other receivables) * **Disponibilités**: Cash and marketable securities ### CERFA 2050 Data Structure Within the `document_parts` array, the fr\_cerfa\_2050\_2024 `data` object contains the following fields: ``` { "AH": { "value": 50000, "label": "Fonds commercial (Brut)", "column": "value_brut" }, "AI": { "value": 5000, "label": "Fonds commercial (Amortissements)", "column": "value_amort_prov" }, "AH_n": { "value": 45000, "label": "Fonds commercial (Net)", "column": "net_n" }, "AN": { "value": 200000, "label": "Terrains (Brut)", "column": "value_brut" }, "AO": { "value": 0, "label": "Terrains (Amortissements)", "column": "value_amort_prov" }, "AN_n": { "value": 200000, "label": "Terrains (Net)", "column": "net_n" }, "AP": { "value": 450000, "label": "Constructions (Brut)", "column": "value_brut" }, "AQ": { "value": 125000, "label": "Constructions (Amortissements)", "column": "value_amort_prov" }, "AP_n": { "value": 325000, "label": "Constructions (Net)", "column": "net_n" }, "AR": { "value": 180000, "label": "Installations techniques, matériel et outillage industriels (Brut)", "column": "value_brut" }, "AS": { "value": 85000, "label": "Installations techniques, matériel et outillage industriels (Amortissements)", "column": "value_amort_prov" }, "AR_n": { "value": 95000, "label": "Installations techniques, matériel et outillage industriels (Net)", "column": "net_n" }, "BX": { "value": 125000, "label": "Clients et comptes rattachés (Brut)", "column": "value_brut" }, "BY": { "value": 5000, "label": "Clients et comptes rattachés (Provisions)" }, "BX_n": { "value": 120000, "label": "Clients et comptes rattachés (Net)", "column": "net_n" }, "CF": { "value": 45000, "label": "Disponibilités (Brut)", "column": "value_brut" }, "CG": { "value": 0, "label": "Disponibilités (Amortissements)", "column": "value_amort_prov" }, "CF_n": { "value": 45000, "label": "Disponibilités (Net)", "column": "net_n" }, "CO": { "value": 1250000, "label": "TOTAL GÉNÉRAL (Brut)", "column": "value_brut" }, "1A": { "value": 220000, "label": "TOTAL GÉNÉRAL (Amortissements, provisions)" }, "CO_n": { "value": 1030000, "label": "TOTAL GÉNÉRAL (Net)", "column": "net_n" }, "company_name": "SAS EXEMPLE INDUSTRIE", "siret": "98765432100015", "exercise_end_date": "31/12/2023" } ``` ### Column Structure Each asset line item has three columns: * **Brut (col1)**: Gross book value of the asset * **Amortissements, provisions (col2)**: Accumulated depreciation or provisions * **Net (col3)**: Net book value = Brut - Amortissements/provisions Every scalar numeric field carries a machine-readable `column` property naming the form column it belongs to (`value_brut`, `value_amort_prov`, `net_n`; all columns of this form belong to the current exercice; no N-1 values are extracted). Use it instead of parsing labels. Metadata fields (company info, dates) have no `column` property. ### Key Fields Extracted | Section | Field ID | Description | Columns | | --- | --- | --- | --- | | Immobilisations incorporelles | AH | Fonds commercial | Brut, Amort (AI), Net | | AF | Concessions, brevets et droits similaires | Brut, Amort (AG), Net | | AJ | Autres immobilisations incorporelles | Brut, Amort (AK), Net | | Immobilisations corporelles | AN | Terrains | Brut, Amort (AO), Net | | AP | Constructions | Brut, Amort (AQ), Net | | AR | Installations techniques, matériel et outillage industriels | Brut, Amort (AS), Net | | Actif circulant | BX | Clients et comptes rattachés | Brut, Prov (BY), Net | | CD | Valeurs mobilières de placement | Brut, Prov (CE), Net | | CF | Disponibilités | Brut, Prov (CG), Net | | Total général | CO | TOTAL GÉNÉRAL (I à VI) | Brut, Amort/Prov (1A), Net | ### Field Naming Convention Net values (column 3, current exercice) are stored with `_n` suffix: * **Brut**: Field ID (e.g., "AN", "AP", "BX") * **Amort/Prov**: Field ID + 1 letter (e.g., "AO", "AQ", "BY") * **Net**: Field ID + "\_n" (e.g., "AN\_n", "AP\_n", "BX\_n") --- CERFA 2051 - Balance Sheet Liabilities (Normal Regime) ====================================================== The **CERFA 2051** (Passif du Bilan) is the detailed balance sheet liabilities form used by companies under the normal tax regime. This form presents the company's equity, provisions, and debts, showing how assets are financed. part\_type:`CERFA-2051` **Versions supported:** 2025, 2024, 2023 [2025 (5013) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2050-liasse/2025/2050-liasse_5013.pdf)[2024 (4594) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2050-liasse/2024/2050-liasse_4594.pdf)[2023 (4225) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2050-liasse/2023/2050-liasse_4225.pdf) ### Document Structure The CERFA 2051 balance sheet liabilities section includes: * **Capitaux propres (I)**: Shareholders' equity (capital, reserves, retained earnings, current year result) * **Autres fonds propres (II)**: Other equity (participatory securities, conditional advances) * **Provisions pour risques et charges (III)**: Provisions for risks and contingencies * **Dettes (IV)**: Liabilities (bonds, bank loans, supplier debts, tax & social debts) * **Écarts de conversion passif (V)**: Foreign exchange translation differences ### CERFA 2051 Data Structure Within the `document_parts` array, the fr\_cerfa\_2051\_2024 `data` object contains the following fields: ``` { "DA": { "value": 100000, "label": "Capital social ou individuel", "column": "amount" }, "DA_verse": { "value": 100000, "label": "Capital social ou individuel (Dont versé)", "column": "amount" }, "DD": { "value": 10000, "label": "Réserve légale", "column": "amount" }, "DE": { "value": 15000, "label": "Réserves statutaires ou contractuelles", "column": "amount" }, "DF": { "value": 8000, "label": "Réserves réglementées", "column": "amount" }, "DG": { "value": 25000, "label": "Autres réserves", "column": "amount" }, "DH": { "value": 12000, "label": "Report à nouveau", "column": "amount" }, "DI": { "value": 45000, "label": "RÉSULTAT DE L'EXERCICE (bénéfice ou perte)", "column": "amount" }, "DJ": { "value": 5000, "label": "Subventions d'investissement", "column": "amount" }, "DK": { "value": 3000, "label": "Provisions réglementées", "column": "amount" }, "DL": { "value": 223000, "label": "TOTAL (I) - Capitaux propres", "column": "amount" }, "DP": { "value": 8000, "label": "Provisions pour risques", "column": "amount" }, "DQ": { "value": 12000, "label": "Provisions pour charges", "column": "amount" }, "DR": { "value": 20000, "label": "TOTAL (III) - Provisions pour risques et charges", "column": "amount" }, "DU": { "value": 450000, "label": "Emprunts et dettes auprès des établissements de crédit", "column": "amount" }, "DX": { "value": 85000, "label": "Dettes fournisseurs et comptes rattachés", "column": "amount" }, "DY": { "value": 42000, "label": "Dettes fiscales et sociales", "column": "amount" }, "EC": { "value": 620000, "label": "TOTAL (IV) - Dettes", "column": "amount" }, "EE": { "value": 863000, "label": "TOTAL GÉNÉRAL (I à V)", "column": "amount" }, "company_name": "SAS EXEMPLE INDUSTRIE" } ``` Every scalar numeric field carries a machine-readable `column` property naming the form column it belongs to (`amount`: this form reports a single exercice, so every value belongs to the current fiscal year). Use it instead of parsing labels. Metadata fields (company info, dates) have no `column` property. ### Column Structure The liabilities side has one main column showing the values for fiscal year N (Exercice N). Some fields include sub-details shown within the field description (like "Dont versé" for capital). ### Key Fields Extracted | Section | Field ID | Description | | --- | --- | --- | | Capitaux propres (I) | DA | Capital social ou individuel | | DB | Primes d'émission, de fusion, d'apport | | DD | Réserve légale | | DE | Réserves statutaires ou contractuelles | | DF | Réserves réglementées | | DG | Autres réserves | | DH | Report à nouveau | | DI | Résultat de l'exercice | | DL | TOTAL (I) - Capitaux propres | | Provisions (III) | DP | Provisions pour risques | | DQ | Provisions pour charges | | DR | TOTAL (III) | | Dettes (IV) | DS | Emprunts obligataires convertibles | | DU | Emprunts et dettes auprès des établissements de crédit | | DX | Dettes fournisseurs et comptes rattachés | | DY | Dettes fiscales et sociales | | EC | TOTAL (IV) - Dettes | | Total général | EE | TOTAL GÉNÉRAL (I à V) | ### Validation Rules The extraction applies the following validation rule: * **Balance Verification**: Field `EE` (Total général passif) must equal the total général actif from CERFA 2050 for a complete balance sheet --- CERFA 2052 - Income Statement (Normal Regime) ============================================= The **CERFA 2052** (Compte de Résultat) is the detailed income statement used by companies under the normal tax regime. This form presents operating, financial, and exceptional results with a breakdown of domestic sales and exports. part\_type:`CERFA-2052` **Versions supported:** 2025, 2024, 2023 [2025 (5013) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2050-liasse/2025/2050-liasse_5013.pdf)[2024 (4594) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2050-liasse/2024/2050-liasse_4594.pdf)[2023 (4225) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2050-liasse/2023/2050-liasse_4225.pdf) ### Document Structure The CERFA 2052 income statement includes the following sections: * **Produits d'exploitation**: Operating income (sales of goods and services, production, subsidies) * **Charges d'exploitation**: Operating expenses (purchases, external costs, salaries, depreciation) * **Résultat d'exploitation**: Operating result * **Produits financiers**: Financial income (dividends, interest income, forex gains) * **Charges financières**: Financial expenses (interest expense, forex losses) * **Résultat financier**: Financial result * **Résultat courant avant impôts**: Current result before tax ### CERFA 2052 Data Structure Within the `document_parts` array, the fr\_cerfa\_2052\_2024 `data` object contains the following fields: ``` { "FA": { "value": 450000, "label": "Ventes de marchandises (France)", "column": "amount" }, "FB": { "value": 150000, "label": "Ventes de marchandises (Exportations)", "column": "amount" }, "FC": { "value": 600000, "label": "Ventes de marchandises (Total)", "column": "amount" }, "FG": { "value": 280000, "label": "Production vendue - services (France)", "column": "amount" }, "FH": { "value": 70000, "label": "Production vendue - services (Exportations)", "column": "amount" }, "FI": { "value": 350000, "label": "Production vendue - services (Total)", "column": "amount" }, "FJ": { "value": 730000, "label": "Chiffres d'affaires nets (France)", "column": "amount" }, "FK": { "value": 220000, "label": "Chiffres d'affaires nets (Exportations)", "column": "amount" }, "FL": { "value": 950000, "label": "Chiffres d'affaires nets (Total)", "column": "amount" }, "FO": { "value": 15000, "label": "Subventions d'exploitation", "column": "amount" }, "FR": { "value": 965000, "label": "Total des produits d'exploitation (I)", "column": "amount" }, "FS": { "value": 420000, "label": "Achats de marchandises (y compris droits de douane)", "column": "amount" }, "FW": { "value": 185000, "label": "Autres achats et charges externes", "column": "amount" }, "FX": { "value": 28000, "label": "Impôts, taxes et versements assimilés", "column": "amount" }, "FY": { "value": 142000, "label": "Salaires et traitements", "column": "amount" }, "FZ": { "value": 78000, "label": "Charges sociales", "column": "amount" }, "GA": { "value": 45000, "label": "Sur immobilisations - dotations aux amortissements", "column": "amount" }, "HS": { "value": 2500, "label": "dont amortissement du fonds de commerce", "column": "amount" }, "GF": { "value": 920000, "label": "Total des charges d'exploitation (II)", "column": "amount" }, "GG": { "value": 45000, "label": "RÉSULTAT D'EXPLOITATION (I - II)", "column": "amount" }, "GL": { "value": 3500, "label": "Autres intérêts et produits assimilés", "column": "amount" }, "GP": { "value": 3500, "label": "Total des produits financiers (V)", "column": "amount" }, "GR": { "value": 12000, "label": "Intérêts et charges assimilées", "column": "amount" }, "GU": { "value": 12000, "label": "Total des charges financières (VI)", "column": "amount" }, "GV": { "value": -8500, "label": "RÉSULTAT FINANCIER (V - VI)", "column": "amount" }, "GW": { "value": 36500, "label": "RÉSULTAT COURANT AVANT IMPÔTS (I - II + III - IV + V - VI)", "column": "amount" }, "company_name": "SAS EXEMPLE INDUSTRIE" } ``` Every scalar numeric field carries a machine-readable `column` property naming the form column it belongs to (`amount`: this form reports a single exercice, so every value belongs to the current fiscal year). Use it instead of parsing labels. Metadata fields (company info, dates) have no `column` property. ### Column Structure Sales-related fields are split into three columns to track export activity: * **France (col1)**: Domestic sales (France only) * **Exportations et livraisons intracommunautaires (col2)**: Exports and intra-EU deliveries * **Total (col3)**: Total amount for fiscal year N Other income and expense items only show the Total column (col3). ### Key Fields Extracted | Section | Field ID | Description | Columns | | --- | --- | --- | --- | | Produits d'exploitation | FA, FB, FC | Ventes de marchandises | France, Exportations, Total | | FG, FH, FI | Production vendue - services | France, Exportations, Total | | FJ, FK, FL | Chiffres d'affaires nets | France, Exportations, Total | | FO | Subventions d'exploitation | Total only | | FR | Total des produits d'exploitation (I) | Total only | | Charges d'exploitation | FS | Achats de marchandises | Total only | | FW | Autres achats et charges externes | Total only | | FX | Impôts, taxes et versements assimilés | Total only | | FY | Salaires et traitements | Total only | | FZ | Charges sociales | Total only | | GA | Sur immobilisations - dotations aux amortissements **Version 2025 only:** Field HD no longer included within GA. Label is "dotations aux amortissements" only (without "sur immobilisations"). | Total only | | HS | Dont amortissement du fonds de commerce **Version 2025 only:** This field is new in 2025 and not present in 2023-2024 versions. In 2023-2024, this information was included within field GA. | Total only | | GF | Total des charges d'exploitation (II) | Total only | | Résultat d'exploitation | GG | RÉSULTAT D'EXPLOITATION (I - II) | Total only | | Produits financiers | GP | Total des produits financiers (V) | Total only | | Charges financières | GU | Total des charges financières (VI) | Total only | | Résultat financier | GV | RÉSULTAT FINANCIER (V - VI) | Total only | | Résultat courant | GW | RÉSULTAT COURANT AVANT IMPÔTS | Total only | ### Field Naming Convention Sales fields use a 3-column pattern: * **France (col1)**: Field ID ending in A, D, G, J (e.g., FA, FD, FG, FJ) * **Exportations (col2)**: Field ID ending in B, E, H, K (e.g., FB, FE, FH, FK) * **Total (col3)**: Field ID ending in C, F, I, L (e.g., FC, FF, FI, FL) The pattern follows: merchandise (A-B-C), goods (D-E-F), services (G-H-I), turnover (J-K-L). --- CERFA 2053 - Income Statement (Continued) ========================================= The **CERFA 2053** (Compte de Résultat - suite) is the continuation of the income statement (CERFA 2052) for companies under the normal tax regime. This form captures exceptional items, taxes, and calculates the final profit or loss for the fiscal year. part\_type:`CERFA-2053` **Versions supported:** 2025, 2024, 2023 [2025 (5013) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2050-liasse/2025/2050-liasse_5013.pdf)[2024 (4594) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2050-liasse/2024/2050-liasse_4594.pdf)[2023 (4225) ↗](https://www.impots.gouv.fr/sites/default/files/formulaires/2050-liasse/2023/2050-liasse_4225.pdf) ### Document Structure The CERFA 2053 continues the income statement from CERFA 2052 and includes: * **Produits exceptionnels**: Exceptional income (non-recurring transactions) * **Charges exceptionnelles**: Exceptional expenses (non-recurring costs) * **Résultat exceptionnel**: Exceptional result * **Participation et impôts**: Employee profit-sharing and corporate income tax * **Résultat final**: Final profit or loss for the fiscal year * **Renvois (footnotes)**: Additional details and breakdowns required by tax authorities ### CERFA 2053 Data Structure Within the `document_parts` array, the fr\_cerfa\_2053\_2024 `data` object contains the following fields: ``` { "HA": { "value": 5000, "label": "Produits exceptionnels sur opérations de gestion", "column": "amount" }, "HB": { "value": 12000, "label": "Produits exceptionnels sur opérations en capital", "column": "amount" }, "HC": { "value": 3000, "label": "Reprises sur provisions et transferts de charges", "column": "amount" }, "HD": { "value": 20000, "label": "Total des produits exceptionnels (VII)", "column": "amount" }, "HE": { "value": 2000, "label": "Charges exceptionnelles sur opérations de gestion", "column": "amount" }, "HF": { "value": 8000, "label": "Charges exceptionnelles sur opérations en capital", "column": "amount" }, "HG": { "value": 1000, "label": "Dotations exceptionnelles aux amortissements et provisions", "column": "amount" }, "HH": { "value": 11000, "label": "Total des charges exceptionnelles (VIII)", "column": "amount" }, "HI": { "value": 9000, "label": "Résultat exceptionnel (VII - VIII)", "column": "amount" }, "HJ": { "value": 2500, "label": "Participation des salariés aux résultats de l'entreprise", "column": "amount" }, "HK": { "value": 12000, "label": "Impôts sur les bénéfices", "column": "amount" }, "HL": { "value": 988500, "label": "Total des produits (I + III + V + VII)", "column": "amount" }, "HM": { "value": 945500, "label": "Total des charges (II + IV + VI + VIII + IX + X)", "column": "amount" }, "HN": { "value": 43000, "label": "Bénéfice ou perte", "column": "amount" }, "HX": { "value": 1500, "label": "Dont dons faits aux organismes d'intérêt général", "column": "amount" }, "company_name": "SAS EXEMPLE INDUSTRIE" } ``` Every scalar numeric field carries a machine-readable `column` property naming the form column it belongs to (`amount`: this form reports a single exercice, so every value belongs to the current fiscal year). Use it instead of parsing labels. Metadata fields (company info, dates) have no `column` property. ### Key Fields Extracted | Section | Field ID | Description | | --- | --- | --- | | Produits exceptionnels | HA | Produits exceptionnels sur opérations de gestion | | HB | Produits exceptionnels sur opérations en capital | | HC | Reprises sur provisions et transferts de charges | | HD | Total des produits exceptionnels (VII) | | Charges exceptionnelles | HE | Charges exceptionnelles sur opérations de gestion | | HF | Charges exceptionnelles sur opérations en capital | | HG | Dotations exceptionnelles aux amortissements et provisions | | HH | Total des charges exceptionnelles (VIII) | | Résultat exceptionnel | HI | Résultat exceptionnel (VII - VIII) | | Participation et impôts | HJ | Participation des salariés aux résultats | | HK | Impôts sur les bénéfices | | Totaux et résultat final | HL | Total des produits (I + III + V + VII) | | HM | Total des charges (II + IV + VI + VIII + IX + X) | | HN | Bénéfice ou perte | | Renvois (footnotes) | HS | (14) Dont montant de l'amortissement du fonds de commerce **Version 2025 only:** This field is new in 2025 and not present in earlier versions. Added after A6 and A9 to capture goodwill amortization amount. | | HO | Produits nets partiels sur opérations à long terme | | HY | Produits de locations immobilières | | HP | Crédit-bail mobilier | | HQ | Crédit-bail immobilier | | HX | Dons faits aux organismes d'intérêt général | | 1G | Produits d'exploitation afférents à des exercices antérieurs | | 1H | Charges d'exploitation afférentes à des exercices antérieurs | | A2 | Cotisations personnelles de l'exploitant | | A5 | Montant des cotisations sociales obligatoires hors CSG-CRDS | | RC, RD | Amortissements spéciaux (PME innovantes, constructions nouvelles) | ### Understanding Exceptional Items Exceptional items (produits et charges exceptionnels) are non-recurring transactions that are: * **Not part of regular operations**: One-time events, asset disposals, restructuring costs * **Material in amount**: Significant enough to impact the understanding of the company's performance * **Separated for analysis**: Allows investors and tax authorities to assess normal operating performance --- CERFA 2059-G - Filiales et Participations ========================================= The **CERFA 2059-G** (Filiales et Participations) lists all subsidiaries and equity holdings where the declaring company holds at least 10% ownership. This form captures subsidiary details including legal form, SIREN number, ownership percentage, and address. part\_type:`CERFA-2059-G` ### Document Structure The CERFA 2059-G contains: * **Company metadata**: Declaring company name, SIRET, address, fiscal year end * **Filiales**: Up to 8 subsidiary blocks per page, each with legal form, name, SIREN, ownership %, and address * **Multi-page**: When more than 8 subsidiaries exist, the form spans multiple pages ### CERFA 2059-G Data Structure Within the `document_parts` array, the `data` object contains the following structure: ``` { "company_name": "SARL LELEU", "exercice_clos_le": "2024-06-30", "siret": "44808159600026", "adresse_voie": null, "lieu_dit": "LIEU DIT CANTO PERLIC", "code_postal": "31570", "ville": "LANTA", "nombre_total_filiales": 1, "filiales": [ { "legal_form": "SAS", "name": "SCI IMMO 24", "siren": "889260329", "ownership_pct": 5.00, "street_number": "24", "street": "AVENUE DE TOULOUSE", "postal_code": "31570", "city": "LANTA", "country": "FRANCE" } ] } ``` ### Field Descriptions #### Document Metadata | Field | Type | Description | | --- | --- | --- | | company\_name | string | Declaring company name | | exercice\_clos\_le | string | Fiscal year end date (ISO 8601) | | siret | string | 14-digit SIRET identifier | | adresse\_voie | string | null | Street address | | lieu\_dit | string | null | Locality or place name | | code\_postal | string | Postal code | | ville | string | City | | nombre\_total\_filiales | integer | null | Total number of subsidiaries declared | | filiales | array | Array of subsidiary objects (see below) | ### Subsidiary Object Structure Each object in the `filiales` array contains the following fields: | Field | Type | French Original | Description | | --- | --- | --- | --- | | legal\_form | string | fj (Forme juridique) | Legal form (e.g., SAS, SARL, SCI) | | name | string | den (Dénomination) | Subsidiary name | | siren | string | null | siren (N° SIREN) | 9-digit SIREN number (if established in France) | | ownership\_pct | number | pct (% de détention) | Ownership percentage | | street\_number | string | null | num (N°) | Street number | | street | string | null | voie (Voie) | Street name | | postal\_code | string | null | cp (Code postal) | Postal code | | city | string | null | commune (Commune) | City or municipality | | country | string | null | pays (Pays) | Country | ### API Response Format The complete API response for a CERFA 2059-G document looks like this: ``` { "extraction_id": "12345", "document_name": "2059-G.pdf", "category": "LIASSE_FISCALE", "extraction_date": "2024-11-25T10:30:00Z", "review_status": "NOT_REVIEWED", "reviewed_by_user": null, "reviewed_at": null, "document_parts": [ { "part_type": "CERFA-2059-G", "label": "CERFA 2059-G", "validation_status": "OK", "validation_codes": [], "pages": [1], "data": { "company_name": "SARL LELEU", "exercice_clos_le": "2024-06-30", "siret": "44808159600026", "adresse_voie": null, "lieu_dit": "LIEU DIT CANTO PERLIC", "code_postal": "31570", "ville": "LANTA", "nombre_total_filiales": 1, "filiales": [ { "legal_form": "SAS", "name": "SCI IMMO 24", "siren": "889260329", "ownership_pct": 5.00, "street_number": "24", "street": "AVENUE DE TOULOUSE", "postal_code": "31570", "city": "LANTA", "country": "FRANCE" } ] } } ] } ``` --- DE SUSA - German Trial Balance (Summen- und Saldenliste) ======================================================== The **DE SUSA** (Summen- und Saldenliste) is a German accounting trial balance that lists every account with its opening balance, transactions and closing balance for a specific period. part\_type:`de_susa_summen_und_saldenliste` **Note:** This is a German accounting document (Summen- und Saldenliste). All field names have been translated to English for the API response. ### Document Structure The DE SUSA contains: * **Company Information**: Company name, reference number, and reporting period * **Accounts Array**: A list of all accounts with their balances and transactions ### DE SUSA Data Structure Within the `document_parts` array, the de\_susa\_summen\_und\_saldenliste `data` object contains the following structure: ``` { "company": "Thorberg UG (haftungsbeschränkt), Eichfeldstraße 36, 94496 Ortenburg", "ref": "2024-001", "period": "2024-01-01 to 2024-12-31", "doc_date": "2024-12-31", "accounts": [ { "account_number": "480", "description": "Low-value assets (GWG)", "account_class": null, "opening_balance": -796.64, "closing_balance": -4217.04, "debit": 3420.4, "credit": null, "cumulative_debit": 3420.4, "cumulative_credit": null }, { "account_number": "800", "description": "Share capital", "account_class": null, "opening_balance": 3001, "closing_balance": 3001, "debit": null, "credit": null, "cumulative_debit": null, "cumulative_credit": null }, { "account_number": "1000", "description": "Cash (1)", "account_class": null, "opening_balance": 924.1, "closing_balance": 834.16, "debit": 284.93, "credit": 194.99, "cumulative_debit": 284.93, "cumulative_credit": 194.99 } ] } ``` ### Field Descriptions | Field | Type | Description | | --- | --- | --- | | company | string | Company name and address | | ref | string | Reference number for this document | | period | string | Reporting period (date range) | | doc\_date | string | Document date (typically end of period) | | accounts | array | Array of account objects (see below) | ### Account Object Structure Each account object in the `accounts` array contains the following fields: | Field | Type | German Original | Description | | --- | --- | --- | --- | | account\_number | string | kt (Konto) | Account number in the chart of accounts | | description | string | ds (Designation) | Account description or name | | account\_class | string | null | klass (Klasse) | Account classification combining class number and balance type (e.g., `1_debit`, `1_credit`, `2_debit`, `2_credit`) | | opening\_balance | number | ib (Anfangsbilanz) | Opening balance at start of period | | closing\_balance | number | fb (Final Balance) | Closing balance at end of period | | debit | number | null | de (Debit) | Debit transactions for the period | | credit | number | null | cr (Credit) | Credit transactions for the period | | cumulative\_debit | number | null | cumde (Kumulativ Debit) | Cumulative debit for the period | | cumulative\_credit | number | null | cumcr (Kumulativ Credit) | Cumulative credit for the period | ### Understanding the Trial Balance The SUSA (Summen- und Saldenliste) covers every account in the company's chart of accounts. For each account it shows: * **Opening Balance (IB)**: The balance at the start of the reporting period * **Period Movements**: Debit and credit transactions during the period * **Cumulative Movements**: Total debit and credit amounts (may include prior periods) * **Closing Balance (FB)**: The balance at the end of the reporting period **Balance Calculation:** The closing balance is calculated as: `closing_balance = opening_balance + debit - credit` Negative balances indicate credit balances (liabilities, equity, income), while positive balances indicate debit balances (assets, expenses). ### Common Account Types The SUSA typically includes accounts from all sections of the German chart of accounts (SKR): | Account Range | Category | Examples | | --- | --- | --- | | 0xxx | Fixed Assets | Land, buildings, equipment, intangible assets | | 1xxx | Current Assets | Cash (1000), bank accounts, receivables, inventory | | 2xxx-3xxx | Equity & Liabilities | Share capital (800), reserves, loans, payables | | 4xxx | Operating Expenses | Low-value assets (480), materials, wages, rent | | 5xxx-7xxx | Revenue & Other Income | Sales, service revenue, interest income | | 8xxx-9xxx | Opening/Closing Accounts | Opening balance sheet, closing entries | ### API Response Format The complete API response for a SUSA document looks like this: ``` { "extraction_id": "12345", "document_name": "susa_2024.pdf", "category": "FINANCIAL_STATEMENT", "extraction_date": "2024-11-25T10:30:00Z", "review_status": "NOT_REVIEWED", "reviewed_by_user": null, "reviewed_at": null, "document_parts": [ { "part_type": "de_susa_summen_und_saldenliste", "validation_status": null, "validation_codes": [], "pages": [1, 2, 3], "version": null, "data": { "company": "Thorberg UG (haftungsbeschränkt), Eichfeldstraße 36, 94496 Ortenburg", "ref": "2024-001", "period": "2024-01-01 to 2024-12-31", "doc_date": "2024-12-31", "accounts": [ { "account_number": "480", "description": "Low-value assets (GWG)", "account_class": null, "opening_balance": -796.64, "closing_balance": -4217.04, "debit": 3420.4, "credit": null, "cumulative_debit": 3420.4, "cumulative_credit": null } ] } } ] } ``` --- BE Interne Jaarrekening - Bilan Actif (Activa) ============================================== The **BE Interne Jaarrekening - Bilan Actif** covers the asset side of the Belgian internal annual accounts balance sheet (*interne jaarrekening - balans activa*). Field labels are bilingual (French / Dutch) and anchored on **NBB rubric codes** (e.g. `20/58`, `21/28`, `29/58`) which are stable across the micro, abrégé, and complet schéma variants. part\_type:`be-interne-jaarrekening-bilan-actif` **Note:** Unlike the French Bilan Actif (Brut / Amort/Prov / Net N / Net N-1), Belgian internal accounts typically expose a single **net** column per exercise. This schema extracts net values only. ### Document Structure * **Vaste activa (21/28)**: Actif immobilisé (`BA01` total, detailed by `BA02`–`BA04`) * **Vlottende activa (29/58)**: Actif circulant (`BA05` total, detailed by `BA06`–`BA11`) * **Totaal activa (20/58)**: Total actif (`BA12`) ### Data Structure Within the `document_parts` array, the `data` object contains fields keyed by a stable `BA##` identifier for the current exercise (Boekjaar). **Every field has a `_n1` sibling** (e.g. `BA01_n1`) carrying the prior exercise (Vorig boekjaar). There is no N-2 column. ``` { "BA01": { "value": 450000, "label": "Actif immobilisé net total (Vaste activa — 21/28)", "column": "net_n" }, "BA02": { "value": 15000, "label": "Immobilisations incorporelles (Immateriële vaste activa — 21)", "column": "net_n" }, "BA03": { "value": 380000, "label": "Immobilisations corporelles (Materiële vaste activa — 22/27)", "column": "net_n" }, "BA05": { "value": 280000, "label": "Actif circulant net total (Vlottende activa — 29/58)", "column": "net_n" }, "BA07": { "value": 120000, "label": "Stocks nets (Voorraden en bestellingen in uitvoering — 3)", "column": "net_n" }, "BA10": { "value": 60000, "label": "Disponibilités (Liquide middelen — 54/58)", "column": "net_n" }, "BA12": { "value": 730000, "label": "Total actif net (Totaal activa — 20/58)", "column": "net_n" }, "BA12_n1": { "value": 695000, "label": "Total actif net (Totaal activa — 20/58) (Net N-1)", "column": "net_n_1" }, "company_name": "BV VOORBEELD", "company_vat": "BE0123.456.789", "fiscal_year_end_n": "2024-12-31", "fiscal_year_end_n1": "2023-12-31", "schema_type": "abrege" } ``` Each numeric field is an object with `value`, a human-readable `label`, and a machine-readable `column`: `net_n` for exercice N, `net_n_1` for the `*_n1` fields. Use `column` instead of parsing labels. Metadata fields (company info, dates) are plain scalars with no `column`. Values are taken **as printed** on the document, anchored on the NBB rubric code; subtotals are never recomputed from their detail lines. A rubric that does not appear on the document (common in `micro` / `abrege` layouts, or for the N-1 column of a first exercise) is absent from `data` or `null`, never an invented `0`. ### Field Reference Each field below also exists with a `_n1` suffix for the prior exercise. #### Vaste activa (Actif immobilisé) | Field | NBB Rubric | Description (FR / NL) | | --- | --- | --- | | BA01 | 21/28 | Actif immobilisé net total (Vaste activa) | | BA02 | 21 | Immobilisations incorporelles (Immateriële vaste activa) | | BA03 | 22/27 | Immobilisations corporelles (Materiële vaste activa) | | BA04 | 28 | Immobilisations financières (Financiële vaste activa) | #### Vlottende activa (Actif circulant) | Field | NBB Rubric | Description (FR / NL) | | --- | --- | --- | | BA05 | 29/58 | Actif circulant net total (Vlottende activa) | | BA06 | 29 | Créances clients nettes à plus d'un an (Vorderingen op meer dan één jaar) | | BA07 | 3 | Stocks nets (Voorraden en bestellingen in uitvoering) | | BA08 | 40/41 | Créances clients nettes à moins d'un an (Vorderingen op ten hoogste één jaar) | | BA09 | 50/53 | Placements de trésorerie (Geldbeleggingen) | | BA10 | 54/58 | Disponibilités (Liquide middelen) | | BA11 | 490/1 | Comptes de régularisation (Overlopende rekeningen) | #### Totaal (Total) | Field | NBB Rubric | Description (FR / NL) | | --- | --- | --- | | BA12 | 20/58 | Total actif net (Totaal activa) | ### Consistency Checks Each exercise is validated with `BA12 = BA02 + BA03 + BA04 + BA06 + BA07 + BA08 + BA09 + BA10 + BA11`. The result is reported in the part's validation status. ### Metadata Fields * **company\_name**: Raison sociale (Naam) * **company\_vat**: Numéro TVA (BTW-nummer), Belgian VAT number, format `BE0xxx.xxx.xxx` * **fiscal\_year\_end\_n**: Exercice N clos le (Boekjaar N afgesloten op), ISO 8601 `YYYY-MM-DD` * **fiscal\_year\_end\_n1**: Exercice N-1 clos le (Boekjaar N-1 afgesloten op), ISO 8601 `YYYY-MM-DD` * **schema\_type**: Type de schéma, one of `micro`, `abrege`, `complet` --- BE Interne Jaarrekening - Bilan Passif (Passiva) ================================================ The **BE Interne Jaarrekening - Bilan Passif** covers the liability side of the Belgian internal annual accounts balance sheet (*interne jaarrekening - balans passiva*). Field labels are bilingual (French / Dutch) and anchored on **NBB rubric codes** (e.g. `10/15`, `17/49`, `42/48`, `10/49`) which are stable across the micro, abrégé, and complet schéma variants. part\_type:`be-interne-jaarrekening-bilan-passif` **Note:** Totals and their detail lines are both exposed (e.g. `BP15` dettes à un an au plus and its components `BP16`–`BP20`). When summing, pick one level to avoid double-counting. ### Document Structure * **Eigen vermogen (10/15)**: Capitaux propres (`BP01` total, detailed by `BP02`–`BP08`) * **Voorzieningen en uitgestelde belastingen (16)**: Provisions et impôts différés (`BP09`, detailed by `BP10`–`BP11`) * **Schulden (17/49)**: Dettes (`BP12` total; `BP13`–`BP14` long term, `BP15`–`BP20` short term, `BP21` comptes de régularisation) * **Totaal passiva (10/49)**: Total passif (`BP22`) ### Data Structure Within the `document_parts` array, the `data` object contains fields keyed by a stable `BP##` identifier for the current exercise (Boekjaar). **Every field has a `_n1` sibling** (e.g. `BP01_n1`) carrying the prior exercise (Vorig boekjaar). There is no N-2 column. ``` { "BP01": { "value": 310000, "label": "Total capitaux propres (Eigen vermogen — 10/15)", "column": "net_n" }, "BP13": { "value": 150000, "label": "Dettes à plus d'un an (Schulden op meer dan één jaar — 17)", "column": "net_n" }, "BP14": { "value": 150000, "label": "Emprunts et dettes financières (Financiële schulden — 170/4)", "column": "net_n" }, "BP15": { "value": 262000, "label": "Dettes à un an au plus (Schulden op ten hoogste één jaar — 42/48)", "column": "net_n" }, "BP18": { "value": 95000, "label": "Dettes commerciales (Handelsschulden — 44)", "column": "net_n" }, "BP19": { "value": 40000, "label": "Dettes fiscales, salariales et sociales (Schulden belastingen, bezoldigingen en sociale lasten — 45)", "column": "net_n" }, "BP22": { "value": 730000, "label": "Total passif (Totaal passiva — 10/49)", "column": "net_n" }, "BP22_n1": { "value": 695000, "label": "Total passif (Totaal passiva — 10/49) (Net N-1)", "column": "net_n_1" }, "company_name": "BV VOORBEELD", "company_vat": "BE0123.456.789", "fiscal_year_end_n": "2024-12-31", "fiscal_year_end_n1": "2023-12-31", "schema_type": "abrege" } ``` Each numeric field is an object with `value`, a human-readable `label`, and a machine-readable `column`: `net_n` for exercice N, `net_n_1` for the `*_n1` fields. Use `column` instead of parsing labels. Metadata fields (company info, dates) are plain scalars with no `column`. Values are taken **as printed** on the document, anchored on the NBB rubric code; subtotals are never recomputed from their detail lines. A rubric that does not appear on the document (common in `micro` / `abrege` layouts, or for the N-1 column of a first exercise) is absent from `data` or `null`, never an invented `0`. ### Field Reference Each field below also exists with a `_n1` suffix for the prior exercise. #### Eigen vermogen (Capitaux propres) | Field | NBB Rubric | Description (FR / NL) | | --- | --- | --- | | BP01 | 10/15 | Total capitaux propres (Eigen vermogen) | | BP02 | 10 | Capital (Kapitaal) | | BP03 | 11 | Prime d'emission (Uitgiftepremies) | | BP04 | 12 | Plus-values de réevaluation (Herwaarderingsmeerwaarden) | | BP05 | 13 | Réserves (Reserves) | | BP06 | 14 | Résultat reporté (Overgedragen winst) | | BP07 | 15 | Subsides en capital (Kapitaalsubsidies) | | BP08 | 19 | Avance aux associés sur la répartition de l'actif net (Voorschot aan de vennoten op de verdeling van het netto-actief) | #### Voorzieningen (Provisions) | Field | NBB Rubric | Description (FR / NL) | | --- | --- | --- | | BP09 | 16 | Provisions et impôts différés (Voorzieningen en uitgestelde belastingen) | | BP10 | 160/5 | Provisions pour risques et charges (Voorzieningen voor risico's en kosten) | | BP11 | 168 | Impôts différés (Uitgestelde belastingen) | #### Schulden (Dettes) | Field | NBB Rubric | Description (FR / NL) | | --- | --- | --- | | BP12 | 17/49 | Total Dettes (Schulden) | | BP13 | 17 | Dettes à plus d'un an (Schulden op meer dan één jaar) | | BP14 | 170/4 | Emprunts et dettes financières (Financiële schulden) | | BP15 | 42/48 | Dettes à un an au plus (Schulden op ten hoogste één jaar) | | BP16 | 43 | Dettes financières (Financiële schulden) | | BP17 | 430/8 | Concours bancaires courant (Kredietinstellingen) | | BP18 | 44 | Dettes commerciales (Handelsschulden) | | BP19 | 45 | Dettes fiscales, salariales et sociales (Schulden met betrekking tot belastingen, bezoldigingen en sociale lasten) | | BP20 | 47/48 | Autres dettes (Overige schulden) | | BP21 | 492/3 | Comptes de régularisation (Overlopende rekeningen) | #### Totaal (Total) | Field | NBB Rubric | Description (FR / NL) | | --- | --- | --- | | BP22 | 10/49 | Total passif (Totaal passiva) | ### Consistency Checks Each exercise is validated with `BP22 = BP02 + BP03 + BP04 + BP05 + BP06 + BP07 + BP08 + BP10 + BP11 + BP13 + BP15 + BP21`. The result is reported in the part's validation status. ### Metadata Fields * **company\_name**: Raison sociale (Naam) * **company\_vat**: Numéro TVA (BTW-nummer), Belgian VAT number, format `BE0xxx.xxx.xxx` * **fiscal\_year\_end\_n**: Exercice N clos le (Boekjaar N afgesloten op), ISO 8601 `YYYY-MM-DD` * **fiscal\_year\_end\_n1**: Exercice N-1 clos le (Boekjaar N-1 afgesloten op), ISO 8601 `YYYY-MM-DD` * **schema\_type**: Type de schéma, one of `micro`, `abrege`, `complet` --- --- ### Identity & KYC Schemas URL: https://app.holofin.ai/docs/api/v1.1/schemas/identity/ Jump to a document type PreviewThese schemas are preview contracts The `part_type` values and field layouts on this page describe Holofin's upcoming prebuilt extractors for these document types. They are not yet emitted by workflow runs. If your workflow processes one of these document types with a **custom extractor**, each extracted part carries that extractor's own `part_type` and schema, which may differ from this page. The authoritative schema is the one defined on the extractor in your workflow configuration. National Identity Card (Carte nationale d'identité) =================================================== part\_type`fr_carte_nationale_identite`PreviewIdentity & KYC The **Carte nationale d'identité** (CNI) is the French national identity card. The extraction returns the cardholder identity, the document numbers and dates, and the parsed machine-readable zone (MRZ). It is used to establish and verify the identity of a natural person during onboarding and KYC review. ### Document structure The CNI contains: * **Holder identity**: Surname, given names, gender, date and place of birth, nationality * **Document details**: Card number, issue date, expiry date, issuing authority * **Physical attributes**: Height when printed on the card * **Machine-readable zone**: The TD1 (current, three-line) or legacy two-line French format printed on the reverse ### Data structure Within the `document_parts` array, the fr\_carte\_nationale\_identite `data` object contains: dataJSON ``` { "last_name": "MARTIN", "first_names": ["Camille", "Alex"], "gender": "F", "birth_date": "1990-05-12", "birth_place": "LYON (69)", "nationality": "FRA", "document_number": "SPECIMEN0000", "issue_date": "2021-06-01", "expiry_date": "2031-05-31", "issuing_authority": "Préfecture du Rhône", "height_cm": 170, "mrz": { "format": "TD1", "lines": [ "IDFRASPECIMEN0000<<<<<<<<<<<<<<", "9005120F3105319FRA<<<<<<<<<<<4", "MARTIN< bool: """Verify a Holofin deletion receipt signature.""" canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")) expected = hmac.new( secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, signature) ``` Use `hmac.compare_digest` (not `==`) for timing-safe comparison. [Accessing Deletion Logs](#accessing-logs) ------------------------------------------ Deletion jobs and receipts are available through the Holofin dashboard. All data is scoped to the authenticated user's organization; you cannot access receipts belonging to another tenant. [View Deletion History](https://app.holofin.ai/logs/data-lifecycle/) Individual receipts can be downloaded as JSON from the deletion history table. The **Export All Receipts** button downloads a ZIP archive containing one JSON file per receipt, named `.json`. Each file includes both the `payload` and `signature` fields for independent verification. [Security Overview](https://app.holofin.ai/docs/api/v1.1/security/)[Data and Compliance](https://app.holofin.ai/docs/api/v1.1/data-compliance/) ---