Errores de la API
Todos los errores de Pabilo llegan con la misma forma y un código estable contra el que puedes programar.
Formato de la respuesta
Sin importar el endpoint, un error siempre devuelve exactamente dos claves. El status HTTP acompaña al código.
{
"message": "bad request: client phone or email is required: error building subscription",
"error": "BAD_REQUEST"
}| Campo | Para qué sirve |
|---|---|
| error | El código estable. Es lo único contra lo que debes programar. No cambia entre versiones. |
| message | Texto humano para depurar. Arrastra todo el contexto del intento, separado por :, del más general al más específico. Puede cambiar sin aviso. |
Nunca compares contra message
error.Errores no controlados
Si algo falla fuera de los casos previstos, el status es 500 y el código es internal_server_error — en minúscula, a diferencia del resto que va en mayúsculas. Trátalo como un fallo inesperado y repórtalo con el message completo.
{
"message": "unexpected EOF",
"error": "internal_server_error"
}Catálogo de códigos
Autenticación y permisos
| Código | HTTP | Significado | Qué hacer |
|---|---|---|---|
| UNAUTHORIZED | 401 | No enviaste credenciales o el token/API key no es válido. | Revisa el header Authorization: Bearer o appKey. Si el JWT expiró, refréscalo. |
| FORBIDDEN | 403 | Estás autenticado pero el recurso no es tuyo, o tu empleado no tiene el permiso. | No reintentes. Verifica que el recurso pertenezca a tu cuenta. |
| USER_IS_NOT_ACTIVE | 400 | La cuenta está deshabilitada. | Contacta a soporte. |
| CLIENT_IS_NOT_ACTIVE | 400 | El cliente con el que operas está deshabilitado. | Reactívalo con PUT /v1/clients/:clientId/toggle-disabled. |
Plan y créditos
| Código | HTTP | Significado | Qué hacer |
|---|---|---|---|
| PLAN_IS_NOT_ACTIVE | 400 | El plan venció o nunca se activó. | Renueva el plan. No reintentes hasta entonces. |
| PLAN_IS_ALREADY_ACTIVE | 400 | Intentaste activar un plan que ya está vigente. | Ignorable: el estado deseado ya se cumple. |
| NOT_ENOUGH_CREDITS | 400 | No hay créditos para cobrar la verificación. | Recarga créditos. No reintentes: cada intento fallaría igual. |
| REQUEST_LIMIT_REACHED | 400 | Se agotó la cuota de peticiones del plan. | Espera al siguiente período o sube de plan. |
| BANK_ACCOUNT_LIMIT_REACHED | 400 | Llegaste al máximo de cuentas bancarias del plan. | Elimina una cuenta o sube de plan. |
| RENOVATION_IS_ALREADY_PAID | 400 | La renovación ya estaba pagada. | Ignorable. |
Verificación de pagos
| Código | HTTP | Significado | Qué hacer |
|---|---|---|---|
| PAYMENT_NOT_FOUND | 400 | El banco no tiene ningún movimiento con esa referencia y monto. | Muéstraselo al pagador: la referencia está mal o el pago aún no se refleja. |
| PAYMENT_AMOUNT_NOT_VALID | 400 | La referencia existe pero el monto no coincide con el esperado. | Muéstraselo al pagador. Revisa status_detail para el detalle. |
| PAYMENT_ALREADY_EXISTS | 400 | Esa transferencia ya se usó para confirmar otro pago. | No reintentes. Pide una transferencia nueva. |
| IS_NOT_POSITIVE_PAYMENT | 400 | El movimiento encontrado es un débito, no un crédito. | La referencia corresponde a un egreso; pide la correcta. |
| IS_NOT_POSITIVE_AMOUNT | 400 | El monto enviado es cero o negativo. | Corrige el monto antes de reintentar. |
| INVALID_MOVEMENT_TYPE | 400 | El tipo de movimiento no aplica a esa cuenta bancaria. | Consulta los tipos válidos del banco. |
| MOVEMENT_TYPE_REQUIRED | 400 | Ese banco exige indicar el tipo de movimiento y no lo enviaste. | Agrega movement_type al body. |
Banco y conexión
| Código | HTTP | Significado | Qué hacer |
|---|---|---|---|
| BANK_NOT_AVAILABLE | 400 | El banco no respondió o devolvió un error de plataforma. | Reintentable con backoff. |
| BANK_TEMPORARILY_INACTIVE | 503 | El banco está marcado como caído del lado de Pabilo. | Reintentable con backoff largo. |
| BANK_TOO_MANY_REQUESTS | 429 | El banco está limitando la tasa de peticiones. | Reintentable: espera antes del siguiente intento. |
| USER_BANCK_NOT_FOUND | 400 | La cuenta bancaria no existe o no es tuya. | Revisa el user_bank_id. |
| USER_BANCK_BAD_PASSWORD | 400 | Las credenciales guardadas ya no sirven. | Actualiza la contraseña de la cuenta bancaria. |
| USER_BANCK_PASSWORD_EXPIRED | 400 | El banco pide cambiar la contraseña. | Cámbiala en el portal del banco y actualízala en Pabilo. |
| USER_BANCK_BLOCKED | 400 | El banco bloqueó el usuario. | Desbloquéalo con el banco. Pabilo envía un correo cuando esto pasa. |
| USER_BANCK_BAD_API_KEY | 400 | La API key del banco (Binance, Mercantil empresa) no es válida. | Regenera la credencial en el banco. |
| USER_BANK_ALREADY_EXISTS | 400 | Ya registraste esa cuenta bancaria. | Usa la existente. |
| USER_BANK_IS_DISABLED | 400 | La cuenta bancaria está deshabilitada. | Reactívala antes de verificar pagos con ella. |
| INVALID_BANK_PROVIDER | 500 | El proveedor bancario guardado no existe en el sistema. | Reporta a soporte: es un dato inconsistente. |
| SESSION_ALREADY_ACTIVE | 400 | Ya hay una sesión abierta contra el banco para esa cuenta. | Reintentable: espera a que la sesión anterior termine. |
| PROXY_ERROR | 400 | Falló la salida a internet asignada a esa cuenta. | Reintentable con backoff. |
Cuenta y registro
| Código | HTTP | Significado | Qué hacer |
|---|---|---|---|
| THIS_EMAIL_ALREADY_EXISTS | 400 | Ese correo ya está registrado. | Usa otro correo. |
| THIS_USERNAME_ALREADY_EXISTS | 400 | El usuario o nickname ya está tomado por un usuario, empleado o cliente. | Elige otro. Con clientes puedes usar generate_nick para que Pabilo elija. |
| INVALID_PHONE | 400 | El teléfono no tiene un formato reconocible. | Usa +584121234567, 04121234567, 4121234567 o 584121234567. |
| TEMPORAL_CODE_NOT_AVAILABLE | 400 | El código expiró, ya se usó o se agotaron los intentos. | Pide un código nuevo. |
Secretos bancarios
| Código | HTTP | Significado | Qué hacer |
|---|---|---|---|
| COORDINATE_KEY_EXTRACTION_FAILED | 422 | No se pudo leer la tarjeta de coordenadas de la imagen. | Sube una foto más nítida y completa. |
| COORDINATE_KEY_NOT_VALID | 422 | La imagen no es una tarjeta de coordenadas. | Sube la imagen correcta. |
Notificaciones de dispositivo
| Código | HTTP | Significado | Qué hacer |
|---|---|---|---|
| NOTIFICATION_IGNORED | 400 | Es una notificación de sistema conocida, no un pago. Ruido esperado. | Ignorable: no reintentes ni la reenvíes. |
| NOTIFICATION_PARSE_FAILED | 400 | Ningún patrón conocido reconoció el texto de la notificación. | Se guarda para escribir el patrón faltante. No reintentes. |
Genéricos
| Código | HTTP | Significado | Qué hacer |
|---|---|---|---|
| BAD_REQUEST | 400 | Validación genérica. El detalle va en message. | Lee message: dice exactamente qué campo falta o está mal. |
| NOT_FOUND | 404 | El recurso no existe. | Revisa el id. |
| PAGE_NOT_FOUND | 404 | La ruta no existe en la API. | Revisa la URL y el método. |
| METHOD_NOT_SUPPORT | 405 | El método HTTP no aplica a esa ruta. | Revisa el método en la documentación del endpoint. |
| NOT_IMPLEMENTED | 404 | La operación no está disponible para ese proveedor bancario. | No todos los bancos soportan todas las operaciones. |
| INTERNAL_ERROR | 500 | Error interno controlado. | Reintentable una vez; si persiste, reporta a soporte con el message. |
| MISSING_CONFIG | 400 | Falta configuración en la cuenta para completar la operación. | Lee message para saber qué falta. |
| GET_CURRENCY_RATE_FAILED | 500 | No se pudo obtener la tasa de cambio. | Reintentable con backoff. |
Sobre USER_BANCK_*
BANCK. Es un error de tipeo histórico que ya está en producción; renombrarlo rompería a todos los integradores, así que se mantiene tal cual.Errores de verificación de pago
Cuando falla el cobro de un link de pago hay una segunda fuente de información además del código: el campo payment_link.status_detail, que viene en español y está pensado para mostrárselo al pagador. Lo recibes en el webhook y al consultar el link de pago.
| Situación | status_detail |
|---|---|
| Monto no coincide | Monto del pago no coincide con el esperado |
| Banco no disponible | Banco temporalmente no disponible: <detalle> |
| Pago no encontrado | Pago no encontrado en el banco con la referencia proporcionada |
| Pago ya usado | El pago ya fue procesado anteriormente |
| Otro | El mensaje del banco, o «Error al procesar el pago» |
Duplicados detectados por aproximación
status_detail lo dice explícitamente: «El usuario intento usar un pago ya existente (detectado por referencia+monto+fecha)». Sirve para distinguir un duplicado real de un falso positivo.Manejo recomendado
En la práctica los errores caen en tres grupos que se tratan distinto: los que conviene reintentar, los que requieren una acción del dueño de la cuenta, y los que hay que mostrarle al pagador.
// Reintentar con backoff: son transitorios.
const REINTENTABLES = new Set([
"BANK_NOT_AVAILABLE",
"BANK_TEMPORARILY_INACTIVE",
"BANK_TOO_MANY_REQUESTS",
"PROXY_ERROR",
"SESSION_ALREADY_ACTIVE",
]);
// Requieren que el dueño de la cuenta haga algo. Reintentar no sirve.
const REQUIEREN_ACCION = new Set([
"NOT_ENOUGH_CREDITS",
"PLAN_IS_NOT_ACTIVE",
"REQUEST_LIMIT_REACHED",
"USER_BANCK_BAD_PASSWORD",
"USER_BANCK_BLOCKED",
"USER_BANK_IS_DISABLED",
]);
// Se le muestran al pagador: el problema está en la transferencia.
const DEL_PAGADOR = new Set([
"PAYMENT_NOT_FOUND",
"PAYMENT_AMOUNT_NOT_VALID",
"PAYMENT_ALREADY_EXISTS",
"IS_NOT_POSITIVE_PAYMENT",
]);
async function pagar(linkId, body, intento = 0) {
const res = await fetch(`https://api.pabilo.app/v1/paymentlink/${linkId}/pay`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (res.ok) return res.json();
const { error, message } = await res.json();
if (REINTENTABLES.has(error) && intento < 3) {
await new Promise((r) => setTimeout(r, 2 ** intento * 1000));
return pagar(linkId, body, intento + 1);
}
if (REQUIEREN_ACCION.has(error)) {
// Avisa al dueño de la cuenta; no reintentes.
notificarAlComercio(error, message);
throw new Error(error);
}
if (DEL_PAGADOR.has(error)) {
// Muestra el motivo en el checkout.
mostrarAlPagador(error);
throw new Error(error);
}
// Cualquier otro: loguea el message completo, es lo que sirve para depurar.
console.error("pabilo:", error, message);
throw new Error(error);
}Un 4xx no siempre es culpa tuya
400, incluso los transitorios. Por eso conviene ramificar por error y no por el status HTTP.