Errores
El catálogo completo, qué se reintenta y qué no, y para qué sirve el requestId.
Todos los errores tienen la misma forma:
{
"error": {
"code": "SCOPE_REQUIRED",
"message": "Esta API key no tiene permiso para esta operación.",
"details": { "scope": "expenses.read" },
"requestId": "req_b0b926d7fbe34e53be33d869981cc9b9",
"docUrl": "https://docs.verifika.tech/docs/guias/errores"
}
}Ramifica por code, nunca por message. Los códigos son parte del contrato; los mensajes
están en español para humanos y pueden cambiar de redacción. Lo mismo con details: usa los
campos que esta página nombra y no dependas de los demás.
Catálogo
| HTTP | code | Qué pasó | ¿Reintentar? |
|---|---|---|---|
| 400 | BAD_REQUEST | Cuerpo o parámetros inválidos. details trae el detalle. También sale si tu llave entra a varios negocios y no mandaste companyId. | No, hasta corregir |
| 401 | UNAUTHORIZED | Falta la llave, o es inválida, revocada, vencida, o viene de una IP no permitida. | No |
| 403 | SCOPE_REQUIRED | La llave no tiene ese permiso. details.scope dice cuál. | No |
| 403 | PLAN_FEATURE_REQUIRED | El plan no incluye la función. details.feature dice cuál (public_api, webhooks_outbound, expenses_module…). Qué incluye tu plan lo dice GET /v1/plan. | No |
| 403 | PLAN_LIMIT_REACHED | Se agotó un límite del plan, casi siempre las verificaciones del mes. details.feature, details.limit y details.current. | No, hasta el mes siguiente o un cambio de plan |
| 403 | ACCOUNT_TRIAL_ENDED · ACCOUNT_CANCELED · ACCOUNT_READ_ONLY | La cuenta está bloqueada: terminó la prueba, se canceló o tiene un pago pendiente. Los tres traen details.readOnly: true y details.reason. Cierra también las lecturas. | No, hasta que el dueño la regularice |
| 403 | TEST_MODE_UNAVAILABLE | Llave de prueba (vk_test_) en un endpoint o una escritura sin modo prueba. Ver Modo de prueba. | No, usa una llave real |
| 403 | FORBIDDEN | El companyId no es de esta llave, la llave no sirve para esta API, o —en POST /v1/verifications— el phone es de alguien del equipo cuyo rol no puede registrar pagos. | No |
| 404 | NOT_FOUND | El recurso no existe o no es tuyo. | No |
| 404 | PHONE_NOT_REGISTERED | En POST /v1/verifications: el phone no está en el equipo del negocio. | No, hasta agregarlo en Equipo |
| 409 | IDEMPOTENCY_IN_PROGRESS | Esa Idempotency-Key se está procesando. | Sí, en unos segundos |
| 409 | IDEMPOTENCY_KEY_REUSED | Misma llave con cuerpo distinto. | No, usa una llave nueva |
| 409 | INCOME_NOT_EDITABLE | Ese ingreso no se edita ni se borra por API: no lo declaró nadie a mano (nació de un comprobante o del banco), o es un abono de cuentas por cobrar, que se anula desde el cuaderno. | No |
| 422 | RECEIVABLE_OVERPAYMENT | Un abono o una condonación por más de lo que debe el cliente. details.balance es el saldo real. | No, hasta corregir el monto |
| 422 | RECEIVABLE_ALREADY_PAID | La cuenta ya está en cero: no se salda otra vez ni se mueve de etapa. | No |
| 422 | RECEIVABLE_STAGE_REQUIRES_PAYMENT | Pediste la etapa PAID con saldo pendiente. Se salda con POST …/settle. | No |
| 422 | RECEIVABLE_NEGATIVE_BALANCE | Anular ese fiado dejaría al cliente con saldo a favor. Anula primero el abono. | No |
| 429 | RATE_LIMITED | Te pasaste del límite. | Sí, respetando Retry-After |
| 429 | TEST_MODE_LIMIT_REACHED | Tu llave de prueba llegó a 200 verificaciones en 24 horas. | Sí, más tarde |
| 503 | AI_UNAVAILABLE | La lectura con IA (de un comprobante o de una factura) falló. | Según details.retryable |
| 500 | INTERNAL_ERROR | Nos falló algo. | Sí, con espera creciente |
404 y 403 no se distinguen a propósito
Pedir un ingreso de otro negocio devuelve 404, no 403. Si respondiéramos "no tienes
permiso", estaríamos confirmando que ese id existe — y eso convierte el endpoint en una forma
de descubrir datos ajenos probando ids.
Los de verificaciones, conciliación y gastos con IA
Cada uno tiene su tabla en su guía:
- Verificar un comprobante —
IMAGE_REQUIRED,INVALID_IMAGE_URL,BLOCKED_IMAGE_URL,IMAGE_TOO_LARGE,IMAGE_TYPE_NOT_ALLOWED,IMAGE_FETCH_FAILED,IMAGE_FETCH_TIMEOUT,INVALID_IMAGE_BASE64,PHONE_REQUIRED—: Verifica desde tu propio bot. Ojo: un comprobante duplicado o que no es un comprobante no es un error: es un200constatus: DUPLICATEoREJECTED. - Conciliación —
STATEMENT_*,LINE_*,BULK_TOO_LARGE,SERVICE_BUSY—: Conciliación por API. - Gastos con IA —
EXPENSE_DUPLICATE,EXPENSE_SCAN_*,FILE_*—: Gastos con IA.
details.retryable y details.customerMessage
Muchos errores traen details.retryable: true quiere decir que repetir lo mismo (con la
misma Idempotency-Key) puede funcionar —una URL que no respondió, la IA que falló un momento—;
false, que va a fallar igual hasta que cambies algo. Cuando lo traen, créele más que a la regla
general de abajo.
Los errores de cupo agotado (PLAN_LIMIT_REACHED) o cuenta bloqueada (ACCOUNT_*) de una
verificación traen además details.customerMessage: la frase para la persona que pagó («En este
momento no podemos verificar pagos para este negocio. Comunícate directamente con el negocio.»),
que no le cuenta lo que pasa con la cuenta del negocio.
Qué reintentar
Regla corta: 429 y 5xx se reintentan con espera creciente; el resto, no. Un 403 no
se arregla insistiendo, y un 400 tampoco.
const REINTENTABLE = (status) => status === 429 || status >= 500;El requestId
Va en el cuerpo de todo error y en la cabecera X-Request-Id de toda respuesta, incluidas
las exitosas.
Guárdalo en tus logs. Con ese id, soporte encuentra tu petición exacta; sin él, un reporte de "a veces me da error" no se puede investigar.
Si tu sistema ya genera ids de traza, mándalo tú en X-Request-Id y lo respetamos, así tu
traza no se parte al entrar a Verifika.