Constaia
Concepts

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.

Esta página ainda não está traduzida para o seu idioma. Mostramos a versão em inglês.

The verdict answers one specific question: "is this document a valid X according to my rules?". It appears in verdict when you state what you expect with expect. Without expect, verdict is null and you only get the classification (document), the fields (fields), the deterministic checks (checks) and the signals (warnings).

The verdict object

POST /v1/analyze response (excerpt)
"verdict": {
  "expected": ["es_dni"],
  "match": true,
  "status": "valid",
  "reasons": [
    { "code": "type_match", "severity": "info", "message": "The document is Spanish ID card (DNI)." },
    { "code": "not_expired", "severity": "info", "message": "Valid until 12/03/2031." }
  ]
}
FieldTypeDescription
expectedstring[]The types you asked for in expect, always as a list.
matchbooleantrue if the detected type (document.type) is one of the expected ones.
statusvalid | invalid | reviewThe decision.
reasonsobject[]Every rule evaluated, with a stable code, a severity and a human-readable message.

Each reason looks like this:

FieldTypeDescription
codestringStable code. Write your logic against it.
severityinfo | warning | errorinfo = the rule holds; warning = it cannot be confirmed; error = the rule fails.
messagestringText for people in the language you chose (es, en, pt, fr). Wording may change: do not compare it as a string.

The three statuses

StatusWhen
VálidoNo reason has severity warning or error.
No válidoAt least one reason has severity error: different type, expired, holder mismatch, wrong NIF check letter, wrong amount…
RevisarNo error, but at least one warning: low quality, confidence below the type's threshold, a value that could not be read…

The rule is mechanical and always the same:

How verdict.status is computed
if any reason has severity "error"      → invalid
else if any has severity "warning"      → review
else                                     → valid

error beats warning: an expired DNI that is also blurry is invalid, not review.

review is not an API failure. It means Constaia does not have enough information to decide safely, and a person should look at it or you should ask for another photo. See the human review guide.

Which reasons appear

  1. Type: always exactly one of type_match, type_mismatch or type_unknown.
  2. Checks you asked for (and those on by default, such as not_expired for documents with an expiry date): one or more reasons each. Details in checks.
  3. Type-specific rules: not_fit_for_sport (medical certificate saying "not fit"), has_records (criminal record certificates).
  4. Failed deterministic checks: if an item in checks[] has passed: false, a reason with the same code and severity error is added. Passing checks only appear in checks[], not in reasons.
  5. Quality and confidence: low_quality and low_confidence, with severity warning.
  6. PDF: the electronic signature result (signature_valid, signature_invalid…) and edited_suspected when the metadata points to editing. See digital signatures in PDF.
  7. Form I-9: i9_list (info) on US documents from lists A, B or C.

Reason code table

These codes are stable. We may add new codes without notice (see versioning): treat any unknown code according to its severity.

CodePossible severitiesWhen it appears
type_matchinfoThe detected type is one of those in expect.
type_mismatcherrorAnother catalogue type was detected with confidence.
type_unknownwarningThe type is not recognised (generic) or classification confidence is below 0.5.
not_expiredinfo, warning, errorValidity. warning if the expiry date could not be read.
max_age_daysinfo, warning, errorAge of the issue date. warning if there is no issue date.
ageinfo, warningAge within the limits you set (info) or unreadable birth date (warning).
min_age_yearserrorThe holder is younger than min_age_years.
max_age_yearserrorThe holder is older than max_age_years.
holderinfo, warning, errorMatch against the holder data. warning if the document does not show that value.
required_field_missingerrorA field from require_fields is missing (one reason per field).
require_signatureinfo, errorSignature present or missing.
require_stampinfo, errorStamp present or missing.
expected_amountinfo, errorAmount (amount, or total for invoice) equal to or different from the expected one.
expected_ibaninfo, errorThe expected IBAN appears, or not, in the document.
expected_referenceinfo, errorThe expected reference appears, or not, in reference or concept.
not_fit_for_sporterrorSports medical certificate stating "not fit".
has_recordsinfo, errorCriminal record certificate: no records (info) or records (error).
low_qualitywarningBlurry, cropped, glare, photo of a screen, photocopy, possible editing or several documents.
low_confidencewarningOverall confidence (classification and key fields) is below the type's threshold.
nif_check_digiterrorWrong check letter in a DNI, NIE or CIF.
mrz_checksumserrorWrong MRZ check digits.
mrz_matches_visualerrorThe MRZ does not match the printed data.
iban_checksumerrorIBAN with wrong check digits.
invoice_totalserrorInvoice base, VAT, withholding and total do not add up.
csv_formaterrorThe secure verification code (CSV) does not have a valid format.
date_consistencyerrorInconsistent dates (birth after issue, issue after expiry…).
id_number_checksum, id_number_formatwarning, errorA national identifier fails its check digit or format. warning for identifiers marked as soft in the catalogue.
id_number_matches_birth_dateerrorThe birth date encoded in the identifier doesn't match the printed one.
aamva_matches_visualerrorThe PDF417 barcode on the back of a US/Canadian license or ID doesn't match the front.
signature_validinfoThe PDF carries an intact, trusted electronic signature. See digital signatures in PDF.
signature_invaliderrorThe PDF signature is broken: the content doesn't match what was signed.
signature_missingwarning, errorThe PDF has no electronic signature (for types that are usually signed), or require_valid_signature was requested and it is not a signed PDF.
document_modified_after_signingwarning, errorThe PDF was modified after being signed. error with require_valid_signature.
untrusted_signerwarning, errorThe signature is intact but the certificate doesn't chain to the trust list. error with require_valid_signature.
edited_suspectedwarningThe PDF metadata points to an editor (iLovePDF, Word, Canva…) or to later changes.
i9_listinfoUS document acceptable for Form I-9: tells you the list (A, B or C).

