Human review
What to do with the review verdict. Why it happens, receiving it by webhook, building a review queue, recording the decision and asking for a new photo.
The Revisar verdict means Constaia can't decide with confidence: the photo is blurry, a field can't be read or confidence is low. It isn't a rejection. Most of the time the document is fine and all it takes is a person looking at it, or asking for another photo.
This guide builds that loop in your application: detect review, put it in a queue, show it to a reviewer, store the
decision and, if needed, ask for a new document.
When review happens
The verdict is review when there is at least one reason with severity: "warning" and none with error:
code with warning | Cause |
|---|---|
low_quality | There are quality or authenticity warnings in warnings (see the table below). |
low_confidence | Confidence in the type or key fields is low: "Confidence is low (0.62); a manual review is recommended.". |
type_unknown | The document type could not be identified. |
not_expired, max_age_days, age | You asked for the check but the expiry, issue or birth date can't be found. |
holder | You asked to compare a holder field and it couldn't be read. |
If the image is so poor that it's rejected before OCR, the analysis ends with document: null, review and the
low_quality reason. Those analyses aren't charged.
Warnings that can appear in warnings:
| Código | Significado |
|---|---|
low_quality | Qualidade baixa em geral. |
blurry | Imagem desfocada. |
cropped | O documento está cortado. |
glare | Reflexos que tapam dados. |
screen_photo_suspected | Possível fotografia de um ecrã. |
photocopy_suspected | Possível fotocópia. |
edited_suspected | Possível edição digital. |
multiple_documents | Há mais de um documento no ficheiro. |
side_missing | Falta uma face. |
language_mismatch | O idioma não é o esperado para o tipo. |
Os warnings são indícios, não prova de autenticidade.
Warnings are signals, not proof of fraud. Constaia is not biometric KYC and does no face matching.
Step 1: decide where the reviewer gets the document
A reviewer needs to see the document. Constaia has no endpoint to download the original file you uploaded (only data exports), so you have two options:
- Keep your own copy only as long as needed: when you receive the file, store it encrypted in your storage. If the
verdict is
validorinvalid, delete it right away; if it'sreview, keep it until the reviewer decides. - Store nothing and ask for another photo: if you don't want to hold documents, on
reviewask for a new document straight away (step 5). It's the simplest option when the only reason islow_quality.
Keeping the file at Constaia (storage: "temporary") doesn't help your reviewer, because they can't download it. If you
send storage: "none", the usual choice, remember that the reference copy is yours.
Step 2: receive the review
In a synchronous analysis review comes in the response itself. Constaia also sends the analysis.review_required
webhook:
| Source | Events you receive |
|---|---|
Single analysis (synchronous or async) | analysis.completed and analysis.review_required |
| Document in a batch | Only analysis.review_required (and batch.completed at the end of the batch) |
If you handle both the response and the webhooks, create the queue item with the analysis id as a unique key so it
isn't duplicated. Use metadata to know which record in your system it belongs to.
CREATE TABLE review_queue (
analysis_id text PRIMARY KEY,
registration_id text NOT NULL,
reasons jsonb NOT NULL,
warnings jsonb NOT NULL,
file_path text,
status text NOT NULL DEFAULT 'pending', -- pending | approved | rejected | new_document_requested
reviewer text,
decided_at timestamptz,
note text,
created_at timestamptz NOT NULL DEFAULT now()
);import express from "express";
import { Constaia, WebhookVerificationError } from "@constaia/sdk";
import { sql } from "./db.js";
const app = express();
const constaia = new Constaia();
app.post("/webhooks/constaia", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = await constaia.webhooks.verify(req.body, req.headers, process.env.CONSTAIA_WEBHOOK_SECRET);
} catch (err) {
if (err instanceof WebhookVerificationError) return res.status(400).send("invalid signature");
throw err;
}
if (event.type === "analysis.review_required") {
const analysis = event.data;
await sql`
INSERT INTO review_queue (analysis_id, registration_id, reasons, warnings)
VALUES (${analysis.id}, ${analysis.metadata.registration_id},
${JSON.stringify(analysis.verdict.reasons.filter((r) => r.severity !== "info"))},
${JSON.stringify(analysis.warnings)})
ON CONFLICT (analysis_id) DO NOTHING`;
}
res.sendStatus(200);
});
app.listen(3000);Step 3: show the document to the reviewer
On the review screen show:
- Your copy of the document (step 1).
- The reasons with
warning: they explain why it wasn't decided automatically. They come in thelanguageyou asked for. - The extracted fields and their
confidence, so the reviewer can compare with the image. If you keep results (keep_results: true, the default), read them when needed withGET /v1/analyses/{id}; otherwise, store in the queue the fields the reviewer has to see. fields.<field>.source.bbox(normalised 0–1 coordinates on pagesource.page), in case you want to highlight where each value is on the image. It can benull.
Step 4: record the decision
The human decision is stored in your system: Constaia has no endpoint to change an analysis verdict. Store who
decided, when, what and why, and link it to the analysis_id:
UPDATE review_queue
SET status = 'approved', reviewer = 'ana@club.example', decided_at = now(),
note = 'ID readable on the copy; data matches the form'
WHERE analysis_id = 'an_01J...';Then apply the consequence (confirm the registration, reject it) and delete your copy of the document. If you stored
the file or results at Constaia and no longer need them, delete them with DELETE /v1/analyses/{id}.
Step 5: ask for a new photo
When the reason is quality (low_quality) or the reviewer can't read the document, ask for another one. Send the
person a link to your upload page (with the widget, which flags blurry, dark or low-resolution photos
before sending) and analyze again. The new analysis has a different id: store its link to the previous one and mark
the queue item as new_document_requested.
Tips you can show depending on the warning: blurry (hold the phone steady and focus), glare (avoid reflections, no
flash), cropped (all four corners visible), side_missing (upload both sides), screen_photo_suspected or
photocopy_suspected (photograph the original document).
Test it
With a ck_test_… key, an image named blurry.jpg (or containing blur or borros) returns the dni_valid ID with
the blurry and low_quality warnings (messages in Spanish, the default language):
{
"verdict": {
"expected": ["es_dni"],
"match": true,
"status": "review",
"reasons": [
{ "code": "type_match", "severity": "info", "message": "El documento es DNI (España)." },
{ "code": "not_expired", "severity": "info", "message": "Vigente hasta el 12/03/2031." },
{ "code": "low_quality", "severity": "warning", "message": "La calidad de la imagen es insuficiente (blurry, low_quality)." }
]
},
"warnings": ["blurry", "low_quality"]
}With "language": "en" the last message reads "Image quality is insufficient (blurry, low_quality).". As a single
analysis you receive analysis.completed and analysis.review_required; inside a batch, only
analysis.review_required. From the dashboard you can also send a test event to your endpoint. More in
Test mode.
Next steps
Analyze without storing
Data minimisation (GDPR) with Constaia. Analyze documents without keeping the file or the extracted data using storage none and keep_results false.
US documents
The United States document types in the Constaia catalogue, PDF417 (AAMVA) barcode reading on driver's licenses, Form I-9 lists, and validation of SSN, EIN and other identifiers.