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"]
}
}
}
| Campo | Tipo | Descripción |
|---|---|---|
error | string | Código legible por máquina — ramificá sobre esto, no sobre el status HTTP |
message | string | Resumen legible por humanos, para los logs |
requestId | string | Único por request — incluilo en los tickets de soporte |
detail | object | Opcional. 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ódigo | HTTP | ¿Reintentable? |
|---|---|---|
invalid_body | 400 | No — corregí la request |
missing_api_key | 401 | No |
invalid_api_key | 401 | No |
secret_key_required | 401 | No — usá una clave secreta |
insufficient_credits | 402 | No — recargá saldo |
origin_not_allowed | 403 | No |
verification_not_found | 404 | No |
not_found | 404 | No — URL equivocada |
rate_limited | 429 | Sí, después de Retry-After |
internal_error | 500 | Sí, con backoff |
backend_unavailable | 503 | Sí, 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_body — 400 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") countryno es exactamente dos letras mayúsculasverificationIdno 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:
| Reason | Significado |
|---|---|
doc_front_key_mismatch | keys.docFront no es lo que devolvió /init |
selfie_key_mismatch | keys.selfie no es lo que devolvió /init |
doc_back_key_mismatch | keys.docBack no es lo que devolvió /init |
doc_front_not_uploaded | No hay objeto almacenado en esa key — el cliente nunca subió |
selfie_not_uploaded | Lo mismo, para la selfie |
doc_front_disappeared | El 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:
| Reason | Significado |
|---|---|
missing_upload_token | Sin header X-Veridia-Upload-Token — no reenviaste slot.headers |
invalid_upload_token | El token no corresponde a la intención de esta verificación |
key_mismatch | El rol de la URL no es uno de los que emitió /init |
invalid_role | Segmento de rol no reconocido en la URL |
not_a_jpeg | El body no empieza con los bytes mágicos de JPEG FF D8 FF |
image_too_large | Body 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_small | Body de menos de 100 bytes — normalmente una subida truncada o vacía |
upload_source_forbidden_for_biometric | X-Veridia-Capture-Source: upload en una selfie o un frame de prueba de vida |
no_active_challenge | Se 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_key — 401 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_key — 401 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_required — 401 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_credits — 402 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_allowed — 403 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 pelado — yourapp.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
Originen absoluto.
Ver Autenticación.
verification_not_found — 404 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_found — 404 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_limited — 429 Too Many Requests
Tocaste uno de dos límites. detail.retry_after y el header Retry-After dan la espera en segundos.
| Capa | Límite |
|---|---|
| Por IP de cliente, todas las rutas, verificado antes de la autenticación | 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 |
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_error — 500 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_unavailable — 503 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 comparten401y otros dos comparten404; 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
requestIdy eldetailpara tus logs. - Alertá sobre
insufficient_credits. Es un evento de negocio, no un bug. - Esperá códigos nuevos. Pueden aparecer códigos nuevos en
/v1sin subir la versión. Manejá el caso por defecto en lugar de asumir exhaustividad.
Qué sigue
- Autenticación — familias de claves y la lista de orígenes
- Límites de tasa — los dos límites, y la salvedad de CGNAT
- Referencia de la API — volver a la vista general