Koywe

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:

Check credentials

JavaScript
// 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);
}

Token refresh

JavaScript
// 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:

JavaScript
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 SPEI for Colombia (use PSE instead)
  • Using PSE for Brazil (use PIX instead)
  • 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:

JavaScript
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

JavaScript
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:

JavaScript
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 to see if webhooks are being sent

Check webhook logs

Query webhook delivery attempts:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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');  // Error

Network and Timeout Issues

Request Timeout

Problem: API requests timeout

Causes:

  • Slow network
  • API under heavy load
  • Missing timeout configuration

Solutions:

JavaScript
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:

Email Support

soporte@koywe.com

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

Next Steps