Error Handling
Comprehensive guide to handling API errors
Error Handling
Learn how to handle errors gracefully in your Koywe Payments integration.
Error Response Format
All API errors follow a consistent format:
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 }
Fields:
statusCode: HTTP status codetimestamp: When the error occurred (ISO 8601)path: API endpoint patherrorCode: Machine-readable error codemessage: Human-readable error descriptiondetails: Additional context (optional)
HTTP Status Codes
| Status | Meaning | Action |
|---|---|---|
200 | Success | Continue |
400 | Bad Request | Fix request parameters |
401 | Unauthorized | Check credentials/token |
403 | Forbidden | Check permissions |
404 | Not Found | Verify resource ID |
409 | Conflict | Duplicate/idempotency issue |
422 | Validation Error | Fix input validation |
429 | Rate Limit | Wait and retry |
500 | Server Error | Retry with backoff |
503 | Service Unavailable | Retry later |
Common Error Codes
Authentication Errors
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 }
Cause: Wrong API key or secret
Solution: Verify credentials in environment variables
Fix
1 // Verify credentials are set correctly 2 console.log('API Key exists:', !!process.env.KOYWE_API_KEY); 3 console.log('Secret exists:', !!process.env.KOYWE_SECRET); 4 5 // Test authentication 6 try { 7 const token = await authenticate(); 8 console.log('โ Authentication successful'); 9 } catch (error) { 10 console.error('โ Authentication failed'); 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 }
Cause: Token older than 1 hour
Solution: Implement token refresh
Token Refresh
1 async function requestWithTokenRefresh(fn) { 2 try { 3 return await fn(); 4 } catch (error) { 5 if (error.response?.status === 401) { 6 // Token expired, refresh and retry 7 token = await authenticate(); 8 return await fn(); 9 } 10 throw error; 11 } 12 } 13 14 // Usage 15 const order = await requestWithTokenRefresh(() => 16 createPayinOrder(token, orgId, merchantId, orderData) 17 );
Validation Errors
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 }
Solution: Validate amounts before API calls
Amount Validation
1 const MINIMUMS = { 2 'COP': 1000, 3 'BRL': 1, 4 'MXN': 1, 5 'CLP': 100, 6 'USD': 0.01 7 }; 8 9 function validateAmount(amount, currency) { 10 const min = MINIMUMS[currency] || 1; 11 12 if (amount < min) { 13 throw new Error(`Minimum amount: ${min} ${currency}`); 14 } 15 16 if (amount <= 0) { 17 throw new Error('Amount must be positive'); 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 }
Solution: Use correct currency for country
Currency Validation
1 const COUNTRY_CURRENCIES = { 2 'CO': ['COP'], 3 'BR': ['BRL'], 4 'MX': ['MXN'], 5 'CL': ['CLP'] 6 }; 7 8 function validateCurrency(country, currency) { 9 const valid = COUNTRY_CURRENCIES[country]; 10 if (!valid || !valid.includes(currency)) { 11 throw new Error(`Use ${valid.join(', ')} for ${country}`); 12 } 13 }
Order Creation Errors
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 }
Cause: Not enough funds in virtual account
Solution: Check balance before creating PAYOUT
Balance Check
1 async function safeCreatePayout(payoutData) { 2 // Check balance first 3 const balance = await getBalance(token, orgId, merchantId, payoutData.currency); 4 5 if (balance.availableBalance < payoutData.amount) { 6 throw new Error( 7 `Insufficient balance: ${balance.availableBalance} ${payoutData.currency} available, ` + 8 `${payoutData.amount} ${payoutData.currency} required` 9 ); 10 } 11 12 // Create 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 }
Solution: Query available methods first
Method Validation
1 async 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 `Method ${method} not available. Use: ${methods.map(m => m.method).join(', ')}` 8 ); 9 } 10 11 return true; 12 }
Contact/Bank Account Errors
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 }
Solution: Validate document format per country
Document Validation
1 const 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 12 function validateDocument(country, type, number) { 13 const regex = DOCUMENT_REGEX[country]?.[type]; 14 if (regex && !regex.test(number)) { 15 throw new Error(`Invalid ${type} format for ${country}`); 16 } 17 }
Error Handling Strategies
1. Retry Logic
Implement exponential backoff for transient errors:
Retry with Backoff
1 async 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 || // Server errors 8 error.response?.status === 429 || // Rate limit 9 error.code === 'ECONNABORTED'; // Timeout 10 11 const isLastAttempt = i === maxRetries - 1; 12 13 if (!isRetryable || isLastAttempt) { 14 throw error; 15 } 16 17 // Exponential backoff: 1s, 2s, 4s 18 const delay = Math.pow(2, i) * 1000; 19 console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`); 20 await new Promise(resolve => setTimeout(resolve, delay)); 21 } 22 } 23 } 24 25 // Usage 26 const order = await retryWithBackoff(() => 27 createPayinOrder(token, orgId, merchantId, orderData) 28 );
2. Graceful Degradation
Fallback Handling
1 async function createOrderWithFallback(orderData) { 2 try { 3 // Try primary payment method 4 return await createPayinOrder(token, orgId, merchantId, { 5 ...orderData, 6 paymentMethods: [{ method: 'PSE', extra: 'BANCOLOMBIA' }] 7 }); 8 } catch (error) { 9 if (error.response?.data?.code === 'PAYMENT_METHOD_NOT_SUPPORTED') { 10 // Fallback to alternative method 11 console.log('Falling back to alternative payment method...'); 12 return await createPayinOrder(token, orgId, merchantId, { 13 ...orderData, 14 paymentMethods: [{ method: 'NEQUI' }] 15 }); 16 } 17 throw error; 18 } 19 }
3. User-Friendly Error Messages
Error Translation
1 function translateErrorToUser(error) { 2 const errorMap = { 3 'INSUFFICIENT_BALANCE': 'We don\'t have enough funds to process this payout. Please try again later.', 4 'INVALID_AMOUNT': 'The payment amount is invalid. Please check and try again.', 5 'PAYMENT_METHOD_NOT_SUPPORTED': 'This payment method is not available. Please choose another.', 6 'INVALID_DOCUMENT': 'Your document number appears to be invalid. Please verify and try again.' 7 }; 8 9 const errorCode = error.response?.data?.errorCode; 10 return errorMap[errorCode] || 'An unexpected error occurred. Please try again or contact support.'; 11 } 12 13 // Usage 14 try { 15 await createPayinOrder(token, orgId, merchantId, orderData); 16 } catch (error) { 17 const userMessage = translateErrorToUser(error); 18 showErrorToUser(userMessage); 19 20 // Log technical details for debugging 21 console.error('Technical error:', error.response?.data); 22 }
4. Logging and Monitoring
Error Logging
1 function 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 // Send to logging service (e.g., Sentry, CloudWatch) 14 console.error('Error occurred:', JSON.stringify(errorLog, null, 2)); 15 16 // Alert for critical errors 17 if (error.response?.status >= 500) { 18 alertOpsTeam(errorLog); 19 } 20 } 21 22 // Usage 23 try { 24 await createPayoutOrder(token, orgId, merchantId, payoutData); 25 } catch (error) { 26 logError(error, { operation: 'create_payout', payoutData }); 27 throw error; 28 }
Production Error Handler
Complete production-ready error handler:
Complete Handler
1 class 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 // Log error 15 this.logError(error, { ...context, attempt }); 16 17 if (shouldRetry) { 18 const delay = this.getRetryDelay(attempt); 19 this.logger.info(`Retrying in ${delay}ms...`); 20 await this.sleep(delay); 21 continue; 22 } 23 24 // Not retryable or max retries reached 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 // Retry on server errors, rate limits, timeouts 37 return ( 38 status >= 500 || 39 status === 429 || 40 errorCode === 'TIMEOUT' || 41 error.code === 'ECONNABORTED' 42 ); 43 } 44 45 getRetryDelay(attempt) { 46 // Exponential backoff with 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('Koywe API error:', 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 // Usage 88 const errorHandler = new KoyweErrorHandler({ maxRetries: 3 }); 89 90 const order = await errorHandler.execute( 91 () => createPayinOrder(token, orgId, merchantId, orderData), 92 { operation: 'create_payin', merchantId } 93 );