GET /v1/verify/:id
Consulta o estado atual de uma verificação, e o veredicto assim que o pipeline terminar.
GET https://api.xxuxe.online/v1/verify/vf_AG07CDWRRFQV4T05ZXG2
Autenticação
Este endpoint exige uma chave secreta. Uma chave publicável é rejeitada com 401 secret_key_required, mesmo que a mesma chave tenha tido permissão para criar e submeter a verificação.
Authorization: Bearer qv_sec_YOUR_SECRET_KEY
Em modo de teste o prefixo é qv_sect_.
O motivo não é arbitrário: uma chave publicável vai embarcada no código-fonte da sua página, onde qualquer pessoa pode lê-la. Se ela pudesse ler veredictos, o resultado de KYC de cada cliente — incluindo número do documento e data de nascimento — estaria a um fetch de distância de qualquer pessoa com as ferramentas de desenvolvedor abertas. Então os resultados são somente server-side, por construção.
Se você precisa que o navegador saiba que o fluxo terminou, o evento veridia:complete do widget informa que as imagens foram submetidas. Ele deliberadamente não carrega o veredicto. Decida no servidor.
Parâmetro de caminho
| Parâmetro | Tipo | Descrição |
|---|---|---|
:id | string | O verificationId do /init, por exemplo vf_AG07CDWRRFQV4T05ZXG2 |
status e verdict são eixos diferentes
Leia isto antes de escrever qualquer lógica de ramificação. É o erro mais caro que esta API permite.
| Campo | Pergunta que responde | Valores |
|---|---|---|
status | O pipeline rodou? | queued, processing, completed, failed |
verdict | A pessoa passou? | approved, review, rejected |
status: "completed" significa que a maquinaria terminou o trabalho dela. Todo solicitante rejeitado também chega a completed — é assim que uma rejeição bem-sucedida se parece.
// ERRADO — isso faz onboarding de todo solicitante que o sistema rejeitou.
// Não lança erro nenhum, não registra nada de anormal, e parece correto em
// testes enquanto todos os seus usuários de teste passarem.
if (data.status === 'completed') {
await enableAccount(userId);
}
// CERTO — os dois eixos verificados separadamente
if (data.status === 'completed') {
if (data.verdict === 'approved') await enableAccount(userId);
else if (data.verdict === 'review') await queueForManualReview(userId);
else if (data.verdict === 'rejected') await blockOnboarding(userId);
}
Uma segunda armadilha da mesma família: review é um veredicto final, não transitório. O pipeline terminou; agora uma pessoa precisa agir. Fazer polling esperando que review se resolva sozinho espera para sempre. A resolução chega como um novo evento de webhook quando um revisor decide.
Exemplo de requisição
curl
curl -X GET https://api.xxuxe.online/v1/verify/vf_AG07CDWRRFQV4T05ZXG2 \
-H "Authorization: Bearer qv_sec_YOUR_SECRET_KEY"
JavaScript / Node.js
const response = await fetch(
`https://api.xxuxe.online/v1/verify/${verificationId}`,
{ headers: { 'Authorization': `Bearer ${process.env.VERIDIA_SECRET_KEY}` } }
);
const data = await response.json();
console.log(data.status, data.verdict);
Python
import os
import requests
response = requests.get(
f"https://api.xxuxe.online/v1/verify/{verification_id}",
headers={"Authorization": f"Bearer {os.environ['VERIDIA_SECRET_KEY']}"},
)
response.raise_for_status()
data = response.json()
print(data["status"], data.get("verdict"))
Resposta
200 OK
Enquanto processa:
{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"status": "processing",
"verdict": null,
"confidence": null,
"scores": null,
"flags": null,
"submittedAt": "2026-05-01T18:39:05Z",
"completedAt": null
}
null, não ausentesEnquanto uma verificação está em andamento, verdict, confidence, scores, flags e completedAt são retornados como null de JSON — as chaves existem.
Então 'verdict' in data é true desde o primeiríssimo polling, e data.verdict !== undefined também é true. Nenhum dos dois é um teste válido de "já terminou?". Verifique status, ou verifique se o valor é null.
Em clientes tipados isso importa ainda mais: um campo modelado como string não opcional vai falhar na desserialização já no primeiro polling.
Quando completa:
{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"status": "completed",
"verdict": "approved",
"confidence": 93.4,
"scores": {
"ocr_confidence": 78.0,
"face_match": 96.2,
"liveness": 91.5,
"doc_quality": 85.0,
"mrz_valid": 100.0,
"name_match": 97.0
},
"flags": [
{ "level": "ok", "text": "auto_approved_all_checks_passed" },
{ "level": "ok", "text": "mrz_checksums_valid" }
],
"submittedAt": "2026-05-01T18:39:05Z",
"completedAt": "2026-05-01T18:39:08Z"
}
Precisando de revisão manual:
{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"status": "completed",
"verdict": "review",
"confidence": 64.5,
"scores": {
"ocr_confidence": 88.0,
"face_match": 71.2,
"liveness": null,
"doc_quality": 55.0,
"mrz_valid": 0.0,
"name_match": 62.0
},
"flags": [
{ "level": "warn", "text": "mrz_checksum_failed" },
{ "level": "warn", "text": "heavy_glare" }
],
"submittedAt": "2026-05-01T18:39:05Z",
"completedAt": "2026-05-01T18:39:08Z"
}
Campos da resposta
| Campo | Tipo | Presente | Descrição |
|---|---|---|---|
verificationId | string | Sempre | O ID da verificação |
status | string | Sempre | queued, processing, completed, failed |
verdict | string | null | null até completed | approved, review, rejected |
confidence | number | null | null até completed | Score ponderado geral, 0-100 |
scores | object | null | null até completed | Detalhamento por sinal — veja abaixo |
flags | array | null | null até completed | Objetos { level, text } |
submittedAt | string | Sempre | ISO 8601 UTC, quando o /submit foi chamado |
completedAt | string | null | null até o estado terminal | ISO 8601 UTC |
scores — as chaves são snake_case
Seis chaves, todas em snake_case. Este é o campo que integradores mais erram, porque o erro é silencioso.
| Chave | Faixa | Significado |
|---|---|---|
ocr_confidence | 0-100 | A confiança autodeclarada do modelo de extração no texto do documento |
face_match | 0-100 | Similaridade biométrica entre a selfie e a foto do documento |
liveness | 0-100 ou null | Sinal anti-spoofing |
doc_quality | 0-100 | Qualidade da imagem do documento (nitidez, reflexo, resolução, moiré) |
mrz_valid | 0-100 | Validade do checksum da zona de leitura mecânica (MRZ) |
name_match | 0-100 | Correspondência aproximada entre submittedFullName e o nome lido por OCR |
Duas coisas com que tomar cuidado:
Não existe faceMatch. Em JavaScript, scores.faceMatch é undefined, e undefined < 70 avalia para false. Então um limiar escrito em camelCase não lança erro — ele silenciosamente nunca dispara, e todo solicitante passa pela sua checagem. Se você está portando um limiar de uma versão antiga desta documentação, esta é a linha a corrigir.
liveness pode ser null. Ele é null quando nenhum sinal de prova de vida foi produzido ou quando o modelo de prova de vida deu erro. É o único membro anulável de scores. Proteja-o antes de fazer aritmética:
const liveness = data.scores.liveness;
if (liveness !== null && liveness < 50) {
// ...
}
mrz_valid e name_match são os dois sinais de fraude documental mais frequentemente ignorados. name_match, em particular, é o score produzido a partir do submittedFullName que você enviou ao /init — se você envia esse campo, leia este score.
flags
flags é um array de objetos, não de strings:
{ "level": "warn", "text": "heavy_glare" }
Níveis
São exatamente três, e um deles significa bom:
| Nível | Significado |
|---|---|
ok | Uma checagem passou. Este é um sinal positivo, não um problema |
warn | Algo está fora do esperado, mas não é desqualificante |
err | Um sinal sério — falha grave, correspondência em lista de sanções, ou indicador de falsificação |
Não existe info nem critical. Duas consequências que vale dizer:
flags.some(f => f.level === 'critical')é semprefalse. Sinais sérios sãoerr. Uma regra de prioridade para revisores escrita contracriticalnunca dispara, e os casos que mais precisam de um humano recebem prioridade normal.- Um array
flagsnão vazio não significa que algo está errado. Toda verificação aprovada carrega ao menos{ "level": "ok", "text": "auto_approved_all_checks_passed" }. Tratarflags.length > 0como "problema" manda 100% das suas aprovações limpas para revisão manual.
Filtre pelo nível:
const problems = data.flags.filter(f => f.level !== 'ok');
const serious = data.flags.filter(f => f.level === 'err');
Flags que você realmente vai ver
text | Nível típico | Significado |
|---|---|---|
auto_approved_all_checks_passed | ok | Aprovado sem falhas graves. Sempre o primeiro quando presente |
image_blurry | warn | Nitidez abaixo do limiar |
heavy_glare | warn | Reflexo forte obscurecendo o documento |
possible_screen_capture | warn | Padrão de moiré — o "documento" pode ser uma foto de uma tela |
low_resolution | warn | Resolução da imagem baixa demais |
mrz_checksums_valid | ok | Checksums da MRZ verificados |
mrz_checksum_failed | warn / err | MRZ presente mas os checksums não conferem |
mrz_viz_consistent | ok | A MRZ concorda com os campos impressos |
mrz_viz_mismatch | err | MRZ válida que contradiz os campos impressos — forte sinal de falsificação |
no_face_detected_on_document | err | Nenhum rosto encontrado na foto do documento |
no_face_detected_on_selfie | err | Nenhum rosto encontrado na selfie |
face_match_below_critical_threshold | err | É muito improvável que a selfie e a foto do documento sejam a mesma pessoa |
document_quality_unusable | err | Imagem do documento degradada demais para ser avaliada |
active_liveness_spoof | err | O desafio de prova de vida ativa concluiu que a captura não era ao vivo |
active_liveness_live | ok | O desafio passou |
aml_sanctions_match | err | Correspondência forte com uma lista oficial de sanções |
aml_possible_match | warn | Correspondência de sanções mais fraca, vale olhar |
missing_images | err | As imagens esperadas não estavam presentes no momento do pipeline |
As três que carregam peso regulatório ou de fraude e são as mais fáceis de deixar passar: possible_screen_capture (injeção de imagem), mrz_viz_mismatch (documento fabricado) e aml_sanctions_match (a pessoa está em uma lista de sanções). Encaminhe essas para algum lugar onde uma pessoa as veja.
Não exiba o texto das flags para o seu usuário final. Dizer a alguém em qual checagem ele falhou diz a um atacante exatamente o que corrigir.
Como o veredicto é decidido
| Veredicto | Condição |
|---|---|
approved | confidence >= 90, e nenhuma falha grave, e nenhuma retenção de compliance |
review | Qualquer coisa entre os dois — incluindo todo caso com falha grave que não seja catastrófica |
rejected | confidence < 60 |
Três regras que os números sozinhos não contam:
- Uma falha grave nunca aprova automaticamente, qualquer que seja o score. Qualquer falha grave de nível
errlimita o resultado areview, ou arejectedse a confiança também estiver abaixo de 60. - Uma correspondência forte de sanções força
review. Ela rebaixa um caso que de outra forma seria aprovado; ela nunca rejeita automaticamente. Um acerto de sanções é uma decisão de compliance para uma pessoa, não para uma máquina. - A faixa 60-89 é
review, nãoapproved. Se você está acostumado com um limiar de 80, é essa a lacuna que vai encher a sua fila manual inesperadamente.
Os limiares são configurações no nível do deployment, não configuração por tenant — não existe hoje um botão por cliente para eles.
Se você reimplementar o limiar do seu lado lendo confidence, você vai perder as regras 1 e 2 e vai aprovar automaticamente casos que o sistema deliberadamente segurou, incluindo correspondências de sanções. Leia verdict.
Máquina de estados do status
queued -> processing -> completed
-> failed
| Status | Significado |
|---|---|
queued | Aguardando na fila do backend |
processing | Pipeline em execução |
completed | Pipeline terminou. Agora leia verdict |
failed | O pipeline não conseguiu concluir. Não há veredicto — não leia um |
failed não é uma rejeição. Significa que o sistema não conseguiu chegar a uma conclusão. Trate como um caso de tentar de novo ou escalar, não como uma decisão sobre o solicitante.
Polling
Webhooks são melhores: eles disparam assim que o veredicto está pronto, e também entregam o evento posterior em que uma pessoa resolve um review. O polling não consegue ver esse segundo resultado a menos que você faça polling indefinidamente. Veja Webhooks.
Se você precisa mesmo fazer polling:
async function waitForVerdict(verificationId, timeoutMs = 30000) {
const start = Date.now();
let interval = 1000; // 1s — veja a nota sobre limite de taxa abaixo
while (Date.now() - start < timeoutMs) {
const response = await fetch(
`https://api.xxuxe.online/v1/verify/${verificationId}`,
{ headers: { 'Authorization': `Bearer ${process.env.VERIDIA_SECRET_KEY}` } }
);
const data = await response.json();
if (data.status === 'completed' || data.status === 'failed') {
return data; // quem chamou ainda precisa ramificar por data.verdict
}
await new Promise(r => setTimeout(r, interval));
interval = Math.min(interval * 1.5, 3000);
}
throw new Error('Verification timed out');
}
Repare que o loop retorna em qualquer um dos status terminais. Quem chama precisa tratar failed, onde verdict é null.
O limite por tenant aqui é 600/minuto, mas o limite por IP é 100/minuto e ele é verificado primeiro. Fazer polling de um servidor a cada 500 ms são 120 requisições por minuto e será limitado muito antes do limite do tenant. Veja Limites de taxa.
Erros
| HTTP | Código de erro | Quando |
|---|---|---|
401 | missing_api_key | Sem cabeçalho Authorization |
401 | invalid_api_key | Chave revogada, malformada, ou que nunca existiu |
401 | secret_key_required | Uma chave publicável foi usada. O erro mais comum neste endpoint |
404 | verification_not_found | O ID não existe, está malformado, ou pertence a outro tenant |
429 | rate_limited | Limite por IP ou por tenant |
500 | internal_error | Reporte o requestId |
503 | backend_unavailable | Backend inacessível — transitório, tente de novo com backoff |
Se você chega aqui vindo de /init e /submit reutilizando a mesma chave e recebe um 401, confira o código antes de mexer na chave. secret_key_required significa que a chave é perfeitamente válida — ela é apenas da família errada para esta chamada. Rotacioná-la não vai ajudar.
Uma verificação que pertence a outro tenant retorna 404, não 403: quem não é dono de uma verificação não deve descobrir que ela existe.
Catálogo completo: Erros.
Notas
- Verificações não se tornam ilegíveis com o tempo. A posse é verificada contra o tenant registrado com a própria verificação, então este endpoint continua funcionando muito depois de a intenção de upload de uma hora ter expirado. Essa expiração se aplica aos uploads e ao
/submit, não à leitura dos resultados. - Uma vez
completedoufailed, a resposta de uma dada execução automatizada é estável — mas um caso emreviewpode mudar depois, quando uma pessoa o resolver. Se você faz cache, invalide no webhook. submittedAtecompletedAtsão ISO 8601 em UTC. (expiresAtno/inité diferente — aquele é em segundos Unix.)userRefnão é retornado aqui. Ele viaja apenas em webhooks. Mantenha o seu próprio mapeamentoverificationId→ usuário.
O que vem a seguir
- Webhooks — receba veredictos por push, incluindo resultados de revisão humana
- Erros — referência completa de códigos de erro
- Limites de taxa — por que o polling bate em um limite antes do que você esperaria