Checks
Reference for every checks option (expiry, issue age, holder age, holder, signature, amounts) and the deterministic NIF, MRZ, IBAN and invoice validations.
Checks are the business rules Constaia evaluates on a document. You send them in options.checks of
POST /v1/analyze (and in batch options) and each one produces one
or more reasons in verdict.reasons. How those reasons combine into valid, invalid or review is explained in
verdicts.
There are two families:
- Checks you ask for (
options.checks): expiry, issue age, holder age, holder, required fields, signature, stamp, amounts, IBAN and reference. - Deterministic validations (
checks[]in the response): Constaia runs them whenever the document type supports them (NIF check letter, MRZ, IBAN, invoice totals…). They are not configurable.
Checks need expect
Checks are always evaluated, but their reasons live inside verdict, and verdict only exists when you send
expect. Without expect you see fields and checks[], but not the outcome of not_expired, holder and the
rest.
Example
curl https://api.constaia.com/v1/analyze \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-F file=@dni_valid.jpg \
-F 'options={
"expect": "es_dni",
"language": "en",
"checks": {
"min_age_years": 18,
"holder": { "full_name": "María García López", "document_number": "12345678Z" }
}
}'camelCase in the JavaScript SDK, snake_case in the API
The JavaScript SDK takes options in camelCase (notExpired, maxAgeDays, holder.fullName, expectedAmount…) and
converts them to snake_case. The API, the PHP SDK, Python and the MCP server use snake_case as is (not_expired,
max_age_days, holder.full_name, expected_amount). Responses are always snake_case.
Options reference
| Option (API) | JS SDK | Type | Default | Reason produced |
|---|---|---|---|---|
not_expired | notExpired | boolean | true for types with an expiry date | not_expired |
reference_date | referenceDate | "YYYY-MM-DD" | today | (shifts the date of the other rules) |
max_age_days | maxAgeDays | integer 0–36500 | — | max_age_days |
min_age_years | minAgeYears | integer 0–150 | — | age or min_age_years |
max_age_years | maxAgeYears | integer 0–150 | — | age or max_age_years |
holder | holder | object | — | holder |
require_fields | requireFields | string[] (max. 50) | — | required_field_missing |
require_signature | requireSignature | boolean | — | require_signature |
require_stamp | requireStamp | boolean | — | require_stamp |
require_valid_signature | requireValidSignature | boolean | — | signature_valid, signature_invalid, signature_missing, document_modified_after_signing, untrusted_signer |
expected_amount | expectedAmount | number | — | expected_amount |
expected_iban | expectedIban | string (max. 40) | — | expected_iban |
expected_reference | expectedReference | string (max. 140) | — | expected_reference |
Checks apply to the detected type (document.type) and read its fields. To see which checks make sense for each
type, look at checks and expiry_check_default in
GET /v1/document-types/{type} or in the catalogue.
not_expired
Checks that the expiry date is today (or reference_date) or later.
- On by default for types with an expiry date (
expiry_check_default: true, e.g.es_dni,es_nieandpassport). Send"not_expired": falseto turn it off. - Only evaluated when the type has an expiry date.
infoif valid,errorif expired,warningif the date could not be read.
{ "code": "not_expired", "severity": "error", "message": "Expired on 15/06/2020." }reference_date
A YYYY-MM-DD date that replaces "today" in not_expired, max_age_days and the age calculation. Useful to evaluate
as of an event date: "will the ID still be valid on competition day?", "will they be 18 on 1 January?".
{ "expect": "es_dni", "checks": { "reference_date": "2027-01-01", "min_age_years": 18 } }max_age_days
The issue date must not be older than N days. Typical for certificates: medical certificate under one year, sexual offences certificate under 90 days.
infoif it passes,errorif older (or if the issue date is more than one day in the future),warningif there is no issue date.
{ "code": "max_age_days", "severity": "info", "message": "Issued 14 days ago (maximum 90)." }min_age_years and max_age_years
Holder's age computed from the birth date as of today (or reference_date).
- If it passes: one
agereason withinfo("The holder is 36 years old."). - Younger than the minimum:
min_age_yearswitherror. Older than the maximum:max_age_yearswitherror. - Birth date unreadable:
agewithwarning.
holder
Compares the document data with the person you expect. Every sub-field is optional; send only what you have.
| Sub-field (API) | JS SDK | How it is compared |
|---|---|---|
full_name | fullName | Full name with normalisation (see below). |
first_name | firstName | With last_name, as a full name. On its own, its words just need to appear in the document name. |
last_name | lastName | Same as first_name. |
document_number | documentNumber | Normalised number (no spaces or hyphens, case-insensitive). |
birth_date | birthDate | Exact match in YYYY-MM-DD format. |
Name matching rules:
- Case and accent insensitive:
María García=MARIA GARCIA. - Accepts a different order of given names and surnames.
- Tolerates small typos.
- With only
first_nameor onlylast_name, particles (de,del…) are ignored.
Outcome:
- Everything you sent matches: one
holderreason withinfolisting the fields ("The holder details match (full_name)."). - Each value that does not match:
holderwitherror, showing what was read and what was expected. - Each value the document does not show or that could not be read:
holderwithwarning.
{ "code": "holder", "severity": "error", "message": "Holder mismatch: full_name is “MARÍA GARCÍA LÓPEZ”, expected “Juan Pérez”." }require_fields
List of fields that must have a value. Dotted paths work for nested fields (e.g. seller.tax_id on invoice). Each
empty field produces a required_field_missing reason with error. Field names per type are in
GET /v1/document-types/{type}.
require_signature and require_stamp
Require a signature or a stamp. They read the signature_present and stamp_present fields, which exist on types
such as medical_certificate_sport. info if present, error if not. Use them only on types that have those fields:
on a type without them the result is error.
require_valid_signature
Requires the PDF to carry an intact PAdES electronic signature, not modified afterwards and from a trusted issuer.
It is different from require_signature, which only looks for a visible signature. It applies to certificates that are
downloaded signed from an official e-government site (signed_pdf: true in the catalogue: Spanish sexual offences and
criminal record certificates, AEAT, Social Security, work history…).
- Without this check, the signature of any PDF is still verified and shown in
signature: a broken signature givessignature_invalid(error) and the other problems arewarning. - With it,
signature_missing,document_modified_after_signinganduntrusted_signerbecomeerror. A photo or a scan never passes.
What it guarantees and what it doesn't: Digital signatures in PDF.
expected_amount, expected_iban, expected_reference
For payment receipts (payment_receipt) and invoices (invoice):
| Option | What it compares |
|---|---|
expected_amount | amount (or total for invoice) with a tolerance of half a cent. |
expected_iban | That the IBAN appears among those in the document (payer or beneficiary). Spaces and case ignored. |
expected_reference | That the normalised expected text is contained in reference or concept. |
{
"expect": "payment_receipt",
"language": "en",
"checks": {
"expected_amount": 45,
"expected_iban": "ES7921000813610123456789",
"expected_reference": "INSCRIPCION 123"
}
}[
{ "code": "type_match", "severity": "info", "message": "The document is Payment / bank transfer receipt." },
{ "code": "expected_amount", "severity": "info", "message": "amount matches the expected value." },
{ "code": "expected_iban", "severity": "info", "message": "iban matches the expected value." },
{ "code": "expected_reference", "severity": "info", "message": "reference matches the expected value." }
]Type-specific rules
Some rules always apply, without asking:
| Type | Code | Severity |
|---|---|---|
medical_certificate_sport | not_fit_for_sport | error if the certificate does not declare the holder fit. |
es_sexual_offences_certificate, es_criminal_record_certificate | has_records | info with no records, error with records. |
Deterministic validations
On top of what you ask for, Constaia runs algorithmic validations (no AI) depending on the type. They appear in
checks[]:
"checks": [
{ "code": "nif_check_digit", "passed": true, "message": "The check letter of 12345678Z is correct." },
{ "code": "mrz_checksums", "passed": true, "message": "The MRZ check digits are correct." },
{ "code": "mrz_matches_visual", "passed": true, "message": "The MRZ matches the printed data." }
]| Code | What it verifies |
|---|---|
nif_check_digit | Check letter of every DNI, NIE or CIF in the document (holder, seller, buyer…). Numbers without a Spanish format (passports, foreign IDs) are skipped. |
mrz_checksums | Check digits of the MRZ (DNI, NIE/TIE, passport). On a one-sided DNI or NIE, without MRZ, the side_missing signal is added. |
mrz_matches_visual | With a valid MRZ: that number, birth date, expiry date and name match the printed data. |
iban_checksum | Check digits of each IBAN (one entry per IBAN: payer, beneficiary…). |
invoice_totals | That the invoice lines, tax base, VAT, withholding and total add up. |
csv_format | Only the format of the secure verification code (CSV) on Spanish certificates. |
date_consistency | Date consistency: birth before issue, issue before expiry, birth not in the future. Only appears when it fails. |
id_number_checksum | Check digit of a national identifier in the catalogue (CPF, CURP, codice fiscale, PESEL, ABA routing number…). |
id_number_format | Format of an identifier without a check digit: each US state's license number, SSN, EIN, USCIS number, ZIP code… |
id_number_matches_birth_date | The birth date encoded in the identifier (CURP, PESEL, CNP, codice fiscale…) matches the printed one. Only appears when it fails. |
aamva_matches_visual | On US and Canadian licenses and IDs: the PDF417 barcode (AAMVA) on the back, read on Constaia's servers, matches the printed front. |
Which validators each type runs is listed in validators in GET /v1/document-types/{type}.
When a validation fails (passed: false), a reason with the same code and severity error is added to
verdict.reasons, so the verdict becomes invalid. The affected field also gets
fields.<field>.validated: false; if it passes, validated: true. Fields without a validator have
validated: null.
csv_format does not query the Ministry
csv_format checks that the code has a valid format, not that the certificate exists. Constaia does not query the
Spanish Ministry of Justice verification service. If you need that guarantee, verify the CSV on the issuer's official
website.
Strict options: 422
options and options.checks are strict. An unknown key, a wrong type or an out-of-range value returns 422 with
code invalid_parameter. param tells you where the problem is:
- Invalid value: the full path, e.g.
options.checks.max_age_days. - Unknown key: the path of the object that contains it (
optionsoroptions.checks), andmessagenames the key.
{
"error": {
"type": "invalid_request",
"code": "invalid_parameter",
"message": "Unrecognized key: \"not_expire\"",
"param": "options.checks",
"request_id": "req_01J..."
}
}That way a typo (not_expire, holder.name) is never silently ignored. In the JavaScript SDK it arrives as
InvalidRequestError; in PHP as InvalidRequestException. See errors.
Next steps
Verdicts and reasons
How the valid, invalid or review verdict is computed from reason severities, the full table of stable reason codes and what to do in each case.
Digital signature verification in PDF
How Constaia verifies the PAdES electronic signature of a PDF: integrity, chain of trust and later changes, the signature object, require_valid_signature and what it doesn't check yet.