Dossiers
Reference for /v1/dossiers: group several documents of one person or procedure with requirements, add documents by file or analysis_id and get an overall verdict with cross-document holder and date checks.
A dossier (dos_…) groups the documents of one person or procedure (the ID, the medical certificate and the
payment receipt of a registration, for example) and computes an overall verdict: whether everything is there,
whether something fails or whether it needs a review. It also checks that every document belongs to the same
holder and that their dates are consistent.
| Method and path | What it does |
|---|---|
POST /v1/dossiers | Creates a dossier with its requirements. |
POST /v1/dossiers/{id}/documents | Adds a document: a file (analysed) or an existing analysis. |
GET /v1/dossiers | Lists the dossiers of the key's mode. |
GET /v1/dossiers/{id} | Retrieves a dossier with its verdict. |
DELETE /v1/dossiers/{id} | Deletes the dossier (not its analyses). |
Every route uses a secret key (ck_test_… or ck_live_…) from your backend and only sees the dossiers of the key's
mode. In the dashboard they are under Dossiers.
Verification links create their own dossier
Every verification link creates a dossier with one requirement per requested
document (dossier_id on the link, verification_link_id on the dossier). Each upload by the person is added
automatically, and the dossier verdict arrives in the verification_link.completed webhook or callback. You only need
to create dossiers by hand when the documents reach you through your own upload.
Create a dossier
POST /v1/dossiers
Content-Type: application/json| Field | Type | Description |
|---|---|---|
reference | string | null | Your reference (up to 200 characters). Useful to filter the list. |
template | string | null | Template (tpl_…) applied to every document analysed in the dossier. 422 template_not_found if it doesn't exist. |
requirements | object[] (1–20) | Documents the dossier must have. Each one: key (1–40 characters: letters, digits, - or _, unique), label (up to 120; by default, the name of the expected type or the key), expect (type or list of types; if missing, the template's or any) and checks (as in analyze). Without requirements and with template: one, key: "document". |
metadata | object string → string | Up to 20 keys. Copied to every analysis made inside the dossier. |
face_verification | { enabled, required? } | null | Face verification requirement (optional module). Fulfilled with POST /v1/face-verifications and dossier_id; required and missing → incomplete; no match → invalid. |
A dossier without requirements (or template) works too: it accepts any document, and the verdict is computed with all the ones you add.
curl https://api.constaia.com/v1/dossiers \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reference": "registration-4821",
"metadata": { "registration_id": "4821" },
"requirements": [
{ "key": "id_card", "label": "ID card", "expect": ["es_dni", "es_nie"], "checks": { "not_expired": true } },
{ "key": "medical", "label": "Medical certificate", "expect": "medical_certificate_sport", "checks": { "max_age_days": 180 } }
]
}'Answers 201 with the dossier object, with verdict.status: "incomplete" until the documents arrive. A repeated key
returns 422 duplicate_document_key; an invalid expect or check, 422 invalid_parameter with
param: "requirements.<i>.<field>".
Add documents
POST /v1/dossiers/{id}/documentsThere are two ways:
| How | Body | What happens |
|---|---|---|
| A file | multipart/form-data with file, requirement_key (optional) and options (JSON, optional); or JSON with file_url or file_base64 + filename. | It's analysed right away, just like POST /v1/analyze (spends credits in live), with the dossier's template, the requirement's expect and checks and, on top, your options. |
| An existing analysis | JSON { "analysis_id": "an_…", "requirement_key": "…" } | It's linked without analysing again. It must belong to the account and to the dossier's mode. |
Answers 200 with the updated dossier.
# A file, for a given requirement
curl https://api.constaia.com/v1/dossiers/dos_01J9Z8Q3K4M5N6P7Q8R9S0T1V2/documents \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-F "file=@dni.jpg" \
-F "requirement_key=id_card"
# An analysis you already had
curl https://api.constaia.com/v1/dossiers/dos_01J9Z8Q3K4M5N6P7Q8R9S0T1V2/documents \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "analysis_id": "an_01J9Z8Q3K4M5N6P7Q8R9S0T1V3", "requirement_key": "medical" }'Which requirement it fills. If you send requirement_key, that one (if it doesn't exist: 422 invalid_document_key).
Otherwise:
- with a file and a single requirement, that one; with several, it's analysed accepting the union of their
expect(without theirchecks) and assigned to the first requirement with no document whoseexpectincludes the detected type; - with
analysis_id, the analysis's requirement if it came from a link with the same key or, otherwise, as above according to its type; - if no requirement fits, it stays as a loose document: it covers no requirement, but counts for the cross-document checks.
You can add several documents to the same requirement (a new photo after a rejection, for example). The latest one counts; if the latest failed, the latest one that completed.
The dossier object
{
"id": "dos_01J9Z8Q3K4M5N6P7Q8R9S0T1V2",
"object": "dossier",
"livemode": false,
"reference": "registration-4821",
"template": null,
"verification_link_id": null,
"metadata": { "registration_id": "4821" },
"requirements": [
{ "key": "id_card", "label": "ID card", "expect": ["es_dni", "es_nie"], "checks": { "not_expired": true }, "status": "valid", "analysis_id": "an_01J9Z8Q3K4M5N6P7Q8R9S0T1V3" },
{ "key": "medical", "label": "Medical certificate", "expect": ["medical_certificate_sport"], "checks": { "max_age_days": 180 }, "status": "missing", "analysis_id": null }
],
"documents": [
{ "analysis_id": "an_01J9Z8Q3K4M5N6P7Q8R9S0T1V3", "requirement_key": "id_card", "type": "es_dni", "file_name": "dni.jpg", "status": "completed", "verdict_status": "valid", "final_status": "valid", "created_at": "2026-09-30T10:02:11Z" }
],
"verdict": {
"status": "incomplete",
"reasons": [
{ "code": "requirement_missing", "severity": "warning", "message": "The document “Medical certificate” is missing.", "requirement_key": "medical" }
],
"checks": [
{ "code": "same_holder", "passed": null, "severity": "warning", "message": "There isn't enough data to check that all documents belong to the same person.", "analyses": ["an_01J9Z8Q3K4M5N6P7Q8R9S0T1V3"] },
{ "code": "dates_consistent", "passed": true, "severity": "info", "message": "The dates on the documents are consistent.", "analyses": ["an_01J9Z8Q3K4M5N6P7Q8R9S0T1V3"] }
]
},
"holder": { "name": "MARÍA GARCÍA LÓPEZ", "document_number": "12345678Z", "birth_date": "1990-04-12" },
"created_at": "2026-09-30T10:00:00Z",
"updated_at": "2026-09-30T10:02:12Z"
}| Field | Description |
|---|---|
requirements[].status | missing (no document), processing, valid, invalid, review or failed (the analysis failed). |
requirements[].analysis_id | The analysis that counts for the requirement. |
documents[] | Every analysis in the dossier, in order of arrival: requirement, detected type, file name, analysis status, verdict (verdict_status) and final result after human review (final_status). |
verdict.status | Overall verdict. See below. |
verdict.reasons[] | Why: code, severity, message (in the request's language) and requirement_key if it refers to a requirement. |
verdict.checks[] | Cross-document checks: code, passed (true, false or null when there is no data), severity, message and the analyses involved. |
holder | Reference holder: name, document number and date of birth from the first document that has them, or null. |
verification_link_id | The link that created the dossier, or null. |
Messages in reasons and checks follow the Accept-Language header (es, en, pt, fr).
Overall verdict
Each requirement takes the status of its analysis. If a person reviewed the analysis, their decision counts
(final_status); if it's waiting for review, or has no verdict, it counts as review. The dossier:
verdict.status | When (in this order) |
|---|---|
invalid | A requirement is invalid or a cross-document check fails. |
incomplete | A requirement is missing, is being analysed or its analysis failed. |
review | Everything is there, but some document is in review. |
complete_valid | Every requirement is valid and no cross-document check fails. |
Without requirements, each added document counts as a requirement (and with no document at all, incomplete with the
reason no_documents). The per-requirement reasons are requirement_missing, requirement_processing,
requirement_failed, requirement_invalid and requirement_review.
The verdict is recomputed automatically when an analysis in the dossier finishes or when a person decides its
review (in the dashboard or with POST /v1/analyses/{id}/review). Dossiers have no webhook of
their own: call GET /v1/dossiers/{id} when you receive the document's analysis.completed or analysis.reviewed,
or use a verification link and its verification_link.completed event.
Cross-document checks
code | What it checks | If it fails |
|---|---|---|
same_holder | That every document with a holder belongs to the same person: name (flexible comparison), normalised document number and date of birth; only the data present in both documents. | passed: false, severity: "error" and the dossier becomes invalid. With nothing to compare (for example, a single document with a holder), passed: null and severity: "warning", which doesn't change the verdict. |
dates_consistent | That the date of birth is the same everywhere, that no issue date is in the future and that none is before the date of birth. | passed: false, severity: "error" and the dossier becomes invalid. With no dates to compare, passed: null. |
Only completed analyses count (the one that counts for each requirement and the loose documents). Values masked with
mask_fields are not compared.
List, retrieve and delete
curl -G https://api.constaia.com/v1/dossiers \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
--data-urlencode "verdict=review" \
--data-urlencode "metadata[registration_id]=4821"| Parameter | Description |
|---|---|
verdict | complete_valid, incomplete, invalid or review. |
reference | Your exact reference. |
metadata[key] | Filters by a metadata value. Repeatable: they must all match. |
limit, starting_after | Pagination: 1–100 (20 by default) and the id of the last item on the previous page. |
GET /v1/dossiers/{id} returns the object. DELETE /v1/dossiers/{id} returns
{ "id": "dos_…", "object": "dossier", "deleted": true } and doesn't delete the analyses (for that, see
deletion by metadata). A dossier from another
mode, or one that doesn't exist, returns 404 resource_missing.
SDKs
import { Constaia } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";
const constaia = new Constaia();
const dossier = await constaia.dossiers.create({
reference: "registration-4821",
requirements: [
{ key: "id_card", expect: ["es_dni", "es_nie"] },
{ key: "medical", expect: "medical_certificate_sport", checks: { maxAgeDays: 180 } },
],
});
await constaia.dossiers.addDocument(dossier.id, await fromPath("dni.jpg"), { requirementKey: "id_card" });
const updated = await constaia.dossiers.addDocument(
dossier.id,
{ analysisId: "an_01J9Z8Q3K4M5N6P7Q8R9S0T1V3" },
{ requirementKey: "medical" },
);
console.log(updated.verdict.status); // complete_valid | incomplete | invalid | reviewErrors
| HTTP | code | When |
|---|---|---|
422 | invalid_parameter | A field is not valid (param says which). |
422 | duplicate_document_key | Two requirements with the same key. |
422 | template_not_found | The template doesn't exist in the account. |
422 | invalid_document_key | requirement_key is not a requirement of the dossier. |
404 | resource_missing | The dossier or the analysis_id don't exist (or belong to another mode). |
402, 429… | When adding a file, the same errors as analyze (balance, limits, invalid file). |
Next steps
Verification links
Reference for /v1/verification-links: create a hosted page where the person uploads their documents from their phone, receive the results by webhook, callback or email, and retrieve, list or cancel links.
Sessions and publishable keys
Reference for /v1/sessions and pk_ publishable keys: your backend creates a 15-minute session and the browser uploads each document straight to Constaia with the client_secret, without exposing your secret key.