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; both write to the same dossier.
Overview
Three steps: pick a configuration with GET /case-configs/, create the dossier with that config_id, then send documents with the dossier's case_id through 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_<random>), 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}, 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
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/. 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 oneparty_refson/workflow-runs/names when a document is filed against them. Defaults to the field key.party_refis 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) orBE(bénéficiaire effectif). Descriptive only: it does not decide which documents are expected. Rules read it as@case.<participant>.party_type. Kept when a later call omits it.- The same
party_reftwice 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/ 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_idalone 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} 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} returns, documented under The 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
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.completedwebhook 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 fields.<key>
Role participants.<role>
Party parties[]
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.
Document row documents[] · unassigned_documents[]
Rule rules[]
Every key is always present.
Dossier activity
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
Activity types
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: 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
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}.
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
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
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/. |
| 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 and activities, a query parameter the endpoint does not support. The message names it and lists what is supported. |
| INVALID_FILTER | 400 | On the listing and 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. |