Troubleshooting Accepting Payments
Solutions to common issues when integrating payment acceptance.
Authentication Issues
401 Unauthorized Error
Problem: API returns 401 Unauthorized
Causes:
- Invalid API key or secret
- Expired token
- Token not included in request
Solutions:
// Verify credentials are correct
console.log('API Key:', process.env.KOYWE_API_KEY);
console.log('Secret:', process.env.KOYWE_SECRET ? '***' : 'MISSING');
// Test authentication
try {
const token = await authenticate();
console.log('✓ Authentication successful');
} catch (error) {
console.error('✗ Authentication failed:', error.response?.data);
}// Implement token caching with refresh
class KoyweClient {
constructor(apiKey, secret) {
this.apiKey = apiKey;
this.secret = secret;
this.token = null;
this.tokenExpiry = null;
}
async getToken() {
// Return cached token if still valid (with 5-minute buffer)
if (this.token && this.tokenExpiry > Date.now() + (5 * 60 * 1000)) {
return this.token;
}
// Get new token
const response = await axios.post(
'https://api-sandbox.koywe.com/api/v1/auth/sign-in',
{ apiKey: this.apiKey, secret: this.secret }
);
this.token = response.data.token;
this.tokenExpiry = Date.now() + (60 * 60 * 1000); // 1 hour
return this.token;
}
async request(method, url, data) {
const token = await this.getToken();
try {
return await axios({
method,
url,
data,
headers: { 'Authorization': `Bearer ${token}` }
});
} catch (error) {
// If 401, token might have expired, retry once
if (error.response?.status === 401) {
this.token = null; // Clear token
const newToken = await this.getToken();
return await axios({
method,
url,
data,
headers: { 'Authorization': `Bearer ${newToken}` }
});
}
throw error;
}
}
}Order Creation Issues
Payment Method Not Supported
Problem: Error “Payment method not supported for country/currency”
Cause: Using a payment method that isn’t available for the target country or currency
Solution:
async function createOrderSafely(country, currency, amount, preferredMethod) {
// 1. Get available methods
const methods = await getPaymentMethods(country, currency);
console.log('Available methods:', methods.map(m => m.method));
// 2. Check if preferred method is available
const methodAvailable = methods.some(m => m.method === preferredMethod);
if (!methodAvailable) {
console.error(`Method ${preferredMethod} not available`);
console.log('Use one of:', methods.map(m => m.method));
throw new Error('Payment method not supported');
}
// 3. Create order
return await createPayinOrder(token, orgId, merchantId, {
originCurrencySymbol: currency,
destinationCurrencySymbol: currency,
amountIn: amount,
paymentMethods: [{ method: preferredMethod }]
// ... other fields
});
}Common mistakes:
- Using
SPEIfor Colombia (usePSEinstead) - Using
PSEfor Brazil (usePIXinstead) - Wrong currency for payment method
Invalid Document Number
Problem: Error “Invalid document number format”
Cause: Document number doesn’t match expected format for the document type
Solution:
function validateDocument(country, documentType, documentNumber) {
const validators = {
'CO': {
'CC': /^\d{6,10}$/, // Colombian ID: 6-10 digits
'CE': /^\d{6,7}$/, // Foreign ID: 6-7 digits
'NIT': /^\d{9,10}$/ // Tax ID: 9-10 digits
},
'BR': {
'CPF': /^\d{11}$/, // Individual: 11 digits
'CNPJ': /^\d{14}$/ // Company: 14 digits
},
'MX': {
'RFC': /^[A-Z]{3,4}\d{6}[A-Z0-9]{3}$/ // Tax ID format
},
'CL': {
'RUT': /^\d{7,8}-[\dkK]$/ // Format: 12345678-9
}
};
const regex = validators[country]?.[documentType];
if (!regex) {
console.warn(`No validator for ${country} ${documentType}`);
return true; // Allow if no validator
}
const isValid = regex.test(documentNumber);
if (!isValid) {
console.error(`Invalid ${documentType} format: ${documentNumber}`);
console.log(`Expected format: ${regex}`);
}
return isValid;
}
// Usage
const isValid = validateDocument('CO', 'CC', '1234567890');
if (!isValid) {
throw new Error('Invalid document number');
}Duplicate Order / Idempotency Issues
Problem: Creating duplicate orders or getting “Order already exists” error
Solution: Use externalId for idempotency
async function createOrderIdempotent(internalOrderId, orderData) {
// Use your internal order ID as externalId
const externalId = `order-${internalOrderId}`;
try {
const order = await createPayinOrder(token, orgId, merchantId, {
...orderData,
externalId: externalId // Same externalId = same order
});
console.log('Order created:', order.id);
return order;
} catch (error) {
if (error.response?.status === 409) {
// Order already exists, retrieve it
console.log('Order already exists, retrieving...');
const existingOrder = await getOrderByExternalId(externalId);
return existingOrder;
}
throw error;
}
}
// Safe to retry
const order = await createOrderIdempotent('12345', orderData);Payment Flow Issues
Order Stuck in PENDING
Problem: Order stays in PENDING status and never completes
Causes in Production:
- Customer hasn’t completed payment
- Payment URL expired
- Payment provider issues
Causes in Sandbox:
- Using failure test amount (666)
- Network issues
Solutions:
async function checkOrderStatus(orderId) {
const order = await getOrderStatus(token, orgId, merchantId, orderId);
console.log('Status:', order.status);
console.log('Created:', order.createdAt);
console.log('Due date:', order.dueDate);
if (order.status === 'PENDING') {
const createdTime = new Date(order.createdAt);
const now = new Date();
const minutesElapsed = (now - createdTime) / 1000 / 60;
console.log(`Order pending for ${minutesElapsed.toFixed(0)} minutes`);
if (minutesElapsed > 30) {
console.warn('Order pending for too long - customer may not have paid');
// Consider cancelling or expiring the order
}
}
return order;
}In Sandbox:
- Orders complete automatically after 5-30 seconds
- If using amount 666, order will fail (this is intentional for testing)
Payment Completed but No Webhook Received
Problem: Order shows COMPLETED in API but webhook wasn’t received
Causes:
- Webhook endpoint not configured
- Webhook endpoint unreachable
- Webhook endpoint returning errors
- Firewall blocking webhooks
Solutions:
Verify webhook endpoint
Check that your endpoint is publicly accessible
Test with webhook.site
Use https://webhook.site to see if webhooks are being sent
Check webhook logs
Query webhook delivery attempts:
const deliveries = await getWebhookDeliveries(orderId);
console.log(deliveries);Verify signature validation
Make sure you’re not rejecting webhooks due to failed signature verification
Implement fallback polling
As a backup, poll order status:
async function waitForCompletion(orderId, maxAttempts = 20) {
for (let i = 0; i < maxAttempts; i++) {
const order = await getOrderStatus(token, orgId, merchantId, orderId);
if (order.status === 'COMPLETED') {
return order;
}
if (['FAILED', 'EXPIRED', 'CANCELLED'].includes(order.status)) {
throw new Error(`Order ${order.status}`);
}
// Wait 3 seconds before next check
await new Promise(resolve => setTimeout(resolve, 3000));
}
throw new Error('Timeout waiting for order completion');
}Invalid Webhook Signature
Problem: Webhook signature verification fails
Cause: Incorrect signature calculation or wrong secret
Solution:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
// payload should be the raw body (string or Buffer)
// NOT parsed JSON
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const isValid = signature === expectedSignature;
if (!isValid) {
console.error('Signature mismatch');
console.error('Received:', signature);
console.error('Expected:', expectedSignature);
}
return isValid;
}
// Express example - MUST use raw body
app.post('/webhooks/koywe',
express.raw({ type: 'application/json' }), // Important: raw, not json
(req, res) => {
const signature = req.headers['koywe-signature'];
const secret = process.env.KOYWE_WEBHOOK_SECRET;
// req.body is Buffer when using express.raw
if (!verifyWebhookSignature(req.body, signature, secret)) {
return res.status(401).send('Invalid signature');
}
// Now parse
const event = JSON.parse(req.body);
// ... handle event
res.status(200).send('OK');
}
);Amount and Currency Issues
Amount Validation Errors
Problem: “Invalid amount” or “Amount too low/high”
Causes:
- Amount is 0 or negative
- Amount exceeds limits
- Decimal amounts where integers expected
Solutions:
function validateAmount(amount, currency) {
// Minimum amounts per currency
const minimums = {
'COP': 1000, // 1,000 COP
'BRL': 1, // 1 BRL
'MXN': 1, // 1 MXN
'CLP': 100, // 100 CLP
'USD': 0.01 // $0.01 USD
};
// Maximum amounts per currency
const maximums = {
'COP': 50000000, // 50M COP
'BRL': 100000, // 100K BRL
'MXN': 100000, // 100K MXN
'CLP': 10000000, // 10M CLP
'USD': 50000 // 50K USD
};
const min = minimums[currency] || 1;
const max = maximums[currency] || 1000000;
if (amount < min) {
throw new Error(`Amount too low. Minimum: ${min} ${currency}`);
}
if (amount > max) {
throw new Error(`Amount too high. Maximum: ${max} ${currency}`);
}
// Check for invalid decimals (COP, CLP don't use decimals)
if (['COP', 'CLP'].includes(currency) && amount % 1 !== 0) {
throw new Error(`${currency} does not support decimal amounts`);
}
return true;
}
// Usage
try {
validateAmount(50000, 'COP'); // OK
validateAmount(50.5, 'COP'); // Error: no decimals
validateAmount(0, 'COP'); // Error: too low
} catch (error) {
console.error(error.message);
}Currency Mismatch
Problem: “Currency not supported” or mismatch errors
Cause: Using wrong currency for country or payment method
Solution:
const COUNTRY_CURRENCIES = {
'CO': ['COP'], // Colombia: only COP
'BR': ['BRL'], // Brazil: only BRL
'MX': ['MXN'], // Mexico: only MXN
'CL': ['CLP'], // Chile: only CLP
'AR': ['ARS'], // Argentina: only ARS
'PE': ['PEN'] // Peru: only PEN
};
function validateCurrency(country, currency) {
const validCurrencies = COUNTRY_CURRENCIES[country];
if (!validCurrencies) {
throw new Error(`Country ${country} not supported`);
}
if (!validCurrencies.includes(currency)) {
throw new Error(
`Currency ${currency} not valid for ${country}. ` +
`Use: ${validCurrencies.join(', ')}`
);
}
return true;
}
// Usage
validateCurrency('CO', 'COP'); // OK
validateCurrency('CO', 'USD'); // ErrorNetwork and Timeout Issues
Request Timeout
Problem: API requests timeout
Causes:
- Slow network
- API under heavy load
- Missing timeout configuration
Solutions:
const axios = require('axios');
// Create axios instance with proper timeouts
const koyweApi = axios.create({
baseURL: 'https://api-sandbox.koywe.com/api/v1',
timeout: 30000, // 30 seconds
headers: {
'Content-Type': 'application/json'
}
});
// Add retry logic
async function requestWithRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
const isTimeout = error.code === 'ECONNABORTED';
const isNetworkError = error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED';
const isServerError = error.response?.status >= 500;
const shouldRetry = (isTimeout || isNetworkError || isServerError) && i < maxRetries - 1;
if (shouldRetry) {
const delay = Math.pow(2, i) * 1000; // Exponential backoff
console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
}
// Usage
const order = await requestWithRetry(() =>
createPayinOrder(token, orgId, merchantId, orderData)
);Contact Support
If you’ve tried these solutions and still have issues:
Please include:
- Order ID or External ID
- Error message
- Request/response examples (remove sensitive data)
- Steps to reproduce
- Environment (sandbox/production)
Diagnostic Checklist
Use this checklist when troubleshooting:
- API credentials are correct
- Using correct base URL (sandbox vs production)
- Token is valid and not expired
- Payment method is supported for country/currency
- Amount meets minimum/maximum requirements
- Document number format is correct
- Currency matches country
- Webhook endpoint is publicly accessible
- Webhook signature verification is correct
- Network connectivity is stable
- Proper error handling is implemented
- Timeout values are appropriate