Pular para o conteúdo principal

Erros

Todos os erros da API da Veridia compartilham o mesmo formato. Esta página é a lista canônica dos códigos error.

Formato da resposta de erro

{
"error": "invalid_body",
"message": "Request body failed validation",
"requestId": "9f511d92ac11236d-SJC",
"detail": {
"fieldErrors": {
"country": ["expected 2 characters"]
}
}
}
CampoTipoDescrição
errorstringCódigo legível por máquina — faça o switch neste, não no status HTTP
messagestringResumo legível por humanos, para logs
requestIdstringÚnico por requisição — inclua nos chamados de suporte
detailobjectOpcional. Erros de campo, um reason, ou retry_after

O mesmo requestId está no cabeçalho de resposta X-Request-Id, então você consegue correlacionar até quando o parse do JSON falha.

Os códigos em resumo

CódigoHTTPVale tentar de novo?
invalid_body400Não — corrija a requisição
missing_api_key401Não
invalid_api_key401Não
secret_key_required401Não — use uma chave secreta
insufficient_credits402Não — recarregue
origin_not_allowed403Não
verification_not_found404Não
not_found404Não — URL errada
rate_limited429Sim, depois do Retry-After
internal_error500Sim, com backoff
backend_unavailable503Sim, com backoff

Repare na grafia exata. Não existe unauthorized, nem invalid_key, nem internal — este último é internal_error.

Catálogo de erros

invalid_body400 Bad Request

O corpo da requisição falhou na validação de schema. Confira detail.fieldErrors.

Causas comuns:

  • Tipo errado, ou uma string acima do limite de comprimento (userRef > 128, submittedFullName > 255)
  • Valor de enum inválido (documentType: "id" em vez de "dni")
  • country que não são exatamente duas letras maiúsculas
  • verificationId que não casa com ^vf_[A-Za-z0-9]{16,24}$

Campos desconhecidos não causam este erro. Eles são descartados silenciosamente e a requisição tem sucesso. Se um campo que você enviou não teve efeito, é por isso — confira a tabela de campos do endpoint em vez de esperar por um erro que não vai vir.

No /submit, detail.reason pode ser:

ReasonSignificado
doc_front_key_mismatchkeys.docFront não é o que o /init retornou
selfie_key_mismatchkeys.selfie não é o que o /init retornou
doc_back_key_mismatchkeys.docBack não é o que o /init retornou
doc_front_not_uploadedNenhum objeto armazenado naquela chave — o cliente nunca enviou
selfie_not_uploadedO mesmo, para a selfie
doc_front_disappearedO objeto existia no momento da checagem mas sumiu antes do OCR (muito raro)

Em PUT /v1/verify/upload/..., detail.reason pode ser:

ReasonSignificado
missing_upload_tokenSem cabeçalho X-Veridia-Upload-Token — você não encaminhou slot.headers
invalid_upload_tokenO token não corresponde à intenção desta verificação
key_mismatchO papel na URL não é um dos que o /init emitiu
invalid_roleSegmento de papel não reconhecido na URL
not_a_jpegO corpo não começa com os bytes mágicos de JPEG FF D8 FF
image_too_largeCorpo com mais de 2 MB. Reduza a resolução antes de enviar; 2 MB de JPEG são 2-4 MP, mais do que o OCR precisa
image_too_smallCorpo com menos de 100 bytes — geralmente um envio truncado ou vazio
upload_source_forbidden_for_biometricX-Veridia-Capture-Source: upload em uma selfie ou frame de prova de vida
no_active_challengeUm frame de prova de vida foi enviado para uma verificação que nunca optou por ela

Recuperação: corrija a requisição. Estes são erros de cliente; tentar de novo sem mudar nada os reproduz exatamente.

missing_api_key401 Unauthorized

Sem cabeçalho Authorization, ou ele não é um token Bearer.

Recuperação: envie Authorization: Bearer <chave>.

Note que os endpoints de upload e de desafio não querem esse cabeçalho — eles usam X-Veridia-Upload-Token no lugar.

invalid_api_key401 Unauthorized

O token não corresponde a nenhuma chave conhecida. Ela nunca existiu, foi revogada, ou não é parseável.

