Saltar al contenido principal

Errores

Todos los errores de la API de Veridia comparten una misma forma. Esta página es la lista canónica de códigos error.

Forma de la respuesta de error

{
"error": "invalid_body",
"message": "Request body failed validation",
"requestId": "9f511d92ac11236d-SJC",
"detail": {
"fieldErrors": {
"country": ["expected 2 characters"]
}
}
}
CampoTipoDescripción
errorstringCódigo legible por máquina — ramificá sobre esto, no sobre el status HTTP
messagestringResumen legible por humanos, para los logs
requestIdstringÚnico por request — incluilo en los tickets de soporte
detailobjectOpcional. Errores de campo, un reason, o retry_after

El mismo requestId está en el header de respuesta X-Request-Id, así que podés correlacionar incluso cuando falla el parseo del JSON.

Los códigos de un vistazo

CódigoHTTP¿Reintentable?
invalid_body400No — corregí la request
missing_api_key401No
invalid_api_key401No
secret_key_required401No — usá una clave secreta
insufficient_credits402No — recargá saldo
origin_not_allowed403No
verification_not_found404No
not_found404No — URL equivocada
rate_limited429, después de Retry-After
internal_error500, con backoff
backend_unavailable503, con backoff

Prestá atención a la escritura exacta. No existe unauthorized, ni invalid_key, ni internal — el último es internal_error.

Catálogo de errores

invalid_body400 Bad Request

El body de la request no pasó la validación del esquema. Revisá detail.fieldErrors.

Causas comunes:

  • Tipo equivocado, o un string por encima de su límite de longitud (userRef > 128, submittedFullName > 255)
  • Valor de enum inválido (documentType: "id" en lugar de "dni")
  • country no es exactamente dos letras mayúsculas
  • verificationId no coincide con ^vf_[A-Za-z0-9]{16,24}$

Los campos desconocidos no causan este error. Se descartan en silencio y la request tiene éxito. Si un campo que mandaste no tuvo efecto, esa es la razón — revisá la tabla de campos del endpoint en lugar de esperar un error que no va a llegar.

En /submit, detail.reason puede ser:

ReasonSignificado
doc_front_key_mismatchkeys.docFront no es lo que devolvió /init
selfie_key_mismatchkeys.selfie no es lo que devolvió /init
doc_back_key_mismatchkeys.docBack no es lo que devolvió /init
doc_front_not_uploadedNo hay objeto almacenado en esa key — el cliente nunca subió
selfie_not_uploadedLo mismo, para la selfie
doc_front_disappearedEl objeto existía al momento de la verificación pero desapareció antes del OCR (muy raro)

En PUT /v1/verify/upload/..., detail.reason puede ser:

ReasonSignificado
missing_upload_tokenSin header X-Veridia-Upload-Token — no reenviaste slot.headers
invalid_upload_tokenEl token no corresponde a la intención de esta verificación
key_mismatchEl rol de la URL no es uno de los que emitió /init
invalid_roleSegmento de rol no reconocido en la URL
not_a_jpegEl body no empieza con los bytes mágicos de JPEG FF D8 FF
image_too_largeBody de más de 2 MB. Reescalá la imagen antes de subir; 2 MB de JPEG son 2-4 MP, más de lo que el OCR necesita
image_too_smallBody de menos de 100 bytes — normalmente una subida truncada o vacía
upload_source_forbidden_for_biometricX-Veridia-Capture-Source: upload en una selfie o un frame de prueba de vida
no_active_challengeSe mandó un frame de prueba de vida para una verificación que nunca optó por ella

Recuperación: corregí la request. Estos son errores del cliente; reintentar sin cambios los reproduce exactamente.

missing_api_key401 Unauthorized

Sin header Authorization, o no es un token Bearer.

Recuperación: mandá Authorization: Bearer <clave>.

Tené en cuenta que los endpoints de subida y de reto no quieren este header — usan X-Veridia-Upload-Token en su lugar.

invalid_api_key401 Unauthorized

El token no coincide con ninguna clave conocida. Nunca existió, fue revocada, o no parsea.

El parser acepta exactamente cuatro prefijos: qv_pub_, qv_pubt_, qv_sec_, qv_sect_, seguidos de 16-64 caracteres de [A-Za-z0-9_]. Una clave de la forma qv_pub_test_... no existe — el prefijo de test es qv_pubt_.

Recuperación: revisá primero el prefijo y el entorno, después el panel. Acordate de que la revocación es inmediata, sin período de gracia.

secret_key_required401 Unauthorized

Se usó una clave publicable en GET /v1/verify/:id, el único endpoint que requiere una clave secreta.

La clave está bien. Es la familia equivocada para esta llamada. Las claves publicables viajan dentro del código fuente de tu página, así que nunca se les permite leer resultados de verificación. Este código existe justamente para que no salgas a cazar un typo en una clave válida.

Recuperación: creá una clave secreta y llamá a este endpoint desde tu servidor. Nunca la pongas en código de front-end.

