Manejo de Errores
Cada error devuelto por la API de la Plataforma Koywe trae un errorCode único y estable como MC00015 o DC00010. El prefijo en mayúsculas identifica el dominio (comercios, documentos, policy, …); el sufijo numérico es el error específico dentro de ese dominio.
¿Buscas un código específico? El Catálogo de Códigos de Error lista los 685 códigos registrados agrupados por prefijo.
Formato de la respuesta de error
Todos los errores de la API siguen el mismo envelope:
{
"statusCode": 400,
"timestamp": "2025-04-23T12:34:56.000Z",
"path": "/api/v1/organizations/org_.../merchants/mer_.../orders",
"errorCode": "BAA00008",
"message": "The destination account currency does not match the order destination currency."
}Campos:
errorCode— Ramifica siempre aquí. El código prefijado es estable entre releases.statusCode— Código HTTP; útil para lógica de retry genérica.message— Legible por humanos. Puede reescribirse sin aviso; no lo parsees.timestamp,path— Útiles para tickets de soporte y correlación de logs.
Ramifica tu integración por errorCode, no por message. Los mensajes se refinan con el tiempo; los códigos son contratos.
La convención de prefijos
Los códigos siguen el patrón <PREFIJO><5+ dígitos> — por ejemplo MC00015, POL00007, WE000001.
| Prefijo | Dominio | Prefijo | Dominio |
|---|---|---|---|
AUTH | Autenticación y credenciales | OR | Órdenes |
BAA | Cuentas bancarias | ORG | Organizaciones |
BT | Transferencia de saldo | PER | Permisos |
CT | Contactos | PIVBA | Cuentas bancarias virtuales de entrada |
DC | Documentos (validación) | PL | Payment links |
DL | Deals | PMP | Proveedores de método de pago |
EWW | Embedded wallet | POL | Policy (MFA y aprobaciones) |
IMT | Transferencia inter-merchant | QE | Cotizaciones |
LE | Ledger | RE | Reportes |
MC | Comercios | WE | Webhooks |
Ver el mapa completo prefijo → dominio en el catálogo para los 51 prefijos.
Códigos de estado HTTP
| Status | Significado | Acción |
|---|---|---|
200 | Éxito | Continuar |
400 | Bad Request | Corrige los parámetros |
401 | Unauthorized | Refresca el token / revisa credenciales |
403 | Forbidden | Revisa permisos y el contexto de organización/comercio |
404 | Not Found | Verifica el ID del recurso |
408 | Timeout | Reintenta con backoff |
409 | Conflict | Duplicado / problema de idempotencia |
410 | Gone | Recurso (cotización, payment link) expirado |
422 | Validation | Corrige la validación de entrada |
428 | Precondition Required | Satisface la policy (MFA o aprobación) |
429 | Rate Limit | Espera y reintenta |
500 | Server Error | Reintenta con backoff |
502 / 503 / 504 | Problema upstream | Reintenta con backoff |
Errores comunes que encontrarás primero
Algunos códigos vale la pena memorizarlos porque tropiezan a la mayoría de integraciones. El Catálogo de Códigos de Error tiene una lista más larga.
| Código | Status | Cuándo ocurre | Fix |
|---|---|---|---|
AUTH00001 | 401 | Token rechazado | Re-autentica con koywe auth login o refresca variables de entorno |
MC00015 | 403 | merchantId no pertenece a la organización autenticada | Revisa organizationId / merchantId en config. Los GET devuelven vacío silenciosamente; solo los POST fallan. |
DC00010 | 400 | El número de documento no cumple el formato del tipo/país | Usa documentos de prueba válidos (ver Pruebas en sandbox) |
BAA00008 | 400 | Moneda de cuenta destino ≠ moneda destino de la orden | Elige una cuenta destino cuya moneda coincida |
BAA00014 | 409 | Payout excede el saldo de la cuenta virtual | Acredita la cuenta o reduce el monto |
QE00003 | 410 | Cotización expirada | Crea una nueva cotización |
QE00004 | 409 | Cotización ya usada por otra orden | Las cotizaciones son de un solo uso — crea una nueva |
POL00002 | 403 | Sin policy en la organización | koywe policy create y luego añade una regla ALLOW |
POL00007 | 428 | Aprobación requerida; orden en ON_HOLD | Pasa --wait en flow order, aprueba en dashboard, o usa --mfa-token |
PMP00009 | 408 | Proveedor de pago upstream timeout | Reintenta con backoff |
Estrategias de recuperación
1. Reintentar fallos transitorios con backoff
Reintenta en errores de red y en el rango upstream-unreachable (408, 429, 5xx). Nunca reintentes en errores 4xx de validación — van a seguir fallando con la misma entrada.
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
const status = error.response?.status;
const isRetryable =
status === 408 ||
status === 429 ||
(status >= 500 && status < 600) ||
error.code === 'ECONNABORTED';
const isLastAttempt = i === maxRetries - 1;
if (!isRetryable || isLastAttempt) throw error;
// Backoff exponencial con jitter: ~1s, 2s, 4s
const delay = Math.pow(2, i) * 1000 + Math.random() * 500;
await new Promise(r => setTimeout(r, delay));
}
}
}2. Manejar operaciones bloqueadas por policy
POL00006, POL00007 y POL00008 (todos 428 Precondition Required) significan que la operación necesita verificación MFA, aprobación humana, o ambas antes de ejecutarse. La orden queda en ON_HOLD — la llamada de creación tiene éxito y devuelve la orden — y transiciona cuando se satisface la aprobación/MFA.
async function createOrderAndWait(token, orgId, merchantId, payload) {
const order = await createPayinOrder(token, orgId, merchantId, payload);
if (order.status !== 'ON_HOLD') return order;
// Haz polling hasta que la aprobación se resuelva — `flow order --wait` del
// CLI hace esto por ti. Las integraciones de producción deberían escuchar
// webhooks en vez de hacer polling (ver "Webhooks en Profundidad").
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 5000));
const refreshed = await getOrder(token, orgId, merchantId, order.id);
if (refreshed.status !== 'ON_HOLD') return refreshed;
}
throw new Error(`Orden ${order.id} sigue en ON_HOLD tras 5 minutos`);
}3. Degradación elegante para métodos de pago
Si un método de pago no está disponible para el país/moneda (PMP00012, PMP00017) o el proveedor está momentáneamente caído (PMP00003, PMP00008), cae a un método secundario:
async function createPayinWithFallback(orderBase) {
const attempts = [
{ method: 'PSE', extra: { bankAccount: { name: 'BANCOLOMBIA' } } },
{ method: 'NEQUI' },
];
for (const pm of attempts) {
try {
return await createPayinOrder(token, orgId, merchantId, {
...orderBase,
paymentMethods: [pm],
});
} catch (error) {
const code = error.response?.data?.errorCode;
const retryable = ['PMP00012', 'PMP00017', 'PMP00003', 'PMP00008'];
if (!retryable.includes(code)) throw error;
}
}
throw new Error('No hay método de pago disponible para este país/moneda');
}4. Traducir códigos a mensajes de usuario final
Mapea códigos de error a copy amigable — nunca muestres códigos crudos a los clientes:
function translateErrorToUser(error) {
const code = error.response?.data?.errorCode;
const userMessages = {
BAA00014: 'No tenemos saldo suficiente para completar este pago ahora mismo.',
DC00010: 'Por favor revisa el número de documento.',
QE00003: 'Esa cotización expiró — por favor intenta de nuevo.',
PMP00006: 'Ese monto está por debajo del mínimo del método de pago.',
PMP00007: 'Ese monto excede el límite del método de pago.',
POL00004: 'Esta transacción excede tu límite configurado. Contacta a un admin.',
POL00007: 'Esta operación está esperando aprobación. Te avisaremos pronto.',
};
return userMessages[code] ?? 'Algo salió mal. Intenta de nuevo o contacta a soporte.';
}5. Loguea todo con contexto de correlación
Al abrir un ticket con soporte, lo más útil que puedes enviar es el envelope crudo más path y timestamp:
function logError(error, context) {
const apiError = error.response?.data ?? {};
console.error(JSON.stringify({
timestamp: new Date().toISOString(),
...context,
statusCode: apiError.statusCode,
errorCode: apiError.errorCode,
message: apiError.message,
path: apiError.path,
apiTimestamp: apiError.timestamp,
}));
}
try {
await createPayoutOrder(token, orgId, merchantId, payoutData);
} catch (error) {
logError(error, { operation: 'create_payout', merchantId });
throw error;
}Manejador de errores listo para producción
Todo lo anterior, integrado en una clase reutilizable:
class KoyweErrorHandler {
constructor({ maxRetries = 3, logger = console } = {}) {
this.maxRetries = maxRetries;
this.logger = logger;
}
async execute(fn, context = {}) {
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
this.logError(error, { ...context, attempt });
if (!this.shouldRetry(error, attempt)) {
throw this.formatError(error);
}
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 500;
await new Promise(r => setTimeout(r, delay));
}
}
}
shouldRetry(error, attempt) {
if (attempt >= this.maxRetries - 1) return false;
const status = error.response?.status;
return (
status === 408 ||
status === 429 ||
(status >= 500 && status < 600) ||
error.code === 'ECONNABORTED'
);
}
logError(error, context) {
const apiError = error.response?.data ?? {};
this.logger.error('Error de API Koywe:', {
timestamp: new Date().toISOString(),
...context,
errorCode: apiError.errorCode,
statusCode: apiError.statusCode,
message: apiError.message,
path: apiError.path,
});
}
formatError(error) {
const apiError = error.response?.data;
if (!apiError) return error;
const err = new Error(apiError.message);
err.errorCode = apiError.errorCode;
err.statusCode = apiError.statusCode;
err.path = apiError.path;
return err;
}
}
const errorHandler = new KoyweErrorHandler({ maxRetries: 3 });
const order = await errorHandler.execute(
() => createPayinOrder(token, orgId, merchantId, orderData),
{ operation: 'create_payin', merchantId },
);Chequeo de salud del servicio
Antes de diagnosticar errores a nivel de request, confirma que la API esté alcanzable. Koywe expone un endpoint ligero y sin autenticación:
GET /api/v1/healthz{ "status": "ok" }Características:
- Sin autenticación — útil para páginas de estado y smoke tests en CI.
- Liviano — seguro para golpear frecuentemente desde dashboards.
- Devuelve
503si una dependencia upstream está degradada.