Verification links
Reference for /v1/verification-links: create a hosted page where the person uploads their documents from their phone, receive the results by webhook, callback or email, and retrieve, list or cancel links.
A verification link is a page hosted by Constaia (url, with its QR code in qr_svg) where the person
accepts the consent and uploads each document you ask for, with their phone camera or from a file. Each upload is a
normal analysis in the key's mode (in live it costs credits) and they all go into a dossier (dossier_id) with
an overall verdict.
Step-by-step guide with callback, results email and the return to your website: Create links via API and receive the results.
| Method and route | What it does |
|---|---|
POST /v1/verification-links | Creates a link (and its dossier). |
GET /v1/verification-links | Lists the links in the key's mode. |
GET /v1/verification-links/{id} | Retrieves one, with qr_svg. |
DELETE /v1/verification-links/{id} | Cancels it: the page stops accepting documents. |
All routes use a secret key (ck_test_… or ck_live_…) from your backend.
Create a link
POST /v1/verification-links
Content-Type: application/json| Field | Type | Default | Description |
|---|---|---|---|
template | string | null | — | Id of one of the account's templates (tpl_…). Its analysis options apply to every document. 422 template_not_found if it doesn't exist. |
documents | object[] (1–10) | — | Documents requested, in order. Each one: key (1–40 characters: letters, numbers, - or _, unique), label (visible text, up to 120), expect (a type or list of types) and checks (as in analyze). If missing and there is a template, one document is requested (key: "document") with the template's expect. Without documents or template: 422 documents_required. |
reference | string | null | — | Your reference (up to 200 characters). Useful to filter the list. |
metadata | object string → string | {} | Up to 20 keys. Copied to the dossier and to each analysis. |
expires_in_hours | integer 1–720 | 72 | Hours of validity. Once they pass, the link becomes expired. |
locale | es | en | pt | fr | the template's or the request's language | Language of the page, the emails and the dossier messages. |
notify_email | email | null | — | Sends the link to the person by email, in locale. |
consent_text | string | null | the account's | Your own consent text (up to 2000 characters). |
show_result | boolean | the account's | Show the person the result of each document. |
redirect_url | https string | null | — | Where the person goes back to when they finish (button on the final screen). |
redirect_with_status | boolean | false | Appends ?link_id=vl_…&status=<link status> to redirect_url. |
face_verification | { enabled, required? } | null | the template's | Selfie step after the documents (optional module, EEA accounts with the addendum accepted; otherwise 403 face_verification_not_enabled). The link and the webhook include face_verification and face_match. See Face verification. |
callback_url | https string | null | — | Receives a signed POST with verification_link.completed and verification_link.expired. Must be https://; otherwise 422 invalid_url (param: "callback_url"). |
include_results | boolean | true | Include results (the analysis of each document) in the webhook and the callback. false: only the link and the dossier verdict. |
results_email | email | null | — | Email that receives a summary of the results when the link is completed, in locale. |
Fields are strict: an unknown one returns 422 invalid_parameter with its param.
curl https://api.constaia.com/v1/verification-links \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"documents": [
{ "key": "id_card", "label": "DNI or NIE", "expect": ["es_dni", "es_nie"], "checks": { "min_age_years": 18 } },
{ "key": "receipt", "label": "Payment receipt", "expect": "payment_receipt", "checks": { "expected_amount": 45 } }
],
"reference": "registration-4821",
"metadata": { "registration_id": "4821" },
"locale": "en",
"callback_url": "https://example.com/constaia/callback",
"results_email": "office@example.com",
"redirect_url": "https://example.com/registration/4821",
"redirect_with_status": true
}'{
"id": "vl_01J9Z8Q3K4M5N6P7Q8R9S0T1V2",
"object": "verification_link",
"url": "https://app.constaia.com/v/3kqX…",
"qr_svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" …</svg>",
"status": "pending",
"livemode": false,
"template": null,
"reference": "registration-4821",
"metadata": { "registration_id": "4821" },
"documents": [
{
"key": "id_card",
"label": "DNI or NIE",
"expect": ["es_dni", "es_nie"],
"checks": { "min_age_years": 18 },
"status": "pending",
"analysis_id": null,
"attempts": 0,
"verdict_status": null,
"final_status": null,
"warnings": []
},
{ "key": "receipt", "label": "Payment receipt", "…": "…" }
],
"dossier_id": "dos_01J9Z8Q3K4M5N6P7Q8R9S0T1V3",
"expires_at": "2026-10-03T10:00:00Z",
"redirect_url": "https://example.com/registration/4821",
"redirect_with_status": true,
"callback_url": "https://example.com/constaia/callback",
"include_results": true,
"results_email": "office@example.com",
"results_email_sent_at": null,
"locale": "en",
"notify_email": null,
"last_sent_at": null,
"show_result": false,
"consent_text": null,
"consent": null,
"events": [{ "type": "created", "at": "2026-09-30T10:00:00Z" }],
"created_at": "2026-09-30T10:00:00Z",
"completed_at": null,
"cancelled_at": null
}Send url to the person (or show them qr_svg). The URL carries a secret token: treat it as a single-use
credential and don't publish it.
The verification_link object
| Field | Type | Description |
|---|---|---|
id | string | Prefix vl_. |
url | string | Page the person opens. |
qr_svg | string | QR code of url as SVG. Only on creation and on the individual GET. |
status | string | pending (nobody has started), in_progress, completed, expired or cancelled. |
livemode | boolean | Mode of the key it was created with. |
template, reference, metadata | What you set when creating it. | |
documents[] | object[] | Status of each document: key, label, expect, checks, status (pending, processing, completed, failed), analysis_id (the analysis that counts), attempts, verdict_status, final_status (after human review) and warnings (quality warnings from the last attempt). |
dossier_id | string | null | Dossier with all the documents, the overall verdict and the cross-document checks (same holder, consistent dates). |
expires_at | ISO 8601 string | End of validity. |
redirect_url, redirect_with_status | Return to your website. | |
callback_url, include_results | Signed callback on completion or expiry. | |
results_email | string | null | Recipient of the summary. |
results_email_sent_at | string | null | When the summary was sent. |
locale | string | Language of the page and the emails. |
notify_email, last_sent_at | Who the link was emailed to, and when. | |
show_result, consent_text | Page settings. | |
consent | object | null | Accepted consent: accepted_at, ip and text. |
events[] | object[] | History: type, at and, depending on the type, document_key or email. |
created_at, completed_at, cancelled_at | string | null | Dates. |
events[].type values:
| Type | When |
|---|---|
created | The link is created. |
sent | The link is sent by email (with email). |
opened | The person opens the page for the first time. |
consent_accepted | They accept the consent. |
document_uploaded | They upload a document (with document_key). |
document_accepted | A document is accepted (with document_key). |
document_rejected | A document needs to be redone, for example because of a blurry photo (with document_key). |
completed | All documents are finished. |
expired | expires_at passes without it being completed. |
cancelled | You cancel it. |
results_sent | The summary is sent to results_email (with email). |
Each document allows up to 5 attempts. A document with quality warnings that ask for another photo (and a non-valid verdict) can be redone while attempts remain. The link is completed when every document is accepted or out of attempts.
Results: webhook, callback and email
When the link becomes completed or expired you receive the same body in two ways:
- Global webhook:
verification_link.completedandverification_link.expired, on the webhook endpoints subscribed to those events. - Link callback: a
POSTtocallback_url, signed with Standard Webhooks and with the same retries.
data is the verification_link object (without qr_svg) plus:
| Field | Description |
|---|---|
results[] | Only if include_results is true. One per requested document, in order: document_key, label and analysis (the analysis object that counts for that document, as it was stored: with mask_fields applied and without file_url; null if it wasn't uploaded). |
dossier | { id, verdict: { status, reasons: [{ code, severity, message }] } } of the dossier, in the link's language, or null. status: complete_valid, incomplete, invalid or review. |
With results_email, a summary without any document data is also sent on completion. Details on the signature,
the secret, the retries, the full body and the email are in the
results guide.
List links
curl "https://api.constaia.com/v1/verification-links?status=completed&metadata[registration_id]=4821" \
-H "Authorization: Bearer $CONSTAIA_API_KEY"| Parameter | Description |
|---|---|
limit | 1–100, default 20. |
starting_after | Id of the last link on the previous page. See pagination. |
status | pending, in_progress, completed, expired or cancelled. |
reference | Your exact reference. |
metadata[key] | Filters by a metadata value. |
Returns { "object": "list", "data": [...], "has_more": false, "url": "/v1/verification-links" }, only with the links
in the key's mode and without qr_svg.
Retrieve and cancel
curl https://api.constaia.com/v1/verification-links/vl_01J9Z8Q3K4M5N6P7Q8R9S0T1V2 \
-H "Authorization: Bearer $CONSTAIA_API_KEY"
curl -X DELETE https://api.constaia.com/v1/verification-links/vl_01J9Z8Q3K4M5N6P7Q8R9S0T1V2 \
-H "Authorization: Bearer $CONSTAIA_API_KEY"DELETE returns the link with status: "cancelled"; cancelling one that is already cancelled returns it unchanged. A
completed or expired link can't be cancelled: 409 link_not_active. An id that doesn't exist in the account returns
404 resource_missing.
Errors
| HTTP | code | When |
|---|---|---|
422 | invalid_parameter | A field is not valid (param says which), for example redirect_url without https://. |
422 | invalid_url | callback_url is not an https URL (param: "callback_url"). |
422 | documents_required | Neither documents nor template. |
422 | template_not_found | The template doesn't exist in the account. |
409 | link_not_active | Cancelling a completed or expired link. |
404 | resource_missing | The link doesn't exist. |
Next steps
Getting results back
Signed callback, results email and the return to your website, step by step.
Webhooks
Standard Webhooks signature, retries and good practices.
Verdicts
What valid, invalid or review means for each document.
Dossiers
The link's overall verdict and cross-document checks.
Templates
The link's analysis options, saved once.
Templates
Reference for /v1/templates: save a verification setup (expect, checks, storage, human review, retention) and reuse it with template in analyze, classify, batches, links, sessions and dossiers.
Dossiers
Reference for /v1/dossiers: group several documents of one person or procedure with requirements, add documents by file or analysis_id and get an overall verdict with cross-document holder and date checks.