Constaia
Endpoints

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 pathWhat it does
POST /v1/dossiersCreates a dossier with its requirements.
POST /v1/dossiers/{id}/documentsAdds a document: a file (analysed) or an existing analysis.
GET /v1/dossiersLists 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
FieldTypeDescription
referencestring | nullYour reference (up to 200 characters). Useful to filter the list.
templatestring | nullTemplate (tpl_…) applied to every document analysed in the dossier. 422 template_not_found if it doesn't exist.
requirementsobject[] (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".
metadataobject string → stringUp to 20 keys. Copied to every analysis made inside the dossier.
face_verification{ enabled, required? } | nullFace 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}/documents

There are two ways:

HowBodyWhat happens
A filemultipart/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 analysisJSON { "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 their checks) and assigned to the first requirement with no document whose expect includes 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

dossier
{
  "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"
}
FieldDescription
requirements[].statusmissing (no document), processing, valid, invalid, review or failed (the analysis failed).
requirements[].analysis_idThe 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.statusOverall 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.
holderReference holder: name, document number and date of birth from the first document that has them, or null.
verification_link_idThe 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.statusWhen (in this order)
invalidA requirement is invalid or a cross-document check fails.
incompleteA requirement is missing, is being analysed or its analysis failed.
reviewEverything is there, but some document is in review.
complete_validEvery 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

codeWhat it checksIf it fails
same_holderThat 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_consistentThat 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"
ParameterDescription
verdictcomplete_valid, incomplete, invalid or review.
referenceYour exact reference.
metadata[key]Filters by a metadata value. Repeatable: they must all match.
limit, starting_afterPagination: 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

dossier.ts
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 | review

Errors

HTTPcodeWhen
422invalid_parameterA field is not valid (param says which).
422duplicate_document_keyTwo requirements with the same key.
422template_not_foundThe template doesn't exist in the account.
422invalid_document_keyrequirement_key is not a requirement of the dossier.
404resource_missingThe 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

On this page