Pular para o conteúdo principal

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:

  1. Permite que o cliente costure os próprios logs antes de qualquer coisa chegar ao backend.
  2. Torna a chamada posterior de /submit idempotente — o mesmo verificationId no 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.

CampoTipoDescrição
userRefstringO seu próprio identificador de usuário, 1-128 caracteres. Devolvido apenas em webhooks
countrystringISO 3166-1 alpha-2, maiúsculo (PY, BR, MX). Exatamente 2 caracteres
documentTypestringUm de dni, passport, drivers_license, national_id, other
submittedFullNamestringNome completo como o usuário digitou, 1-255 caracteres. Alimenta o score name_match
activeLivenessbooleanAtiva o desafio de prova de vida ativa. Padrão false
Campos desconhecidos somem sem erro

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

CampoTipoDescrição
verificationIdstring^vf_[A-Za-z0-9]{16,24}$. Passe para /submit e /verify/:id
uploads.docFrontobjectSlot de upload para a frente do documento
uploads.docBackobjectSlot de upload para o verso do documento
uploads.selfieobjectSlot de upload para a selfie
uploads.*.urlstringPara onde fazer o PUT dos bytes brutos da imagem
uploads.*.keystringIdentificador opaco — devolva no /submit
uploads.*.methodstringSempre "PUT"
uploads.*.headersobjectEnvie estes literalmente. Contém o token de upload
expiresAtnumberTimestamp Unix em segundos (inteiro), 15 minutos após o init
livenessobjectPresente 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-src da sua CSP precisa de https://api.xxuxe.online. Colocar *.r2.cloudflarestorage.com na 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

RegraValorFalha
FormatoJPEG de verdade — precisa começar com os bytes FF D8 FFreason: "not_a_jpeg"
Tamanho mínimo100 bytesreason: "image_too_small"
Tamanho máximo2 MBreason: "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

HTTPCódigodetail.reasonCausa
400invalid_bodymissing_upload_tokenVocê não encaminhou slot.headers
400invalid_bodyinvalid_upload_tokenO token não corresponde a esta verificação
400invalid_bodykey_mismatchO papel na URL não é um dos que o /init emitiu
400invalid_bodynot_a_jpegO corpo não é um JPEG
400invalid_bodyimage_too_largeMais de 2 MB. Reduza a resolução antes de enviar — 2 MB de JPEG são 2-4 MP, de sobra para o OCR
400invalid_bodyimage_too_smallMenos de 100 bytes (geralmente um envio truncado ou vazio)
400invalid_bodyupload_source_forbidden_for_biometricOrigem upload em uma selfie ou frame de prova de vida
404verification_not_foundA intenção expirou (1 hora) ou nunca existiu
429rate_limitedLimite por IP. Veja Limites de taxa

Erros

HTTPCódigo de erroQuando
400invalid_bodyO corpo falhou na validação — veja detail.fieldErrors
401missing_api_keySem cabeçalho Authorization
401invalid_api_keyChave revogada, malformada, ou que nunca existiu
402insufficient_creditsSaldo do tenant está zerado
403origin_not_allowedRequisição de navegador vinda de uma origem fora de uma lista permitida não vazia
429rate_limitedLimite por IP ou por tenant
500internal_errorAlgo 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, /submit e os uploads retornam verification_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.
  • submittedFullName nunca é 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:

POST /v1/verify/submit →