O parser aceita exatamente quatro prefixos: qv_pub_, qv_pubt_, qv_sec_, qv_sect_, seguidos de 16-64 caracteres de [A-Za-z0-9_]. Uma chave da forma qv_pub_test_... não existe — o prefixo de teste é qv_pubt_.

Recuperação: confira primeiro o prefixo e o ambiente, depois o painel. Lembre que a revogação é imediata, sem período de carência.

secret_key_required401 Unauthorized

Uma chave publicável foi usada em GET /v1/verify/:id, o único endpoint que exige uma chave secreta.

A chave está boa. Ela é da família errada para esta chamada. Chaves publicáveis vão embarcadas no código-fonte da sua página, então elas nunca têm permissão para ler resultados de verificação. Este código existe justamente para que você não saia caçando um erro de digitação em uma chave válida.

Recuperação: crie uma chave secreta e chame este endpoint a partir do seu servidor. Nunca a coloque em código de front-end.

insufficient_credits402 Payment Required

O saldo de créditos do tenant está zerado. Ele é levantado durante a autenticação, então pode aparecer no /init antes de qualquer outra coisa acontecer — uma causa frequente de "o widget simplesmente falha" em uma conta de teste esgotada.

Recuperação: recarregue no painel. Vale monitorar como alerta de negócio: significa que verificações estão sendo recusadas.

origin_not_allowed403 Forbidden

Uma requisição de navegador carregou um Origin que não está na lista de origens permitidas da chave publicável, e essa lista não está vazia.

Recuperação: adicione o hostname puroyourapp.com, staging.yourapp.com, localhost. Não https://yourapp.com, e não http://localhost:3000; a checagem compara apenas hostnames, então um esquema ou porta nunca casa.

Duas coisas que vale saber antes de mexer nessa lista:

  • Uma lista vazia permite todas as origens. Adicionar a sua primeira entrada muda a chave de "qualquer origem" para "somente estas". Adicione uma entrada que não casa e você vai de tudo permitido para tudo bloqueado em um único passo.
  • A checagem nunca se aplica a clientes fora do navegador, que não enviam cabeçalho Origin nenhum.

Veja Autenticação.

verification_not_found404 Not Found

O verificationId não existe, está malformado, ou pertence a outro tenant. A verificação de outro tenant retorna 404 em vez de 403 para que a resposta não confirme que o ID existe.

No /submit e nos uploads, também significa que a intenção de verificação expirou. A intenção vive por uma hora após o /init; depois disso, reinicie o fluxo.

Em GET /v1/verify/:id não existe essa expiração. Os resultados permanecem legíveis indefinidamente — a posse é verificada contra o tenant registrado com a própria verificação, não contra a intenção de vida curta. Você não precisa espelhar os resultados localmente para mantê-los acessíveis.

not_found404 Not Found

A rota não existe.

Recuperação: confira a URL. O clássico é /v1/verifications ou /v1/verifications/:id — nenhum dos dois existe. O endpoint de resultados é GET /v1/verify/:id.

rate_limited429 Too Many Requests

Você bateu em um de dois limites. detail.retry_after e o cabeçalho Retry-After dão a espera em segundos.

CamadaLimite
Por IP do cliente, todas as rotas, verificado antes da autenticação100 / 60 s
Por tenant — /v1/verify/init60 / 60 s
Por tenant — /v1/verify/submit30 / 60 s
Por tenant — /v1/verify/:id600 / 60 s

A camada por IP é a que surpreende as pessoas: ela roda antes de a sua chave ser lida, cobre os endpoints de upload e de desafio (que não têm limite por tenant nenhum), e é nela que usuários mobile atrás de endereços CGNAT compartilhados esbarram enquanto os contadores do seu tenant parecem ociosos.

Recuperação: espere o Retry-After e então tente de novo. Se voltar a acontecer, leia Limites de taxa — em particular a tabela de contagem de requisições por verificação.

internal_error500 Internal Server Error

Algo quebrou do nosso lado.

Recuperação: tente de novo com backoff exponencial. Se persistir, abra um chamado com o requestId — para este erro em especial, é a única forma de rastrear o que aconteceu.

backend_unavailable503 Service Unavailable

O pipeline de verificação está inacessível, ou o circuit breaker abriu depois de falhas repetidas. detail.retry_after pode estar presente.

