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 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);
}// 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:
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
SPEIpara Colombia (usarPSEen su lugar) - Usar
PSEpara Brasil (usarPIXen 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:
function validateDocument(country, documentType, documentNumber) {
const validators = {
'CO': {
'CC': /^\d{6,10}$/, // ID colombiano: 6-10 dígitos
'CE': /^\d{6,7}$/, // ID extranjero: 6-7 dígitos
'NIT': /^\d{9,10}$/ // ID fiscal: 9-10 dígitos
},
'BR': {
'CPF': /^\d{11}$/, // Individual: 11 dígitos
'CNPJ': /^\d{14}$/ // Empresa: 14 dígitos
},
'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
}
const isValid = regex.test(documentNumber);
if (!isValid) {
console.error(`Formato ${documentType} inválido: ${documentNumber}`);
console.log(`Formato esperado: ${regex}`);
}
return isValid;
}
// Uso
const isValid = validateDocument('CO', 'CC', '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
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:
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:
Verificar endpoint de webhook
Verifica que tu endpoint sea públicamente accesible
Probar con webhook.site
Usa https://webhook.site para ver si los webhooks están siendo enviados
Verificar logs de webhook
Consulta intentos de entrega de webhook:
const deliveries = await getWebhookDeliveries(orderId);
console.log(deliveries);Verificar validación de firma
Asegúrate de no estar rechazando webhooks por fallo en verificación de firma
Implementar sondeo de respaldo
Como respaldo, hacer polling del estado de orden:
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:
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');
}
);