Constaia
Use-case guides

Human review from your backend

Bring human review into your own application with the API. Get pending reviews, download the original document, record the decision and apply the final result.

When Constaia can't decide on its own, the analysis is left pending human review. Your team can review it in the dashboard, but if you already have your own internal tool (a back office, a CRM, a validation screen) you can do everything from your backend:

  1. Analyse the document with expect.
  2. Find out that it needs review (webhook or periodic polling).
  3. Download the original document to show it to the reviewer.
  4. Record the decision at Constaia.
  5. Apply the final result.

With the default storage, Constaia only keeps the document, encrypted, while it needs reviewing, and deletes it once decided. You don't have to hold a copy yourself.

The flow

Your server                                          Constaia
───────────                                          ────────
1. POST /v1/analyze  { expect, metadata } ─────────▶ review verdict → review.status: "pending"
                                                     the original is stored encrypted (storage.kept: true)
2. /webhooks/constaia ◀── analysis.review_required
   or      GET /v1/reviews?status=pending ─────────▶ list of pending reviews, oldest first
3. GET /v1/analyses/{id}/file ─────────────────────▶ the original document (or file_url, 15 min)
4. POST /v1/analyses/{id}/review  { decision } ────▶ verdict.final_status: valid | invalid
   (with Idempotency-Key)                            the original is deleted 24 h later (adjustable)
5. /webhooks/constaia ◀── analysis.reviewed

These endpoints only work with a secret key (ck_…), never from the browser. Full reference in Reviews.

Step 1: analyse with expect

Review is about the verdict, so the analysis needs expect: without it there is nothing to approve or reject (409 not_reviewable). Use metadata to link the analysis to the record in your system.

You don't need to send storage: the default mode (review) keeps the original only if the analysis ends up pending review. If you send storage: "none", the document is never stored and the reviewer will only be able to see the extracted data.

What ends up pending review:

  • By default, analyses with a review verdict.
  • If you use a template (template: "tpl_…"), its review policy can also send invalid ones, or every valid one if you'd rather none are approved automatically. The template can also set its own retention windows.
Terminal
curl https://api.constaia.com/v1/analyze \
  -H "Authorization: Bearer $CONSTAIA_API_KEY" \
  -F file=@blurry.jpg \
  -F 'options={"expect":["es_dni","es_nie","passport"],"metadata":{"registration_id":"124"}}'
Response (excerpt)
{
  "id": "an_01J9Z8Q3K4M5N6P7Q8R9S0T1V3",
  "verdict": { "status": "review", "final_status": null, "…": "…" },
  "review": { "status": "pending", "decision": null, "assignee": null, "reviewed_by": null, "reviewed_at": null, "reason": null, "note": null },
  "storage": { "mode": "review", "kept": true, "reason": "pending_review", "expires_at": "2026-10-29T10:05:01Z", "file_deleted_at": null },
  "file_url": "https://api.constaia.com/v1/files/…",
  "metadata": { "registration_id": "124" }
}

Step 2: find out about pending reviews

There are two ways, and you can combine them:

  • analysis.review_required webhook: arrives when an analysis finishes with a review verdict. It's the most immediate. See Webhooks.
  • Poll GET /v1/reviews: returns every pending analysis, oldest first, like a queue. It also includes the ones a template sends to review even when their verdict is valid or invalid. Use it in a periodic job or to build the inbox of your internal tool.

If you use both, identify each item by the analysis id so it isn't duplicated.

Terminal
curl -G https://api.constaia.com/v1/reviews \
  -H "Authorization: Bearer $CONSTAIA_API_KEY" \
  --data-urlencode "status=pending" \
  --data-urlencode "limit=50"

Filter with type, template or metadata[key] if each team reviews a different document type or event.

Step 3: show the document to the reviewer

While the analysis is pending, the original is still stored (storage.kept: true). Two ways to get it:

  • The analysis file_url: a signed URL that expires after 15 minutes. Use it to show the image or PDF directly on your review screen. Don't store it: fetch the analysis again with GET /v1/analyses/{id} when you need a new one.
  • GET /v1/analyses/{id}/file: downloads the file with your key, for example to serve it yourself from your backend. The Content-Type is the original's (image/jpeg, image/png, image/webp, image/heic or application/pdf).

Next to the document, show the verdict reasons (verdict.reasons with warning or error) and the extracted fields (fields), so the reviewer can compare.

If the original is no longer stored, the download returns 404 file_not_stored: no review was needed, it was analysed with storage: "none" or it was already deleted after its window. The extracted data is still available, so the reviewer can decide with it or ask for a new document.

Terminal
curl -o original \
  https://api.constaia.com/v1/analyses/an_01J9Z8Q3K4M5N6P7Q8R9S0T1V3/file \
  -H "Authorization: Bearer $CONSTAIA_API_KEY"

Step 4: record the decision

When the reviewer decides, send approve or reject with a short reason (reason, up to 200 characters) and, optionally, an internal note (note, up to 2000). Always send an Idempotency-Key: if the network fails and you retry, the decision isn't duplicated.

  • Approving sets verdict.final_status to valid; rejecting, to invalid.
  • review.reviewed_by holds your API key id (key_…), and the decision is saved in the account's audit log under that key. Store in your system which person decided, or put it in note.
  • Repeating the same decision returns the analysis as is, with no error.
Terminal
curl https://api.constaia.com/v1/analyses/an_01J9Z8Q3K4M5N6P7Q8R9S0T1V3/review \
  -H "Authorization: Bearer $CONSTAIA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: review-an_01J9Z8Q3K4M5N6P7Q8R9S0T1V3" \
  -d '{ "decision": "reject", "reason": "The expiry date is not readable in the photo", "note": "Reviewed by Ana" }'

Step 5: apply the result

Apply the outcome in your system with verdict.final_status: valid, confirm; invalid, reject or ask for another document. You can do it with the response to the decision or with the analysis.reviewed webhook, which is sent whenever a review is decided, from the dashboard or through the API. If part of your team reviews in the dashboard, listening to this webhook keeps your system up to date.

Inside your webhook handler
if (event.type === "analysis.reviewed") {
  const analysis = event.data;
  await applyDecision(analysis.metadata.registration_id, analysis.verdict.final_status);
}

What happens to the document afterwards

  • Once decided, the original is kept for 24 more hours (so the decision can be checked) and then deleted. storage.expires_at tells you when.
  • If nobody decides, it is deleted 30 days after the analysis finished. The extracted data is still available.
  • Both windows are changed in the dashboard (Account settings) and, if you need to, per template: review_retention_hours_after_decision (0–720 hours; with 0 it is deleted on decision) and review_max_days (1–90 days).

More detail in Storage and privacy.

Common errors

HTTPcodeWhat it meansWhat to do
404file_not_storedThe original isn't stored (no review was needed, storage: "none" or already deleted).Review with the extracted data or ask for a new document.
409already_reviewedThe analysis already has a different decision.Don't retry. Only an owner or admin can change it, from the dashboard.
409not_reviewableThere is no verdict: it was analysed without expect or hasn't finished yet.Analyse with expect or wait for it to finish.
403publishable_key_not_allowedYou used a publishable key (pk_…).Use the secret key, always from your backend.

Every code in Errors.

Test it

With a ck_test_… key, analyse an image called blurry.jpg with expect: "es_dni": the verdict is review and the analysis shows up in GET /v1/reviews. Decide it with POST /v1/analyses/{id}/review and, if you have a test webhook endpoint, you will receive analysis.review_required and then analysis.reviewed. More in Test mode.

Next steps

On this page