Manejo de Errores

Guía completa para manejar errores de la API

Manejo de Errores

Aprende a manejar errores de forma elegante en tu integración con Koywe Payments.

Formato de Respuesta de Error

Todos los errores de la API siguen un formato consistente:

1{
2 "statusCode": 400,
3 "timestamp": "2024-01-01T00:00:00.000Z",
4 "path": "/api/endpoint",
5 "errorCode": "INSUFFICIENT_BALANCE",
6 "message": "Insufficient balance for payout",
7 "details": {
8 "available": 100000,
9 "required": 500000,
10 "currency": "COP"
11 }
12}

Campos:

  • statusCode: Código de estado HTTP
  • timestamp: Cuándo ocurrió el error (ISO 8601)
  • path: Ruta del endpoint de la API
  • errorCode: Código de error legible por máquina
  • message: Descripción del error legible por humano
  • details: Contexto adicional (opcional)

Códigos de Estado HTTP

EstadoSignificadoAcción
200ÉxitoContinuar
400Solicitud IncorrectaCorregir parámetros de solicitud
401No AutorizadoVerificar credenciales/token
403ProhibidoVerificar permisos
404No EncontradoVerificar ID del recurso
409ConflictoProblema de duplicado/idempotencia
422Error de ValidaciónCorregir validación de entrada
429Límite de TasaEsperar y reintentar
500Error del ServidorReintentar con backoff
503Servicio No DisponibleReintentar más tarde

Códigos de Error Comunes

Errores de Autenticación

INVALID_CREDENTIALS

1{
2 "statusCode": 401,
3 "timestamp": "2024-01-01T00:00:00.000Z",
4 "path": "/api/v1/auth",
5 "errorCode": "INVALID_CREDENTIALS",
6 "message": "Invalid API key or secret"
7}

Causa: API key o secret incorrectos
Solución: Verificar credenciales en variables de entorno

Corrección
1// Verificar que las credenciales estén configuradas correctamente
2console.log('API Key existe:', !!process.env.KOYWE_API_KEY);
3console.log('Secret existe:', !!process.env.KOYWE_SECRET);
4
5// Probar autenticación
6try {
7 const token = await authenticate();
8 console.log('✓ Autenticación exitosa');
9} catch (error) {
10 console.error('✗ Autenticación falló');
11 console.error('Error:', error.response?.data);
12}

TOKEN_EXPIRED

1{
2 "statusCode": 401,
3 "timestamp": "2024-01-01T00:00:00.000Z",
4 "path": "/api/v1/orders",
5 "errorCode": "TOKEN_EXPIRED",
6 "message": "Authentication token has expired"
7}

Causa: Token mayor a 1 hora
Solución: Implementar renovación de token

Renovación de Token
1async function requestWithTokenRefresh(fn) {
2 try {
3 return await fn();
4 } catch (error) {
5 if (error.response?.status === 401) {
6 // Token expirado, renovar y reintentar
7 token = await authenticate();
8 return await fn();
9 }
10 throw error;
11 }
12}
13
14// Uso
15const order = await requestWithTokenRefresh(() =>
16 createPayinOrder(token, orgId, merchantId, orderData)
17);

Errores de Validación

INVALID_AMOUNT

1{
2 "statusCode": 422,
3 "timestamp": "2024-01-01T00:00:00.000Z",
4 "path": "/api/v1/orders",
5 "errorCode": "INVALID_AMOUNT",
6 "message": "Amount must be greater than minimum",
7 "details": {
8 "minimum": 1000,
9 "provided": 500,
10 "currency": "COP"
11 }
12}

Solución: Validar montos antes de llamadas a la API

Validación de Monto
1const MINIMUMS = {
2 'COP': 1000,
3 'BRL': 1,
4 'MXN': 1,
5 'CLP': 100,
6 'USD': 0.01
7};
8
9function validateAmount(amount, currency) {
10 const min = MINIMUMS[currency] || 1;
11
12 if (amount < min) {
13 throw new Error(`Monto mínimo: ${min} ${currency}`);
14 }
15
16 if (amount <= 0) {
17 throw new Error('El monto debe ser positivo');
18 }
19
20 return true;
21}

INVALID_CURRENCY

1{
2 "statusCode": 422,
3 "timestamp": "2024-01-01T00:00:00.000Z",
4 "path": "/api/v1/orders",
5 "errorCode": "INVALID_CURRENCY",
6 "message": "Currency not supported for country",
7 "details": {
8 "country": "CO",
9 "provided": "USD",
10 "supported": ["COP"]
11 }
12}

Solución: Usar la moneda correcta para el país

Validación de Moneda
1const COUNTRY_CURRENCIES = {
2 'CO': ['COP'],
3 'BR': ['BRL'],
4 'MX': ['MXN'],
5 'CL': ['CLP']
6};
7
8function validateCurrency(country, currency) {
9 const valid = COUNTRY_CURRENCIES[country];
10 if (!valid || !valid.includes(currency)) {
11 throw new Error(`Usa ${valid.join(', ')} para ${country}`);
12 }
13}

