Skip to Content
Temas AvanzadosManejo de Errores

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:

  • errorCodeRamifica 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.

PrefijoDominioPrefijoDominio
AUTHAutenticación y credencialesORÓrdenes
BAACuentas bancariasORGOrganizaciones
BTTransferencia de saldoPERPermisos
CTContactosPIVBACuentas bancarias virtuales de entrada
DCDocumentos (validación)PLPayment links
DLDealsPMPProveedores de método de pago
EWWEmbedded walletPOLPolicy (MFA y aprobaciones)
IMTTransferencia inter-merchantQECotizaciones
LELedgerREReportes
MCComerciosWEWebhooks

Ver el mapa completo prefijo → dominio en el catálogo para los 51 prefijos.


Códigos de estado HTTP

StatusSignificadoAcción
200ÉxitoContinuar
400Bad RequestCorrige los parámetros
401UnauthorizedRefresca el token / revisa credenciales
403ForbiddenRevisa permisos y el contexto de organización/comercio
404Not FoundVerifica el ID del recurso
408TimeoutReintenta con backoff
409ConflictDuplicado / problema de idempotencia
410GoneRecurso (cotización, payment link) expirado
422ValidationCorrige la validación de entrada
428Precondition RequiredSatisface la policy (MFA o aprobación)
429Rate LimitEspera y reintenta
500Server ErrorReintenta con backoff
502 / 503 / 504Problema upstreamReintenta 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ódigoStatusCuándo ocurreFix
AUTH00001401Token rechazadoRe-autentica con koywe auth login o refresca variables de entorno
MC00015403merchantId no pertenece a la organización autenticadaRevisa organizationId / merchantId en config. Los GET devuelven vacío silenciosamente; solo los POST fallan.
DC00010400El número de documento no cumple el formato del tipo/paísUsa documentos de prueba válidos (ver Pruebas en sandbox)
BAA00008400Moneda de cuenta destino ≠ moneda destino de la ordenElige una cuenta destino cuya moneda coincida
BAA00014409Payout excede el saldo de la cuenta virtualAcredita la cuenta o reduce el monto
QE00003410Cotización expiradaCrea una nueva cotización
QE00004409Cotización ya usada por otra ordenLas cotizaciones son de un solo uso — crea una nueva
POL00002403Sin policy en la organizaciónkoywe policy create y luego añade una regla ALLOW
POL00007428Aprobación requerida; orden en ON_HOLDPasa --wait en flow order, aprueba en dashboard, o usa --mfa-token
PMP00009408Proveedor de pago upstream timeoutReintenta 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 503 si una dependencia upstream está degradada.

Siguientes pasos

Last updated on