Koywe
En esta página

Solución de Problemas para Aceptar Pagos

Soluciones a problemas comunes al integrar la aceptación de pagos.

Problemas de Autenticación

Error 401 Unauthorized

Problema: La API devuelve 401 Unauthorized

Causas:

  • API key o secret inválido
  • Token expirado
  • Token no incluido en la solicitud

Soluciones:

Verificar credenciales

JavaScript
// Verificar que las credenciales son correctas
console.log('API Key:', process.env.KOYWE_API_KEY);
console.log('Secret:', process.env.KOYWE_SECRET ? '***' : 'FALTANTE');
 
// Probar autenticación
try {
  const token = await authenticate();
  console.log('✓ Autenticación exitosa');
} catch (error) {
  console.error('✗ Autenticación fallida:', error.response?.data);
}

Actualización de token

JavaScript
// Implementar caché de token con actualización
class KoyweClient {
  constructor(apiKey, secret) {
    this.apiKey = apiKey;
    this.secret = secret;
    this.token = null;
    this.tokenExpiry = null;
  }
  
  async getToken() {
    // Devolver token en caché si aún es válido (con buffer de 5 minutos)
    if (this.token && this.tokenExpiry > Date.now() + (5 * 60 * 1000)) {
      return this.token;
    }
    
    // Obtener nuevo 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 hora
    
    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) {
      // Si 401, token pudo haber expirado, reintentar una vez
      if (error.response?.status === 401) {
        this.token = null; // Limpiar token
        const newToken = await this.getToken();
        return await axios({
          method,
          url,
          data,
          headers: { 'Authorization': `Bearer ${newToken}` }
        });
      }
      throw error;
    }
  }
}

Problemas de Creación de Orden

Método de Pago No Soportado

Problema: Error “Método de pago no soportado para país/moneda”

Causa: Usar un método de pago que no está disponible para el país o moneda objetivo

Solución:

JavaScript
async function createOrderSafely(country, currency, amount, preferredMethod) {
  // 1. Obtener métodos disponibles
  const methods = await getPaymentMethods(country, currency);
  console.log('Métodos disponibles:', methods.map(m => m.method));
  
  // 2. Verificar si el método preferido está disponible
  const methodAvailable = methods.some(m => m.method === preferredMethod);
  
  if (!methodAvailable) {
    console.error(`Método ${preferredMethod} no disponible`);
    console.log('Usar uno de:', methods.map(m => m.method));
    throw new Error('Método de pago no soportado');
  }
  
  // 3. Crear orden
  return await createPayinOrder(token, orgId, merchantId, {
    originCurrencySymbol: currency,
    destinationCurrencySymbol: currency,
    amountIn: amount,
    paymentMethods: [{ method: preferredMethod }]
    // ... otros campos
  });
}

Errores comunes:

  • Usar SPEI para Colombia (usar PSE en su lugar)
  • Usar PSE para Brasil (usar PIX en su lugar)
  • Moneda incorrecta para método de pago

Número de Documento Inválido

Problema: Error “Formato de número de documento inválido”

Causa: El número de documento no coincide con el formato esperado para el tipo de documento

Solución:

JavaScript
function validateDocument(country, documentType, documentNumber) {
  const validators = {
    'CO': {
      'CED_CIU': /^\d{1,15}$/,  // Cédula de Ciudadanía (CC): solo dígitos
      'CED_EXT': /^\d{1,15}$/,  // Cédula de Extranjería (CE): solo dígitos
      'NIT': /^(?:\d{3}\.\d{3}\.\d{3}-\d|\d{10})$/  // NIT: 900.123.456-1 o 9001234561
    },
    'BR': {
      'CPF': /^\d{11}$/,   // Individual: 11 dígitos
      'CNPJ': /^[0-9A-Za-z]{12}\d{2}$/   // Empresa: 14 caracteres, numérico o alfanumérico (12 letras/números + 2 dígitos verificadores)
    },
    'MX': {
      'RFC': /^[A-Z]{3,4}\d{6}[A-Z0-9]{3}$/  // Formato ID fiscal
    },
    'CL': {
      'RUT': /^\d{7,8}-[\dkK]$/  // Formato: 12345678-9
    }
  };
  
  const regex = validators[country]?.[documentType];
  if (!regex) {
    console.warn(`No hay validador para ${country} ${documentType}`);
    return true;  // Permitir si no hay validador
  }
  
  // CPF y CNPJ se aceptan con o sin formato: se quita antes de validar
  const value = country === 'BR' ? documentNumber.replace(/[.\/-]/g, '') : documentNumber;
  const isValid = regex.test(value);
  if (!isValid) {
    console.error(`Formato ${documentType} inválido: ${documentNumber}`);
    console.log(`Formato esperado: ${regex}`);
  }
  
  return isValid;
}
 