Errores de Creación de Órdenes

INSUFFICIENT_BALANCE

1{
2 "statusCode": 400,
3 "timestamp": "2024-01-01T00:00:00.000Z",
4 "path": "/api/v1/orders",
5 "errorCode": "INSUFFICIENT_BALANCE",
6 "message": "Insufficient balance for payout",
7 "details": {
8 "available": 100000,
9 "required": 500000,
10 "currency": "COP"
11 }
12}

Causa: Fondos insuficientes en cuenta virtual
Solución: Verificar saldo antes de crear PAYOUT

Verificación de Saldo
1async function safeCreatePayout(payoutData) {
2 // Verificar saldo primero
3 const balance = await getBalance(token, orgId, merchantId, payoutData.currency);
4
5 if (balance.availableBalance < payoutData.amount) {
6 throw new Error(
7 `Saldo insuficiente: ${balance.availableBalance} ${payoutData.currency} disponible, ` +
8 `${payoutData.amount} ${payoutData.currency} requerido`
9 );
10 }
11
12 // Crear payout
13 return await createPayoutOrder(token, orgId, merchantId, payoutData);
14}

PAYMENT_METHOD_NOT_SUPPORTED

1{
2 "statusCode": 400,
3 "timestamp": "2024-01-01T00:00:00.000Z",
4 "path": "/api/v1/orders",
5 "errorCode": "PAYMENT_METHOD_NOT_SUPPORTED",
6 "message": "Payment method not available for country/currency"
7}

Solución: Consultar métodos disponibles primero

Validación de Método
1async function validatePaymentMethod(country, currency, method) {
2 const methods = await getPaymentMethods(country, currency);
3 const available = methods.find(m => m.method === method);
4
5 if (!available) {
6 throw new Error(
7 `Método ${method} no disponible. Usa: ${methods.map(m => m.method).join(', ')}`
8 );
9 }
10
11 return true;
12}

Errores de Contacto/Cuenta Bancaria

INVALID_DOCUMENT

1{
2 "statusCode": 422,
3 "timestamp": "2024-01-01T00:00:00.000Z",
4 "path": "/api/v1/contacts",
5 "errorCode": "INVALID_DOCUMENT",
6 "message": "Invalid document number format"
7}

Solución: Validar formato de documento por país

Validación de Documento
1const DOCUMENT_REGEX = {
2 'CO': {
3 'CC': /^\d{6,10}$/,
4 'NIT': /^\d{9,10}$/
5 },
6 'BR': {
7 'CPF': /^\d{11}$/,
8 'CNPJ': /^\d{14}$/
9 }
10};
11
12function validateDocument(country, type, number) {
13 const regex = DOCUMENT_REGEX[country]?.[type];
14 if (regex && !regex.test(number)) {
15 throw new Error(`Formato de ${type} inválido para ${country}`);
16 }
17}

Estrategias de Manejo de Errores

1. Lógica de Reintento

Implementar backoff exponencial para errores transitorios:

Reintento con Backoff
1async function retryWithBackoff(fn, maxRetries = 3) {
2 for (let i = 0; i < maxRetries; i++) {
3 try {
4 return await fn();
5 } catch (error) {
6 const isRetryable =
7 error.response?.status >= 500 || // Errores de servidor
8 error.response?.status === 429 || // Límite de tasa
9 error.code === 'ECONNABORTED'; // Timeout
10
11 const isLastAttempt = i === maxRetries - 1;
12
13 if (!isRetryable || isLastAttempt) {
14 throw error;
15 }
16
17 // Backoff exponencial: 1s, 2s, 4s
18 const delay = Math.pow(2, i) * 1000;
19 console.log(`Reintento ${i + 1}/${maxRetries} después de ${delay}ms...`);
20 await new Promise(resolve => setTimeout(resolve, delay));
21 }
22 }
23}
24
25// Uso
26const order = await retryWithBackoff(() =>
27 createPayinOrder(token, orgId, merchantId, orderData)
28);

2. Degradación Elegante

Manejo con Fallback
1async function createOrderWithFallback(orderData) {
2 try {
3 // Intentar método de pago primario
4 return await createPayinOrder(token, orgId, merchantId, {
5 ...orderData,
6 paymentMethods: [{ method: 'PSE', extra: 'BANCOLOMBIA' }]
7 });
8 } catch (error) {
9 if (error.response?.data?.errorCode === 'PAYMENT_METHOD_NOT_SUPPORTED') {
10 // Fallback a método alternativo
11 console.log('Cambiando a método de pago alternativo...');
12 return await createPayinOrder(token, orgId, merchantId, {
13 ...orderData,
14 paymentMethods: [{ method: 'NEQUI' }]
15 });
16 }
17 throw error;
18 }
19}

