Constaia
Concepts

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

Terminal
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 SDKTypeDefaultReason produced
not_expirednotExpiredbooleantrue for types with an expiry datenot_expired
reference_datereferenceDate"YYYY-MM-DD"today(shifts the date of the other rules)
max_age_daysmaxAgeDaysinteger 0–36500—max_age_days
min_age_yearsminAgeYearsinteger 0–150—age or min_age_years
max_age_yearsmaxAgeYearsinteger 0–150—age or max_age_years
holderholderobject—holder
require_fieldsrequireFieldsstring[] (max. 50)—required_field_missing
require_signaturerequireSignatureboolean—require_signature
require_stamprequireStampboolean—require_stamp
require_valid_signaturerequireValidSignatureboolean—signature_valid, signature_invalid, signature_missing, document_modified_after_signing, untrusted_signer
expected_amountexpectedAmountnumber—expected_amount
expected_ibanexpectedIbanstring (max. 40)—expected_iban
expected_referenceexpectedReferencestring (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_nie and passport). Send "not_expired": false to turn it off.
  • Only evaluated when the type has an expiry date.
  • info if valid, error if expired, warning if the date could not be read.
Expired DNI
{ "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?".

Options
{ "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.

  • info if it passes, error if older (or if the issue date is more than one day in the future), warning if there is no issue date.
Certificate issued 14 days ago (language: en)
{ "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 age reason with info ("The holder is 36 years old.").
  • Younger than the minimum: min_age_years with error. Older than the maximum: max_age_years with error.
  • Birth date unreadable: age with warning.

holder

Compares the document data with the person you expect. Every sub-field is optional; send only what you have.

Sub-field (API)JS SDKHow it is compared
full_namefullNameFull name with normalisation (see below).
first_namefirstNameWith last_name, as a full name. On its own, its words just need to appear in the document name.
last_namelastNameSame as first_name.
document_numberdocumentNumberNormalised number (no spaces or hyphens, case-insensitive).
birth_datebirthDateExact 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_name or only last_name, particles (de, del…) are ignored.

Outcome:

  • Everything you sent matches: one holder reason with info listing the fields ("The holder details match (full_name).").
  • Each value that does not match: holder with error, showing what was read and what was expected.
  • Each value the document does not show or that could not be read: holder with warning.
Different holder (language: en)
{ "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 gives signature_invalid (error) and the other problems are warning.
  • With it, signature_missing, document_modified_after_signing and untrusted_signer become error. 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):

OptionWhat it compares
expected_amountamount (or total for invoice) with a tolerance of half a cent.
expected_ibanThat the IBAN appears among those in the document (payer or beneficiary). Spaces and case ignored.
expected_referenceThat the normalised expected text is contained in reference or concept.
Options
{
  "expect": "payment_receipt",
  "language": "en",
  "checks": {
    "expected_amount": 45,
    "expected_iban": "ES7921000813610123456789",
    "expected_reference": "INSCRIPCION 123"
  }
}
Reasons
[
  { "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:

TypeCodeSeverity
medical_certificate_sportnot_fit_for_sporterror if the certificate does not declare the holder fit.
es_sexual_offences_certificate, es_criminal_record_certificatehas_recordsinfo 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[] of a DNI (language: en)
"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." }
]
CodeWhat it verifies
nif_check_digitCheck letter of every DNI, NIE or CIF in the document (holder, seller, buyer…). Numbers without a Spanish format (passports, foreign IDs) are skipped.
mrz_checksumsCheck 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_visualWith a valid MRZ: that number, birth date, expiry date and name match the printed data.
iban_checksumCheck digits of each IBAN (one entry per IBAN: payer, beneficiary…).
invoice_totalsThat the invoice lines, tax base, VAT, withholding and total add up.
csv_formatOnly the format of the secure verification code (CSV) on Spanish certificates.
date_consistencyDate consistency: birth before issue, issue before expiry, birth not in the future. Only appears when it fails.
id_number_checksumCheck digit of a national identifier in the catalogue (CPF, CURP, codice fiscale, PESEL, ABA routing number…).
id_number_formatFormat of an identifier without a check digit: each US state's license number, SSN, EIN, USCIS number, ZIP code…
id_number_matches_birth_dateThe birth date encoded in the identifier (CURP, PESEL, CNP, codice fiscale…) matches the printed one. Only appears when it fails.
aamva_matches_visualOn 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 (options or options.checks), and message names the key.
422: unknown key in checks
{
  "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

On this page