Same code, different severity

The code tells you which rule was evaluated; the severity tells you how it went. A valid DNI and an expired one produce the same code:

Valid DNI
{ "code": "not_expired", "severity": "info", "message": "Valid until 12/03/2031." }
Expired DNI → invalid
{ "code": "not_expired", "severity": "error", "message": "Expired on 15/06/2020." }

So your code must always look at the code + severity pair, never just at whether the code is present.

Signals (warnings)

warnings is a list of signals about the image or the document. They are not proof of fraud: they are hints to decide whether to ask for another photo or review by hand. Constaia is not biometric KYC and does no face matching.

CodeMeaning
low_qualityOverall low quality.
blurryBlurry image.
croppedThe document is cropped.
glareGlare hides data.
screen_photo_suspectedPossible photo of a screen.
photocopy_suspectedPossible photocopy.
edited_suspectedPossible digital edit.
multiple_documentsMore than one document in the file.
side_missingOne side is missing.
language_mismatchThe language is not the expected one for the type.

Warnings are signals, not proof of authenticity.

The quality signals (low_quality, blurry, cropped, glare, screen_photo_suspected, photocopy_suspected, edited_suspected, multiple_documents) are summarised in a single low_quality reason with severity warning, so they push the verdict to review (unless there is an error). side_missing and language_mismatch appear in warnings but do not change the verdict on their own.

Blurry photo → review
"verdict": {
  "expected": ["es_dni"],
  "match": true,
  "status": "review",
  "reasons": [
    { "code": "type_match", "severity": "info", "message": "The document is Spanish ID card (DNI)." },
    { "code": "not_expired", "severity": "info", "message": "Valid until 12/03/2031." },
    { "code": "low_quality", "severity": "warning", "message": "Image quality is insufficient (blurry, low_quality)." }
  ]
},
"warnings": ["blurry", "low_quality"]

If the image is so bad that it is rejected before OCR, document is null, match is false, the only reason is low_quality (warning) and the analysis is not charged.

What to do with each verdict

VerdictRecommended action
validAccept automatically. Store the id and the fields you need.
invalidReject and show the user the message of reasons with severity: "error" so they upload another document.
reviewAccept provisionally or ask for another photo, and queue it for human review.

Also remember the analysis may not be finished: with async: true, or if it takes longer than 30 s, you get 202 with status: "queued" or "processing" and the result arrives by webhook.

src/check-dni.ts
import { Constaia, InvalidRequestError } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";

const constaia = new Constaia(); // reads CONSTAIA_API_KEY

type Decision =
  | { action: "accept"; documentNumber: string | undefined }
  | { action: "reject"; errors: string[] }
  | { action: "review"; warnings: string[] }
  | { action: "pending"; id: string };

export async function checkDni(path: string, fullName: string): Promise<Decision> {
  const analysis = await constaia.analyze(await fromPath(path), {
    expect: "es_dni",
    checks: { holder: { fullName } },
    language: "en",
  });

  if (analysis.status !== "completed") {
    return { action: "pending", id: analysis.id }; // will arrive by webhook
  }

  const reasons = analysis.verdict?.reasons ?? [];
  switch (analysis.verdict?.status) {
    case "valid":
      return { action: "accept", documentNumber: analysis.fields.document_number?.value as string | undefined };
    case "invalid":
      return {
        action: "reject",
        errors: reasons.filter((r) => r.severity === "error").map((r) => r.message),
      };
    default:
      return {
        action: "review",
        warnings: reasons.filter((r) => r.severity === "warning").map((r) => r.message),
      };
  }
}

try {
  console.log(await checkDni("./dni_valid.jpg", "María García López"));
} catch (err) {
  if (err instanceof InvalidRequestError) console.error(err.code, err.param, err.message);
  else throw err;
}

React to specific codes when you need to

You can refine the decision per code. For example, on not_expired with error ask for a document that is still valid; on holder with error, say the document does not belong to the registered person. For any code you do not know, decide by its severity only.

Several accepted types

With expect as a list, match is true if the document is any of them. This is the usual setup for "identity document": DNI, NIE/TIE or passport.

Request
{ "file_url": "https://example.com/nie.jpg", "expect": ["es_dni", "es_nie", "passport"], "language": "en" }
Response (excerpt)
"document": { "type": "es_nie", "label": "Spanish foreigner ID (NIE / TIE)", "confidence": 0.97, "side": "both", "country": "ESP" },
"verdict": {
  "expected": ["es_dni", "es_nie", "passport"],
  "match": true,
  "status": "valid",
  "reasons": [
    { "code": "type_match", "severity": "info", "message": "The document is Spanish foreigner ID (NIE / TIE)." },
    { "code": "not_expired", "severity": "info", "message": "Valid until 30/11/2029." }
  ]
}

Checks are evaluated against the detected type, so look at document.type to know which field holds the number (document_number for DNI and passport, nie_number for NIE). You can look up the fields of each type with GET /v1/document-types/{type} or in the catalogue.

Without expect: verdict is null

If you don't send expect, Constaia classifies and extracts, but does not decide:

Response without expect, language en (excerpt)
"document": { "type": "generic", "label": "Other document", "confidence": 0.64, "side": null, "country": null },
"verdict": null,
"checks": [],
"warnings": []

Use it when you don't know which document will be uploaded and want to route on document.type, or when you apply your own rules to fields. If you only need the type, POST /v1/classify costs 0.2 credits; its verdict only contains type_match or type_mismatch.

Next steps

Nesta página