Recuperação: tente de novo com backoff. Este é o erro transitório mais provável em operação normal — certifique-se de que a sua política de novas tentativas realmente o inclui.

Tratando erros bem

JavaScript / Node.js

const RETRYABLE = new Set(['rate_limited', 'internal_error', 'backend_unavailable']);

async function callVeridia(url, options, attempt = 0) {
const response = await fetch(url, options);
if (response.ok) return response.json();

const error = await response.json().catch(() => ({ error: 'unparseable' }));

// Faça o switch no código de erro, não no status HTTP.
switch (error.error) {
case 'invalid_body':
logger.error('Veridia validation failed', {
fieldErrors: error.detail?.fieldErrors,
reason: error.detail?.reason,
requestId: error.requestId,
});
throw new ValidationError(error);

case 'secret_key_required':
// A chave é válida — é a família errada. Não a rotacione.
throw new ConfigError('Use a secret key for GET /v1/verify/:id');

case 'insufficient_credits':
await notifyCreditsExhausted();
throw new BusinessError(error);

case 'verification_not_found':
throw new NotFoundError(error);

case 'rate_limited':
case 'internal_error':
case 'backend_unavailable': {
if (attempt >= 3) throw new ExternalServiceError(error);
const retryAfter =
Number(response.headers.get('Retry-After')) ||
error.detail?.retry_after ||
2 ** attempt;
logger.warn('Veridia transient error, retrying', {
code: error.error,
requestId: error.requestId,
retryAfter,
});
await new Promise(r => setTimeout(r, retryAfter * 1000));
return callVeridia(url, options, attempt + 1);
}

default:
// Novos códigos podem aparecer em /v1 sem mudança de versão — falhe
// de forma barulhenta, mas registre o suficiente para diagnosticar.
logger.error('Unhandled Veridia error', {
code: error.error,
requestId: error.requestId,
status: response.status,
});
throw new Error(`Unhandled Veridia error: ${error.error}`);
}
}

Repare que RETRYABLE e o switch concordam entre si, e que o ramo default registra o requestId. Os dois importam: os códigos que você ainda não trata são justamente os que você mais vai precisar diagnosticar.

Python

import time
import requests

RETRYABLE = {"rate_limited", "internal_error", "backend_unavailable"}

def call_veridia(method, url, *, max_attempts=4, **kwargs):
for attempt in range(max_attempts):
response = requests.request(method, url, **kwargs)
if response.ok:
return response.json()

error = response.json()
code = error.get("error")

if code == "invalid_body":
raise ValidationError(error)

if code == "secret_key_required":
# A chave é válida, só é da família errada. Rotacionar não ajuda.
raise ConfigError("GET /v1/verify/:id requires a secret key")

if code == "insufficient_credits":
notify_credits_exhausted()
raise BusinessError(error)

if code == "verification_not_found":
raise NotFoundError(error)

if code in RETRYABLE and attempt < max_attempts - 1:
retry_after = int(
response.headers.get("Retry-After")
or error.get("detail", {}).get("retry_after")
or 2 ** attempt
)
logger.warning(
"veridia_transient_error",
extra={"code": code, "request_id": error.get("requestId")},
)
time.sleep(retry_after)
continue

logger.error(
"veridia_error",
extra={"code": code, "request_id": error.get("requestId")},
)
raise ExternalServiceError(error)

Boas práticas

  • Faça o switch em error.error, não no status HTTP. Dois códigos diferentes compartilham 401 e dois compartilham 404; o código é a parte que diz o que fazer.
  • Tente de novo em exatamente três códigos: rate_limited, internal_error, backend_unavailable. Tentar de novo em qualquer outro reproduz a mesma falha e ainda queima orçamento de limite de taxa.
  • Sempre registre o requestId em log, inclusive no seu ramo de fallback.
  • Nunca exponha esses códigos a usuários finais. "Algo deu errado, tente novamente" para eles; o código, o requestId e o detail para os seus logs.
  • Alerte sobre insufficient_credits. É um evento de negócio, não um bug.
  • Espere códigos novos. Novos códigos podem aparecer em /v1 sem mudança de versão. Trate o caso default em vez de assumir exaustividade.

O que vem a seguir