3. Mensajes de Error Amigables al Usuario

Traducción de Errores
1function translateErrorToUser(error) {
2 const errorMap = {
3 'INSUFFICIENT_BALANCE': 'No tenemos fondos suficientes para procesar este payout. Por favor intenta más tarde.',
4 'INVALID_AMOUNT': 'El monto del pago es inválido. Por favor verifica e intenta de nuevo.',
5 'PAYMENT_METHOD_NOT_SUPPORTED': 'Este método de pago no está disponible. Por favor elige otro.',
6 'INVALID_DOCUMENT': 'Tu número de documento parece ser inválido. Por favor verifica e intenta de nuevo.'
7 };
8
9 const errorCode = error.response?.data?.errorCode;
10 return errorMap[errorCode] || 'Ocurrió un error inesperado. Por favor intenta de nuevo o contacta soporte.';
11}
12
13// Uso
14try {
15 await createPayinOrder(token, orgId, merchantId, orderData);
16} catch (error) {
17 const userMessage = translateErrorToUser(error);
18 showErrorToUser(userMessage);
19
20 // Registrar detalles técnicos para debugging
21 console.error('Error técnico:', error.response?.data);
22}

4. Registro y Monitoreo

Registro de Errores
1function logError(error, context) {
2 const errorLog = {
3 timestamp: new Date().toISOString(),
4 context: context,
5 status: error.response?.status,
6 statusCode: error.response?.data?.statusCode,
7 errorCode: error.response?.data?.errorCode,
8 message: error.response?.data?.message,
9 path: error.response?.data?.path,
10 stack: error.stack
11 };
12
13 // Enviar a servicio de logging (ej: Sentry, CloudWatch)
14 console.error('Error ocurrido:', JSON.stringify(errorLog, null, 2));
15
16 // Alertar para errores críticos
17 if (error.response?.status >= 500) {
18 alertOpsTeam(errorLog);
19 }
20}
21
22// Uso
23try {
24 await createPayoutOrder(token, orgId, merchantId, payoutData);
25} catch (error) {
26 logError(error, { operation: 'create_payout', payoutData });
27 throw error;
28}

Manejador de Errores de Producción

Manejador completo listo para producción:

Manejador Completo
1class KoyweErrorHandler {
2 constructor(options = {}) {
3 this.maxRetries = options.maxRetries || 3;
4 this.logger = options.logger || console;
5 }
6
7 async execute(fn, context = {}) {
8 for (let attempt = 0; attempt < this.maxRetries; attempt++) {
9 try {
10 return await fn();
11 } catch (error) {
12 const shouldRetry = this.shouldRetry(error, attempt);
13
14 // Registrar error
15 this.logError(error, { ...context, attempt });
16
17 if (shouldRetry) {
18 const delay = this.getRetryDelay(attempt);
19 this.logger.info(`Reintentando en ${delay}ms...`);
20 await this.sleep(delay);
21 continue;
22 }
23
24 // No es reintentable o se alcanzó el máximo de reintentos
25 throw this.formatError(error);
26 }
27 }
28 }
29
30 shouldRetry(error, attempt) {
31 if (attempt >= this.maxRetries - 1) return false;
32
33 const status = error.response?.status;
34 const errorCode = error.response?.data?.errorCode;
35
36 // Reintentar en errores de servidor, límites de tasa, timeouts
37 return (
38 status >= 500 ||
39 status === 429 ||
40 errorCode === 'TIMEOUT' ||
41 error.code === 'ECONNABORTED'
42 );
43 }
44
45 getRetryDelay(attempt) {
46 // Backoff exponencial con jitter
47 const base = Math.pow(2, attempt) * 1000;
48 const jitter = Math.random() * 1000;
49 return base + jitter;
50 }
51
52 logError(error, context) {
53 const log = {
54 timestamp: new Date().toISOString(),
55 ...context,
56 error: {
57 statusCode: error.response?.data?.statusCode,
58 errorCode: error.response?.data?.errorCode,
59 message: error.response?.data?.message,
60 path: error.response?.data?.path
61 }
62 };
63
64 this.logger.error('Error de API de Koywe:', log);
65 }
66
67 formatError(error) {
68 const apiError = error.response?.data;
69
70 if (apiError) {
71 const err = new Error(apiError.message);
72 err.statusCode = apiError.statusCode;
73 err.errorCode = apiError.errorCode;
74 err.details = apiError.details;
75 err.path = apiError.path;
76 return err;
77 }
78
79 return error;
80 }
81
82 sleep(ms) {
83 return new Promise(resolve => setTimeout(resolve, ms));
84 }
85}
86
87// Uso
88const errorHandler = new KoyweErrorHandler({ maxRetries: 3 });
89
90const order = await errorHandler.execute(
91 () => createPayinOrder(token, orgId, merchantId, orderData),
92 { operation: 'create_payin', merchantId }
93);

Próximos Pasos