// Uso
const isValid = validateDocument('CO', 'CED_CIU', '1234567890');
if (!isValid) {
  throw new Error('Número de documento inválido');
}

Orden Duplicada / Problemas de Idempotencia

Problema: Crear órdenes duplicadas u obtener error “Orden ya existe”

Solución: Usar externalId para idempotencia

JavaScript
async function createOrderIdempotent(internalOrderId, orderData) {
  // Usar tu ID de orden interno como externalId
  const externalId = `order-${internalOrderId}`;
  
  try {
    const order = await createPayinOrder(token, orgId, merchantId, {
      ...orderData,
      externalId: externalId  // Mismo externalId = misma orden
    });
    
    console.log('Orden creada:', order.id);
    return order;
    
  } catch (error) {
    if (error.response?.status === 409) {
      // Orden ya existe, recuperarla
      console.log('Orden ya existe, recuperando...');
      const existingOrder = await getOrderByExternalId(externalId);
      return existingOrder;
    }
    throw error;
  }
}
 
// Seguro para reintentar
const order = await createOrderIdempotent('12345', orderData);

Problemas de Flujo de Pago

Orden Atascada en PENDING

Problema: La orden permanece en estado PENDING y nunca se completa

Causas en Producción:

  • Cliente no ha completado el pago
  • URL de pago expiró
  • Problemas con proveedor de pago

Causas en Sandbox:

  • Usar monto de prueba de fallo (666)
  • Problemas de red

Soluciones:

JavaScript
async function checkOrderStatus(orderId) {
  const order = await getOrderStatus(token, orgId, merchantId, orderId);
  
  console.log('Estado:', order.status);
  console.log('Creada:', order.createdAt);
  console.log('Fecha límite:', order.dueDate);
  
  if (order.status === 'PENDING') {
    const createdTime = new Date(order.createdAt);
    const now = new Date();
    const minutesElapsed = (now - createdTime) / 1000 / 60;
    
    console.log(`Orden pendiente por ${minutesElapsed.toFixed(0)} minutos`);
    
    if (minutesElapsed > 30) {
      console.warn('Orden pendiente por demasiado tiempo - cliente puede no haber pagado');
      // Considerar cancelar o expirar la orden
    }
  }
  
  return order;
}

En Sandbox:

  • Las órdenes se completan automáticamente después de 5-30 segundos
  • Si usas monto 666, la orden fallará (esto es intencional para pruebas)

Pago Completado pero No se Recibió Webhook

Problema: La orden muestra COMPLETED en API pero no se recibió webhook

Causas:

  • Endpoint de webhook no configurado
  • Endpoint de webhook no alcanzable
  • Endpoint de webhook devolviendo errores
  • Firewall bloqueando webhooks

Soluciones:

  1. Verificar endpoint de webhook

    Verifica que tu endpoint sea públicamente accesible

  2. Probar con webhook.site

    Usa para ver si los webhooks están siendo enviados

  3. Verificar logs de webhook

    Consulta intentos de entrega de webhook:

    JavaScript
    const deliveries = await getWebhookDeliveries(orderId);
    console.log(deliveries);
  4. Verificar validación de firma

    Asegúrate de no estar rechazando webhooks por fallo en verificación de firma

  5. Implementar sondeo de respaldo

    Como respaldo, hacer polling del estado de orden:

    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(`Orden ${order.status}`);
        }
        
        // Esperar 3 segundos antes de próxima verificación
        await new Promise(resolve => setTimeout(resolve, 3000));
      }
      
      throw new Error('Tiempo de espera agotado');
    }

Firma de Webhook Inválida

Problema: La verificación de firma de webhook falla

Causa: Cálculo de firma incorrecto o secret incorrecto

Solución:

JavaScript
const crypto = require('crypto');
 
function verifyWebhookSignature(payload, signature, secret) {
  // payload debe ser el body crudo (string o Buffer)
  // NO JSON parseado
  
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  
  const isValid = signature === expectedSignature;
  
  if (!isValid) {
    console.error('Firma no coincide');
    console.error('Recibida:', signature);
    console.error('Esperada:', expectedSignature);
  }
  
  return isValid;
}
 
// Ejemplo Express - DEBE usar body crudo
app.post('/webhooks/koywe', 
  express.raw({ type: 'application/json' }),  // Importante: raw, no json
  (req, res) => {
    const signature = req.headers['koywe-signature'];
    const secret = process.env.KOYWE_WEBHOOK_SECRET;
    
    // req.body es Buffer al usar express.raw
    if (!verifyWebhookSignature(req.body, signature, secret)) {
      return res.status(401).send('Firma inválida');
    }
    
    // Ahora parsear
    const event = JSON.parse(req.body);
    console.log('Evento webhook válido:', event.type);
    
    // Procesar evento...
    
    res.status(200).send('OK');
  }
);

Próximos Pasos