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"]
}
}
}
| Campo | Tipo | Descrição |
|---|---|---|
error | string | Código legível por máquina — faça o switch neste, não no status HTTP |
message | string | Resumo legível por humanos, para logs |
requestId | string | Único por requisição — inclua nos chamados de suporte |
detail | object | Opcional. 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ódigo | HTTP | Vale tentar de novo? |
|---|---|---|
invalid_body | 400 | Não — corrija a requisição |
missing_api_key | 401 | Não |
invalid_api_key | 401 | Não |
secret_key_required | 401 | Não — use uma chave secreta |
insufficient_credits | 402 | Não — recarregue |
origin_not_allowed | 403 | Não |
verification_not_found | 404 | Não |
not_found | 404 | Não — URL errada |
rate_limited | 429 | Sim, depois do Retry-After |
internal_error | 500 | Sim, com backoff |
backend_unavailable | 503 | Sim, com backoff |
Repare na grafia exata. Não existe unauthorized, nem invalid_key, nem internal — este último é internal_error.
Catálogo de erros
invalid_body — 400 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") countryque não são exatamente duas letras maiúsculasverificationIdque 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:
| Reason | Significado |
|---|---|
doc_front_key_mismatch | keys.docFront não é o que o /init retornou |
selfie_key_mismatch | keys.selfie não é o que o /init retornou |
doc_back_key_mismatch | keys.docBack não é o que o /init retornou |
doc_front_not_uploaded | Nenhum objeto armazenado naquela chave — o cliente nunca enviou |
selfie_not_uploaded | O mesmo, para a selfie |
doc_front_disappeared | O objeto existia no momento da checagem mas sumiu antes do OCR (muito raro) |
Em PUT /v1/verify/upload/..., detail.reason pode ser:
| Reason | Significado |
|---|---|
missing_upload_token | Sem cabeçalho X-Veridia-Upload-Token — você não encaminhou slot.headers |
invalid_upload_token | O token não corresponde à intenção desta verificação |
key_mismatch | O papel na URL não é um dos que o /init emitiu |
invalid_role | Segmento de papel não reconhecido na URL |
not_a_jpeg | O corpo não começa com os bytes mágicos de JPEG FF D8 FF |
image_too_large | Corpo 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_small | Corpo com menos de 100 bytes — geralmente um envio truncado ou vazio |
upload_source_forbidden_for_biometric | X-Veridia-Capture-Source: upload em uma selfie ou frame de prova de vida |
no_active_challenge | Um 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_key — 401 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_key — 401 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_required — 401 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_credits — 402 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_allowed — 403 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 puro — yourapp.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
Originnenhum.
Veja Autenticação.
verification_not_found — 404 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_found — 404 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_limited — 429 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.
| Camada | Limite |
|---|---|
| Por IP do cliente, todas as rotas, verificado antes da autenticação | 100 / 60 s |
Por tenant — /v1/verify/init | 60 / 60 s |
Por tenant — /v1/verify/submit | 30 / 60 s |
Por tenant — /v1/verify/:id | 600 / 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_error — 500 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_unavailable — 503 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 compartilham401e dois compartilham404; 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
requestIdem 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
requestIde odetailpara 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
/v1sem mudança de versão. Trate o caso default em vez de assumir exaustividade.
O que vem a seguir
- Autenticação — famílias de chaves e a lista de origens
- Limites de taxa — os dois limites, e a ressalva do CGNAT
- Referência da API — de volta à visão geral