POST /v1/verify/init
Inicia uma nova verificação. Retorna um verificationId mais três slots de upload (frente do documento, verso do documento, selfie) que o cliente usa para enviar as imagens.
POST https://api.xxuxe.online/v1/verify/init
Por que pré-criar o ID de verificação
Chamar /init primeiro, em vez de simplesmente enviar as imagens e submeter, faz duas coisas:
- Permite que o cliente costure os próprios logs antes de qualquer coisa chegar ao backend.
- Torna a chamada posterior de
/submitidempotente — o mesmoverificationIdno corpo do submit sempre significa a mesma linha no banco de dados.
Autenticação
Bearer token. Qualquer uma das famílias funciona: publicável (qv_pub_ / qv_pubt_) ou secreta (qv_sec_ / qv_sect_).
Authorization: Bearer qv_pub_FJJWXMA2RN2XPRDK6YJX4KTVD0XSQHW9
O tenant é derivado da chave. Não existe campo tenantId.
Corpo da requisição
Todos os campos são opcionais. Você pode fazer POST de um corpo vazio {} e obter uma verificação funcional. country e documentType melhoram significativamente a precisão do OCR, então envie-os quando os tiver.
| Campo | Tipo | Descrição |
|---|---|---|
userRef | string | O seu próprio identificador de usuário, 1-128 caracteres. Devolvido apenas em webhooks |
country | string | ISO 3166-1 alpha-2, maiúsculo (PY, BR, MX). Exatamente 2 caracteres |
documentType | string | Um de dni, passport, drivers_license, national_id, other |
submittedFullName | string | Nome completo como o usuário digitou, 1-255 caracteres. Alimenta o score name_match |
activeLiveness | boolean | Ativa o desafio de prova de vida ativa. Padrão false |
O schema é não estrito: chaves que ele não reconhece são descartadas antes da validação. Enviar tenantId, callbackUrl ou metadata aqui retorna 200 OK e o valor simplesmente se perde.
Nenhum desses três existe no /init. O tenant vem da sua chave de API; a entrega de webhooks é configurada uma vez por tenant no painel, não por requisição.
userRef — onde ele volta
userRef é devolvido nos payloads de webhook. Ele não é retornado por GET /v1/verify/:id, e não está no evento veridia:complete do widget.
Então, se você pretende reconciliar resultados por polling em vez de por webhook, guarde do seu lado o mapeamento verificationId → seu-usuário quando chamar /init. Esse é o único vínculo que você terá.
activeLiveness
Definir activeLiveness: true adiciona um desafio emitido pelo servidor: a resposta ganha um bloco liveness, e o cliente precisa capturar e enviar uma sequência de frames guiada por beacons que ele busca um a um. É o sinal anti-injeção mais forte disponível, e vem desligado por padrão.
Também custa cerca de 6x o volume de requisições — aproximadamente 26 requisições por verificação em vez de 4. Leia Limites de taxa antes de habilitá-lo para tráfego mobile em escala.
O protocolo de desafio é implementado pelo widget e pelos SDKs. Se você está construindo um cliente personalizado e precisa dele, fale conosco antes de começar.
Exemplo de requisição
curl
curl -X POST https://api.xxuxe.online/v1/verify/init \
-H "Authorization: Bearer qv_pub_FJJWXMA2RN2XPRDK6YJX4KTVD0XSQHW9" \
-H "Content-Type: application/json" \
-d '{
"userRef": "customer-12345",
"country": "PY",
"documentType": "dni",
"submittedFullName": "Juan Carlos Perez"
}'
JavaScript / Node.js
const response = await fetch('https://api.xxuxe.online/v1/verify/init', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.VERIDIA_PUBLISHABLE_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
userRef: 'customer-12345',
country: 'PY',
documentType: 'dni',
submittedFullName: 'Juan Carlos Perez',
}),
});
const init = await response.json();
console.log(init.verificationId);
Python
import os
import requests
response = requests.post(
"https://api.xxuxe.online/v1/verify/init",
headers={
"Authorization": f"Bearer {os.environ['VERIDIA_PUBLISHABLE_KEY']}",
"Content-Type": "application/json",
},
json={
"userRef": "customer-12345",
"country": "PY",
"documentType": "dni",
"submittedFullName": "Juan Carlos Perez",
},
)
response.raise_for_status()
init = response.json()
print(init["verificationId"])
Resposta
200 OK
{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"uploads": {
"docFront": {
"url": "https://api.xxuxe.online/v1/verify/upload/vf_AG07CDWRRFQV4T05ZXG2/doc-front",
"key": "verif/tn_xyz/vf_AG07CDWRRFQV4T05ZXG2/doc-front.jpg",
"method": "PUT",
"headers": {
"X-Veridia-Upload-Token": "5f3c1a9e0b7d4426a1c8ef53d20b9a7c6e14f8b2d9a03c57",
"Content-Type": "image/jpeg"
}
},
"docBack": {
"url": "https://api.xxuxe.online/v1/verify/upload/vf_AG07CDWRRFQV4T05ZXG2/doc-back",
"key": "verif/tn_xyz/vf_AG07CDWRRFQV4T05ZXG2/doc-back.jpg",
"method": "PUT",
"headers": {
"X-Veridia-Upload-Token": "5f3c1a9e0b7d4426a1c8ef53d20b9a7c6e14f8b2d9a03c57",
"Content-Type": "image/jpeg"
}
},
"selfie": {
"url": "https://api.xxuxe.online/v1/verify/upload/vf_AG07CDWRRFQV4T05ZXG2/selfie",
"key": "verif/tn_xyz/vf_AG07CDWRRFQV4T05ZXG2/selfie.jpg",
"method": "PUT",
"headers": {
"X-Veridia-Upload-Token": "5f3c1a9e0b7d4426a1c8ef53d20b9a7c6e14f8b2d9a03c57",
"Content-Type": "image/jpeg"
}
}
},
"expiresAt": 1714604000
}
Os três slots são sempre retornados. Use docBack apenas se o seu tipo de documento tiver verso.
Campos da resposta
| Campo | Tipo | Descrição |
|---|---|---|
verificationId | string | ^vf_[A-Za-z0-9]{16,24}$. Passe para /submit e /verify/:id |
uploads.docFront | object | Slot de upload para a frente do documento |
uploads.docBack | object | Slot de upload para o verso do documento |
uploads.selfie | object | Slot de upload para a selfie |
uploads.*.url | string | Para onde fazer o PUT dos bytes brutos da imagem |
uploads.*.key | string | Identificador opaco — devolva no /submit |
uploads.*.method | string | Sempre "PUT" |
uploads.*.headers | object | Envie estes literalmente. Contém o token de upload |
expiresAt | number | Timestamp Unix em segundos (inteiro), 15 minutos após o init |
liveness | object | Presente apenas quando activeLiveness: true foi enviado |
expiresAt é uma contagem inteira de segundos, não uma string ISO 8601. new Date(expiresAt * 1000) em JavaScript; datetime.fromtimestamp(expires_at) em Python.
Enviando as imagens
Esta é a parte que a maioria dos clientes personalizados erra, então ela ganha uma seção própria.
PUT https://api.xxuxe.online/v1/verify/upload/{verificationId}/{role}
Os uploads não vão para o Cloudflare R2, e as URLs dos slots não são URLs S3 pré-assinadas. Elas apontam para o próprio Worker da Veridia. O Worker valida o token, checa que os bytes realmente são um JPEG dentro dos limites de tamanho, e escreve no armazenamento em seu nome, carimbando metadados controlados pelo servidor que o cliente não pode forjar.
Isso é uma escolha deliberada de design, não um detalhe de implementação: é o vínculo câmera-para-bytes. Nenhum byte do cliente chega ao armazenamento sem passar por esse portão, o que é o pré-requisito de toda camada anti-injeção construída em cima. Isso também significa que:
- O
connect-srcda sua CSP precisa dehttps://api.xxuxe.online. Colocar*.r2.cloudflarestorage.comna allowlist não faz nada. - Regras de firewall, certificate pinning e allowlists de saída devem apontar para o host da API.
Autenticação para uploads
O endpoint de upload não aceita a sua chave de API. Enviar Authorization: Bearer ... aqui não tem efeito — o middleware de autenticação não roda nessa rota.
Ele se autentica com X-Veridia-Upload-Token, um token aleatório de 192 bits que o /init gerou para esta única verificação e colocou dentro do objeto headers de cada slot. Ele está vinculado à verificação e expira junto com a intenção.
A regra prática: encaminhe slot.headers literalmente. Não monte o objeto de cabeçalhos à mão a partir do Content-Type que você vê no exemplo — você vai perder o token e todo upload vai falhar com 400 invalid_body, reason: "missing_upload_token", e você nunca vai chegar ao /submit.
Requisitos do corpo
| Regra | Valor | Falha |
|---|---|---|
| Formato | JPEG de verdade — precisa começar com os bytes FF D8 FF | reason: "not_a_jpeg" |
| Tamanho mínimo | 100 bytes | reason: "image_too_small" |
| Tamanho máximo | 2 MB | reason: "image_too_large" |
PNG, HEIC, WebP e PDF são todos rejeitados. Converta para JPEG no cliente antes de enviar.
Exemplo funcional
async function uploadImage(slot, blob) {
const response = await fetch(slot.url, {
method: slot.method, // "PUT"
headers: slot.headers, // literalmente — carrega X-Veridia-Upload-Token
body: blob, // bytes JPEG brutos
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(`Upload failed: ${response.status} ${err.detail?.reason ?? ''}`);
}
return response.json(); // { ok: true, key: "verif/..." }
}
await uploadImage(init.uploads.docFront, docFrontBlob);
await uploadImage(init.uploads.selfie, selfieBlob);
// docBack apenas se o documento tiver verso
Repare no que faz isso funcionar: headers: slot.headers. Todo o resto é acessório.
Declarando a origem da captura
Opcionalmente envie X-Veridia-Capture-Source: camera ou upload para registrar de onde vieram os pixels.
Uma regra é aplicada, e não apenas registrada em log: upload é rejeitado para o papel selfie e para qualquer frame de prova de vida, com reason: "upload_source_forbidden_for_biometric". Uma foto de rosto da galeria não é uma selfie, e nenhum cliente honesto envia essa combinação. Documentos podem legitimamente vir da galeria.
O cabeçalho é autodeclarado e, portanto, forjável — é telemetria e triagem, não um controle de segurança. Não construa uma defesa em cima dele, e também não o omita: tráfego honesto que se rotula mantém o nosso corpus forense limpo.
Erros de upload
| HTTP | Código | detail.reason | Causa |
|---|---|---|---|
400 | invalid_body | missing_upload_token | Você não encaminhou slot.headers |
400 | invalid_body | invalid_upload_token | O token não corresponde a esta verificação |
400 | invalid_body | key_mismatch | O papel na URL não é um dos que o /init emitiu |
400 | invalid_body | not_a_jpeg | O corpo não é um JPEG |
400 | invalid_body | image_too_large | Mais de 2 MB. Reduza a resolução antes de enviar — 2 MB de JPEG são 2-4 MP, de sobra para o OCR |
400 | invalid_body | image_too_small | Menos de 100 bytes (geralmente um envio truncado ou vazio) |
400 | invalid_body | upload_source_forbidden_for_biometric | Origem upload em uma selfie ou frame de prova de vida |
404 | verification_not_found | — | A intenção expirou (1 hora) ou nunca existiu |
429 | rate_limited | — | Limite por IP. Veja Limites de taxa |
Erros
| HTTP | Código de erro | Quando |
|---|---|---|
400 | invalid_body | O corpo falhou na validação — veja detail.fieldErrors |
401 | missing_api_key | Sem cabeçalho Authorization |
401 | invalid_api_key | Chave revogada, malformada, ou que nunca existiu |
402 | insufficient_credits | Saldo do tenant está zerado |
403 | origin_not_allowed | Requisição de navegador vinda de uma origem fora de uma lista permitida não vazia |
429 | rate_limited | Limite por IP ou por tenant |
500 | internal_error | Algo quebrou do nosso lado — reporte o requestId |
Os códigos são missing_api_key, invalid_api_key e internal_error. Não unauthorized, invalid_key nem internal. Catálogo completo: Erros.
Notas
- Os slots de upload são válidos por 15 minutos (
expiresAt). A intenção de verificação em si vive por 1 hora — depois disso,/submite os uploads retornamverification_not_found. - O layout de armazenamento é
verif/<tenantId>/<verificationId>/<role>.jpg. A chave é reconstruída no servidor a partir da intenção validada, nunca a partir de entrada do cliente. submittedFullNamenunca é colocado em um caminho de armazenamento. É PII e permanece no registro da intenção.
O que vem a seguir
Depois do /init, envie as imagens e então: