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.
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.
Each document part includes:
- Its own
part_typeidentifying the document schema - A
validation_statusshowing if the data passed consistency checks - A
pagesarray showing which pages from your original file this document came from - The extracted
datain the format specific to that document type
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.
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:
extractionrules judge whether the document was read correctly (field formats and lengths, arithmetic totals, checksums). A failure means the returned data may be wrong.businessrules 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_statusandvalidation_codesremain the combined verdict of both categories and their behaviour is unchanged, so existing integrations do not need to be updated. Read the failed entries'typewhen 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": ...}, eachOK,WARNING,ERRORornull(that category ran no checks, which is not the same asOK). statussays what happened:passed,failed, orskipped(the document does not carry the data the rule needs;reasonexplains).severitysays how loud a failure would be: a passingerror-severity rule tells you the check can block.codeis the same token that appears invalidation_codes, so the two can be joined directly.messageis 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, nevernull, when unavailable; absence uniformly means "not available".
Validation Status Values
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
Document Part 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:
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".
Complete Example
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.
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:
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:
| 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, orRESTOREtarget- What was modified:transactionorpropertylocation- (For transactions) Object withpageandrownumbersproperty_name- (For properties) The field identifier that was modified (e.g.,bank_name,028_net_n)changes- (For UPDATE) Object showing what changed withfromandtovaluesoriginal_data- (For DELETE) The data that was deletednew_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- EitherVALIDATEDorINVALIDATEDcomment- 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.
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"
}
}
]