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 code
  • timestamp: When the error occurred (ISO 8601)
  • path: API endpoint path
  • errorCode: Machine-readable error code
  • message: Human-readable error description
  • details: Additional context (optional)

HTTP Status Codes

StatusMeaningAction
200SuccessContinue
400Bad RequestFix request parameters
401UnauthorizedCheck credentials/token
403ForbiddenCheck permissions
404Not FoundVerify resource ID
409ConflictDuplicate/idempotency issue
422Validation ErrorFix input validation
429Rate LimitWait and retry
500Server ErrorRetry with backoff
503Service UnavailableRetry 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
2console.log('API Key exists:', !!process.env.KOYWE_API_KEY);
3console.log('Secret exists:', !!process.env.KOYWE_SECRET);
4
5// Test authentication
6try {
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
1async 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
15const 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
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(`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
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(`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
1async 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
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 `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
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(`Invalid ${type} format for ${country}`);
16 }
17}

Error Handling Strategies

1. Retry Logic

Implement exponential backoff for transient errors:

Retry with 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 || // 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
26const order = await retryWithBackoff(() =>
27 createPayinOrder(token, orgId, merchantId, orderData)
28);

2. Graceful Degradation

Fallback Handling
1async 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
1function 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
14try {
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
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 // 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
23try {
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
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 // 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
88const errorHandler = new KoyweErrorHandler({ maxRetries: 3 });
89
90const order = await errorHandler.execute(
91 () => createPayinOrder(token, orgId, merchantId, orderData),
92 { operation: 'create_payin', merchantId }
93);

Next Steps