insufficient_credits402 Payment Required

El saldo de créditos del tenant es cero. Se dispara durante la autenticación, así que puede aparecer en /init antes de que pase cualquier otra cosa — una causa frecuente del "el widget simplemente falla" en una cuenta de prueba agotada.

Recuperación: recargá saldo en el panel. Vale la pena monitorearlo como alerta de negocio: significa que se están rechazando verificaciones.

origin_not_allowed403 Forbidden

Una request de navegador llevó un Origin que no está en la lista de orígenes permitidos de la clave publicable, y esa lista no está vacía.

Recuperación: agregá el hostname peladoyourapp.com, staging.yourapp.com, localhost. No https://yourapp.com, y no http://localhost:3000; la verificación compara solo hostnames, así que un esquema o un puerto nunca coinciden.

Dos cosas que conviene saber antes de tocar esta lista:

  • Una lista vacía permite todos los orígenes. Agregar tu primera entrada cambia la clave de "cualquier origen" a "solo estos". Agregá una entrada que no coincida y pasás de todo permitido a todo bloqueado en un solo paso.
  • La verificación nunca aplica a clientes que no son navegadores, que no mandan header Origin en absoluto.

Ver Autenticación.

verification_not_found404 Not Found

El verificationId no existe, está malformado, o pertenece a otro tenant. La verificación de otro tenant devuelve 404 en lugar de 403 para que la respuesta no confirme que el ID existe.

En /submit y en las subidas, también significa que la intención de verificación expiró. La intención vive una hora después de /init; pasado eso, reiniciá el flujo.

En GET /v1/verify/:id no existe tal expiración. Los resultados quedan legibles indefinidamente — la pertenencia se verifica contra el tenant registrado con la verificación misma, no contra la intención de vida corta. No necesitás espejar los resultados localmente para mantenerlos accesibles.

not_found404 Not Found

La ruta no existe.

Recuperación: revisá la URL. El clásico es /v1/verifications o /v1/verifications/:id — ninguno existe. El endpoint de resultados es GET /v1/verify/:id.

rate_limited429 Too Many Requests

Tocaste uno de dos límites. detail.retry_after y el header Retry-After dan la espera en segundos.

CapaLímite
Por IP de cliente, todas las rutas, verificado antes de la autenticación100 / 60 s
Por tenant — /v1/verify/init60 / 60 s
Por tenant — /v1/verify/submit30 / 60 s
Por tenant — /v1/verify/:id600 / 60 s

La capa por IP es la que sorprende a la gente: corre antes de leer tu clave, cubre los endpoints de subida y de reto (que no tienen ningún límite de tenant), y es con la que chocan los usuarios móviles detrás de direcciones CGNAT compartidas mientras tus contadores de tenant se ven ociosos.

Recuperación: esperá el Retry-After y reintentá. Si se repite, leé Límites de tasa — en particular la tabla de cantidad de requests por verificación.

internal_error500 Internal Server Error

Algo se rompió de nuestro lado.

Recuperación: reintentá con backoff exponencial. Si persiste, abrí un ticket con el requestId — para este error en especial, es la única forma de rastrear qué pasó.

backend_unavailable503 Service Unavailable

El pipeline de verificación está inalcanzable, o el circuit breaker se abrió después de fallas repetidas. Puede venir detail.retry_after.

Recuperación: reintentá con backoff. Este es el error transitorio más probable en operación normal — asegurate de que tu política de reintentos realmente lo incluya.

Manejar bien los errores

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' }));

// Ramificá sobre el código de error, no sobre el 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':
// La clave es válida — es la familia equivocada. No la rotes.
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:
// Pueden aparecer códigos nuevos en /v1 sin subir la versión — fallá
// ruidosamente pero registrá lo 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}`);
}
}

Notá que RETRYABLE y el switch concuerdan, y que la rama por defecto registra el requestId. Las dos cosas importan: los códigos que todavía no manejás son los que más vas a necesitar 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":
# La clave es válida, solo que es la familia equivocada. Rotarla no ayuda.
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)

Buenas prácticas

  • Ramificá sobre error.error, no sobre el status HTTP. Dos códigos distintos comparten 401 y otros dos comparten 404; el código es la parte que te dice qué hacer.
  • Reintentá exactamente tres códigos: rate_limited, internal_error, backend_unavailable. Reintentar cualquier otra cosa reproduce la misma falla y encima quema presupuesto de límite de tasa.
  • Registrá siempre el requestId, incluso en tu rama de fallback.
  • Nunca expongas estos códigos a los usuarios finales. Para ellos, "algo salió mal, probá de nuevo"; el código, el requestId y el detail para tus logs.
  • Alertá sobre insufficient_credits. Es un evento de negocio, no un bug.
  • Esperá códigos nuevos. Pueden aparecer códigos nuevos en /v1 sin subir la versión. Manejá el caso por defecto en lugar de asumir exhaustividad.

Qué sigue