Field enrichment
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:
- 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.
- Every enriched field gets an entry, keyed by the field's path, under
enrichmentson the document part. - 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.<seal field>.<group> 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_<group>; see the 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.<field>.<key>. 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 == @codePostalWithout 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.