Document Extraction

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

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

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

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_typeDescriptionKey FieldsValidation
bank-statementBank account statements with transactions and balancestransactions[], start_balance, end_balanceBalance equation checks
utility_billUtility bills (electricity, gas, water) with consumption periodsconsumptions[], dp_ref, fluidUnit conversion, total consistency
bilan-actifFrench balance sheet - Assets sideField codes with gross/depreciation/net valuesField relationships, sum validations
bilan-passifFrench balance sheet - Liabilities sideField codes for equity, liabilitiesBalance sheet equation checks
compte-resultatFrench income statementRevenue, expenses, profit/loss itemsP&L equation validations
CERFA-2033-ASimplified balance sheet (Regime Simplifie)Field codes with _net_n and _net_n1 suffixesYear-over-year consistency
CERFA-2033-BSimplified income statementRevenue and expense field codesP&L sum checks
CERFA-2033-CFixed assets and depreciation scheduleAsset movements, depreciation detailsDepreciation calculations
CERFA-2050Balance sheet - Assets (Regime Reel Normal)Detailed asset field codesAsset sum validations
CERFA-2051Balance sheet - Liabilities (Regime Reel Normal)Detailed liability field codesLiability sum validations
CERFA-2052Income statement (Regime Reel Normal)Revenue and expense detailsRevenue/expense checks
CERFA-2053Income statement continued (Regime Reel Normal)Additional P&L line itemsContinuation consistency
de-susaGerman trial balance (Summen- und Saldenliste)Account number, debit/credit sumsTrial balance equation

For detailed field specifications and complete schemas, see the Document Schemas page.

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

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

StatusMeaningExample
OKAll consistency checks passedBalance equation matches within tolerance
WARNINGSoft issues detected, data likely usableMinor formatting inconsistencies, missing optional fields
ERRORValidation errors detectedBalance mismatch, required fields missing, calculation errors
nullNo validation rules applyDocument type has no consistency checks configured

Extraction 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

FieldTypeDescription
extraction_idstringUnique document identifier
document_namestringOriginal filename
document_urlstringAbsolute URL to view/edit extraction in Holofin app
categorystringDocument category from classification (e.g., BANK_STATEMENT, UTILITY_BILL, FINANCIAL_STATEMENT)
extraction_datestringISO 8601 timestamp of extraction completion
validation_statusstring | nullOverall validation status (worst status across all parts)
checks_summaryobjectPer-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_partsarrayArray of extracted document parts

Document Part Structure

FieldTypeDescription
keystringStable 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_typestringSchema identifier (e.g., bank-statement, utility_bill, CERFA-2033-A)
labelstring | nullHuman-readable description of this document part (e.g., account number for bank statements, form title and year for tax documents)
validation_statusstring | nullOverall validation result for this part, combining extraction quality and business rules: OK, WARNING, ERROR, or null
checksarray of objectsThe 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_codesarray of stringsStable 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.
enrichmentsobject | omittedPost-extraction sidecar keyed by field path (e.g. address, issuer.address), one entry per field typed Address (FR) or siren in the schema; each entry names its own kind. Absent entirely when the schema has no such fields.
pagesarray of integersPage numbers from original document where this part appears
versionstring | nullDocument version/year when applicable (e.g., 2024 for CERFA forms)
dataobjectExtracted data in schema-specific structure

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:

StatusMeaningFields
matchedResolved to a real street or delivery pointnormalized, housenumber, street, postcode, city, citycode, ban_id, lat, lon, score, granularity
not_foundBAN 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 substitutedgranularity, score, reason
uncheckedCould not be checked (BAN outage or timeout). Not a negative resultreason

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

FieldTypeDescription
barcode_typestringDetected barcode format (e.g., QR Code, Data Matrix, PDF417, Code 128)
pageintegerPage number where the barcode was detected
textstringDecoded 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:

CheckQuestion it answers
SignatureWas the seal altered since the issuer signed it? Verified cryptographically against the ANTS trust list.
CompletenessDoes 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.
ConsistencyDo 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, 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 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:

FieldTypeDescription
idstringUnique identifier for this activity (e.g., "mut_abc123", "com_xyz456")
typestringActivity type: data_edit, comment, or validation
timestampstringISO 8601 timestamp when the activity occurred
userobjectUser who performed the action, containing id, name, and optionally external_id (present when the user was provisioned via partner SSO)
detailsobjectType-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"
    }
  }
]