Constaia
Endpoints

Reviews

Reference for human review through the API. List pending analyses with GET /v1/reviews, download the original with GET /v1/analyses/{id}/file and decide with POST /v1/analyses/{id}/review.

When an analysis needs a person to look at it, you can resolve it from the dashboard or from your own backend with these endpoints. Step-by-step guide in Human review from your backend.

Method and pathWhat it does
GET /v1/reviewsLists analyses pending review (or already decided).
GET /v1/analyses/{id}/fileDownloads the original document, if it is still stored.
POST /v1/analyses/{id}/reviewApproves or rejects an analysis pending review.

They only work with a secret key (ck_test_… or ck_live_…). With a publishable key (pk_…) the response is 403 publishable_key_not_allowed.

When an analysis is pending review

An analysis with a verdict enters the review queue (review.status: "pending") when:

  • its verdict is review;
  • its verdict is invalid and the template used asks for human review of invalid documents;
  • its verdict is valid and the template used does not approve valid documents on its own.

While it is pending, verdict.final_status is null. Once decided it becomes valid (approved) or invalid (rejected). Without expect there is no verdict and the analysis cannot be reviewed (409 not_reviewable).

The analysis review object:

review (pending)
"review": {
  "status": "pending",
  "decision": null,
  "assignee": null,
  "reviewed_by": null,
  "reviewed_at": null,
  "reason": null,
  "note": null
}
FieldDescription
statuspending, approved or rejected.
decisionapprove, reject or null if not decided yet.
assigneeTeam member it was assigned to in the dashboard, or null.
reviewed_byWho decided: the id of a dashboard user or, if decided through the API, the key id (key_…).
reviewed_atWhen it was decided.
reasonShort reason for the decision (up to 200 characters).
noteInternal note (up to 2000 characters).

review is null for analyses that need no review.

List reviews

GET /v1/reviews
ParameterTypeDescription
statuspending | decided | approved | rejected | allDefaults to pending. decided means approved and rejected.
typestringFilters by document type, e.g. es_dni.
templatestringFilters by template (tpl_…).
metadata[key]stringFilters by a metadata value. Repeatable: all must match.
limitinteger 1–100Items per page. Default 10.
starting_afterstringId (an_…) of the last analysis of the previous page.

It only includes analyses of your key's mode (test or live). Order:

  • pending: oldest first, like a queue: handle first what has been waiting longest.
  • The rest: newest first.
Response
{
  "object": "list",
  "data": [
    {
      "id": "an_01J9Z8Q3K4M5N6P7Q8R9S0T1V3",
      "object": "analysis",
      "status": "completed",
      "verdict": { "status": "review", "final_status": null, "…": "…" },
      "review": { "status": "pending", "decision": 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" }
    }
  ],
  "has_more": false,
  "url": "/v1/reviews"
}

Each item is the full analysis object. Pagination works like every other list: see Pagination.

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

Download the original

GET /v1/analyses/{id}/file

Returns the original document as it was uploaded, only if it is still stored (storage.kept: true). With the default storage mode (review) that is the case while the analysis awaits review and during the margin after the decision. See Storage and privacy.

200 response with the binary:

HeaderValue
Content-TypeThe original's: image/jpeg, image/png, image/webp, image/heic or application/pdf.
Content-Dispositioninline; filename="an_….jpg"
Cache-Controlprivate, no-store

Errors:

HTTPcodeWhen
404resource_missingThe analysis doesn't exist or isn't from your account.
404file_not_storedThe original was not stored (no review was needed or it was analysed with storage: "none") or it was already deleted after its window. param is id. The extracted data is still available.

Keyless alternative: while the original is stored, the analysis file_url field holds a signed URL that expires after 15 minutes. It is handy to show the document on your review screen; don't store it, fetch the analysis again when you need a new one.

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

Decide a review

POST /v1/analyses/{id}/review
FieldTypeDescription
decisionapprove | rejectRequired.
reasonstring (≤ 200) | nullShort reason, e.g. "Data matches the form". Optional.
notestring (≤ 2000) | nullInternal note. Optional.

Responds 200 with the updated analysis object:

  • review.status: approved or rejected; review.decision: approve or reject.
  • review.reviewed_by: the id of the key you decided with (key_…); review.reviewed_at, review.reason and review.note.
  • verdict.final_status: valid if you approve, invalid if you reject.
  • storage.expires_at becomes the decision time plus review_retention_hours_after_decision (24 h by default). If that window is 0, the original is deleted on decision and storage.kept becomes false.

The analysis.reviewed webhook is also sent, and the decision is recorded in the account's audit log under the API key.

Response (excerpt)
{
  "id": "an_01J9Z8Q3K4M5N6P7Q8R9S0T1V3",
  "object": "analysis",
  "verdict": { "status": "review", "final_status": "valid", "…": "…" },
  "review": {
    "status": "approved",
    "decision": "approve",
    "assignee": null,
    "reviewed_by": "key_01J9Z8Q3K4M5N6P7Q8R9S0T1V9",
    "reviewed_at": "2026-09-30T08:12:40Z",
    "reason": "Data matches the form",
    "note": null
  },
  "storage": { "mode": "review", "kept": true, "reason": null, "expires_at": "2026-10-01T08:12:40Z", "file_deleted_at": null }
}

Safe retries

  • Send the Idempotency-Key header: the same key with the same body returns the same response, with the Idempotent-Replayed: true header. See Idempotency.
  • Repeating the same decision on an already decided analysis returns the analysis as is (200) and sends no new webhook.
  • A different decision on an already decided analysis returns 409 already_reviewed. Only an owner or admin can change it, from the dashboard.

Errors

HTTPcodeWhen
403publishable_key_not_allowedYou used a publishable key (pk_…).
404resource_missingThe analysis doesn't exist or isn't from your account.
409already_reviewedThe analysis already has a different decision.
409not_reviewableThe analysis has no verdict: no expect was given or it hasn't finished yet.
422invalid_parameterThe body is not valid (decision missing or not approve/reject, reason or note too long).
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": "approve", "reason": "Data matches the form" }'

Templates and retention windows

If you use templates, each one can set its own storage and review windows. The template object always includes these fields:

FieldTypeDescription
storagenone | review | temporary | persistentStorage mode of analyses made with the template.
review_retention_hours_after_decisioninteger 0–720 | nullHours the original is kept after the decision. null = the account value (24 by default).
review_max_daysinteger 1–90 | nullMaximum days a pending original is kept if nobody decides. null = the account value (30 by default).

Account values are changed in the dashboard (Account settings).

Next steps

On this page