Constaia
Use-case guides

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 warningCause
low_qualityThere are quality or authenticity warnings in warnings (see the table below).
low_confidenceConfidence in the type or key fields is low: "Confidence is low (0.62); a manual review is recommended.".
type_unknownThe document type could not be identified.
not_expired, max_age_days, ageYou asked for the check but the expiry, issue or birth date can't be found.
holderYou 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:

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.

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 valid or invalid, delete it right away; if it's review, keep it until the reviewer decides.
  • Store nothing and ask for another photo: if you don't want to hold documents, on review ask for a new document straight away (step 5). It's the simplest option when the only reason is low_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:

SourceEvents you receive
Single analysis (synchronous or async)analysis.completed and analysis.review_required
Document in a batchOnly 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.

migrations/review_queue.sql
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()
);
webhooks.js
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 the language you 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 with GET /v1/analyses/{id}; otherwise, store in the queue the fields the reviewer has to see.
  • fields.<field>.source.bbox (normalised 0–1 coordinates on page source.page), in case you want to highlight where each value is on the image. It can be null.

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:

decision.sql
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).

Coming soon: human review managed by Constaia

We're preparing human review performed by the Constaia team for documents in review, at 0.40 € per document. It isn't available yet: today you run the review yourself with this flow. See pricing and the changelog.

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):

response (excerpt)
{
  "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

On this page