Integra Winora en tu plataforma
Winora es una pasarela de pagos para Colombia: orquesta el cobro con adquirentes autorizados y notifica a tu sistema. Cómo llega el dinero depende del modelo del comercio (ver abajo). Esta guía cubre todo lo que necesitas para cobrar, recibir webhooks y reembolsar desde tu sistema.
# Introducción
La integración tiene tres piezas:
- API de cobros autenticada con una
API keydel comercio. - Checkout alojado (link de pago) o embebido en tu sitio.
- Webhooks que te notifican cada cambio de estado (firma HMAC).
Todo lo que pidas con una test key corre en modo sandbox — pagos simulados, sin dinero real. Al cambiar a una live key el flujo es idéntico, ya en producción.
Además de la API, Winora trae canales propios que no requieren código de tu lado y comparten la misma cuenta de comercio: el agente de WhatsApp (clientes que cobran desde un chat), la tienda y el cobro rápido. Toda la actividad llega al mismo dashboard y dispara los mismos webhooks.
Cómo llega el dinero
# Empezar en 5 minutos
- Crea tu cuenta y registra tu comercio (organización).
- Solicita los métodos de pago que necesitas (tarjeta, PSE, Nequi…) desde tu panel — el equipo de Winora los habilita por ti.
- Genera una API key en
Organización → API keys. La secret key se muestra una sola vez; guárdala en tus variables de entorno. - Configura un endpoint de webhook (opcional pero recomendado).
- Llama al endpoint de cobros y comparte el link con tu cliente.
SDK de Node.js
npm install @winorapay/node trae los tipos de toda la API, la idempotencia y los reintentos seguros, y la verificación de webhooks. Los ejemplos de esta guía lo muestran en la pestaña Node.js (SDK).# Autenticación
Cada organización tiene dos tipos de credenciales:
wk_test_…/wk_live_…— secret key, sólo en el servidor del integrador. Se devuelve hasheada (SHA-256) en la base de datos.wp_test_…/wp_live_…— publishable key, segura para usar en el navegador (sólo permite mintar form tokens).
Las llamadas al servidor llevan la secret key en el header estándar:
Authorization: Bearer wk_test_xxxxxxxxxxxxxxxxxxxxLa URL base es https://api.winorapay.com/api/v1. Todas las respuestas siguen un envelope consistente:
// Éxito
{ "ok": true, "data": { ... } }
// Error
{ "ok": false, "error": { "code": "...", "message": "..." } }# Crear un cobro
El monto va en pesos enteros — el peso colombiano no usa decimales: 50000 = $50.000 COP.
curl -X POST https://api.winorapay.com/api/v1/charges \
-H "Authorization: Bearer wk_test_xxxxxxxx" \
-H "Idempotency-Key: order-1001" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"currency": "COP",
"reference": "Factura 1001",
"description": "Pedido #1001",
"customerEmail": "cliente@correo.com",
"customerFirstName": "Ana",
"customerLastName": "Pérez",
"customerDocType": "CC",
"customerDocNumber": "1020304050",
"customerPhone": "+573001112233",
"returnUrl": "https://tu-tienda.com/pago/resultado"
}'Respuesta
{
"ok": true,
"data": {
"transaction": {
"id": "9b4a…",
"status": "PENDING",
"amount": 50000,
"currency": "COP",
"reference": "Factura 1001",
"createdAt": "2026-05-20T15:30:00.000Z"
},
"checkoutUrl": "https://app.winora/checkout/9b4a…"
}
}checkoutUrl. Winora mantiene la página de pago, y los datos de la tarjeta se escriben en el formulario del adquirente: no pasan por tu servidor.Volver a tu sitio (returnUrl)
Pasa returnUrl al crear el cobro y, cuando el pago quede en un estado final, el checkout devuelve al comprador a esa URL con dos parámetros en el query string: winora_tx (id de la transacción) y winora_status (ej. APPROVED).
https://tu-tienda.com/pago/resultado?winora_tx=9b4a…&winora_status=APPROVEDÚsalo solo para la experiencia de usuario. La confirmación de la que debes fiarte para entregar el producto es el webhook — el comprador podría cerrar el navegador antes de volver.
# Links de cobro
Para cobros que se reusan — pagos por WhatsApp, QR en mostrador, datáfono virtual donde el cliente escribe el monto — crea un payment link. Tienes dos tipos:
type: "fixed"— monto fijo. Equivalente a un link de pago clásico, pero gestionado por tu cuenta de Winora.type: "open"— monto abierto. El comprador decide cuánto pagar al aterrizar enhttps://app.winora/pay/<linkId>. Útil para propinas, servicios variables o un "datáfono virtual".
Los links se crean desde el panel
checkoutUrl para redirigir a tu cliente.Desde el panel puedes mostrar el link como QR, mandarlo por WhatsApp o correo, o ponerlo en tu sitio.
Los pagos hechos a través de un link quedan vinculados a él (campo paymentLinkId en la transacción) para reportes separados.
PSE en links abiertos
open, recuerda que el comprador puede elegir PSE como método de pago — y los pagos PSE no son reembolsables vía API (es un débito ACH). Si quieres permitir solo tarjeta, configura allowedMethods: ["card"].# Checkout
Tienes dos formas de cobrar después de crear la transacción — las dos usan la página de pago alojada por Winora:
1) Checkout alojado (recomendado)
Redirige al cliente al checkoutUrl que Winora devolvió. Es responsivo, soporta tarjeta y PSE, y se actualiza al confirmarse el pago. Lo más simple — no necesitas JavaScript adicional.
// En tu app, después de recibir la respuesta:
window.location.href = data.checkoutUrl;2) Sin salir de tu sitio
Si prefieres no sacar al comprador de tu flujo, abre el checkoutUrl en una ventana modal o una pestaña nueva. Al terminar, el comprador vuelve a tu sitio por el returnUrl que definiste al crear el cobro.
// Abre el checkout sin abandonar tu página
window.open(data.checkoutUrl, 'winora-checkout', 'width=480,height=720');checkoutUrl mientras el pago siga en PENDING — Winora reactiva la sesión automáticamente.# Agente WhatsApp
Tus clientes pueden cobrar dentro de WhatsApp — sin llamarte, sin que tú generes el link, sin salir del chat. Winora corre un agente conversacional sobre un número oficial de WhatsApp Business; el cliente escribe el monto, llena un formulario nativo (Flow) con sus datos y paga con tarjeta o PSE.
Activar el agente para tu comercio
- Entra a tu dashboard en
Agente WhatsApp. Vas a ver un código de comercio tipoWIN-AB12CDy un linkwa.meya pre-rellenado con ese código. - Comparte ese link (o el QR que genera la página) con tus clientes — por tu sitio, redes, factura, lo que sea.
- Cuando un cliente abre el link y manda el primer mensaje, el agente lo asocia automáticamente a tu comercio gracias al código. No hace falta configurar números por tu parte.
WIN-XXXXXX que viaja en el mensaje inicial, y vincula la conversación a tu organización hasta que el cliente envíe el código de otro comercio.Cómo es el flujo para el cliente
- El cliente abre WhatsApp con tu link y manda el saludo (mensaje pre-rellenado con tu código).
- El agente le responde con el nombre de tu comercio y un botón
Pagar ahora. Al tocarlo se abre un Flow nativo de WhatsApp: monto, concepto, datos. - Al confirmar, el agente le envía un botón
Abrir checkoutque lleva a la página de pago alojada por Winora (la misma que describimos arriba). - Cuando el pago queda
APPROVED, el agente notifica al cliente en el chat — y a tu sistema por webhook (eventopayment.approved, idéntico a cualquier otro canal).
Qué ves tú
- Métricas del agente: conversaciones, pagos creados desde WhatsApp, tasa de conversión, monto cobrado por este canal.
- Feed de conversaciones (con teléfono enmascarado para preservar la privacidad del cliente) y trazabilidad de cada link generado.
- Los pagos creados por el agente aparecen en
/transaccionescomo cualquier otro cobro, con supaymentLinkIdy elreferenceque tu cliente escribió en el Flow.
Multi-tenant en V2
# Winora Tap (Android)
En desarrollo
Cuando salga, sus cobros quedarán en el dashboard como cualquier otra transacción, con las mismas reglas de antifraude, webhooks y reembolsos. No tendrá API pública propia: si integras por API, puedes ignorar esta sección.
# Consultar y listar
Filtros soportados: status (PENDING, APPROVED, DECLINED, VOIDED, ERROR, REFUNDED, EXPIRED, ABANDONED) y q (búsqueda parcial en reference). Paginación con page y pageSize (máx 100).
# Un cobro
curl https://api.winorapay.com/api/v1/charges/9b4a… \
-H "Authorization: Bearer wk_test_xxxxxxxx"
# Listado con filtros
curl "https://api.winorapay.com/api/v1/charges?status=APPROVED&q=Factura&page=1&pageSize=20" \
-H "Authorization: Bearer wk_test_xxxxxxxx"# Webhooks salientes
Configura uno o más endpoints en Organización → Webhooks. Winora hace POST a tu URL cuando una transacción cambia de estado. Cada endpoint tiene un secret propio (whsec_…) que firma cada entrega.
Eventos disponibles
Cada endpoint recibe todos los eventos de tu cuenta. Filtra por type y responde 2xx a todo, también a los que no uses: cualquier otra respuesta se reintenta. Los de cartera y suscripciones solo salen con el módulo activo.
| Evento | Cuándo | data |
|---|---|---|
payment.approved | El cobro se aprobó. | { transaction } |
payment.declined | El banco o la red rechazaron el cobro. | { transaction } |
payment.voided | El cobro se anuló. | { transaction } |
payment.error | Falla terminal al procesar el cobro. | { transaction } |
payment.refunded | Se aprobó un reembolso (total o parcial). | { transaction } |
payment.expired | El cobro se cerró sin pago: hubo un intento que nunca se completó. Si el banco aprueba después, llega payment.approved. | { transaction } |
payment.abandoned | El cliente abrió el pago y se fue sin intentar pagar (en PSE, segundos después). Si aun así se aprueba, llega payment.approved. | { transaction } |
receivable.created | Se creó una deuda. | { receivable, eventId } |
receivable.partially_paid | Entró un abono. | { receivable, eventId } |
receivable.paid | La deuda quedó saldada. | { receivable, eventId } |
receivable.overdue | La deuda se venció. | { receivable, eventId } |
receivable.voided | La deuda se anuló. | { receivable, eventId } |
receivable.reopened | El saldo volvió a subir (reembolso o pago quitado). | { receivable, eventId } |
subscription.created | Se creó una suscripción. | { subscription, plan, invoice? } |
subscription.charged | Se cobró un período. | { subscription, plan, invoice } |
subscription.payment_failed | Falló el cobro de un período (se reintenta). | { subscription, plan, invoice } |
subscription.payment_method_required | El cobro ya falló varias veces: pide al cliente otro medio de pago. | { subscription, plan } |
subscription.canceled | Se canceló la suscripción. | { subscription, plan? } |
Cuerpo y headers
{
"id": "<id de la entrega>",
"type": "payment.approved",
"createdAt": "2026-05-20T21:40:00.000Z",
"data": {
"transaction": {
"id": "9b4a…",
"status": "APPROVED",
"amount": 50000,
"reference": "Factura 1001",
"cardLast4": "0002",
"cardBrand": "VISA"
}
}
}Winora-Signature: t=1715890800,v1=<hmac_sha256_hex>
Winora-Event: payment.approved
Content-Type: application/jsonVerificar la firma
Recalcula HMAC_SHA256(secret, "<t>." + rawBody) y compara con v1 en tiempo constante. Usa el cuerpo crudo (no el JSON parseado), o la firma no coincidirá. t es la hora del envío: rechaza las firmas de más de 5 minutos para que nadie te repita una entrega vieja.
import express from 'express';
import { webhooks, WinoraSignatureError } from '@winorapay/node';
const app = express();
const SECRET = process.env.WINORA_WEBHOOK_SECRET; // whsec_…
// La firma va sobre el cuerpo TAL CUAL llegó: express.raw, no express.json.
app.post('/winora/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
// Verifica la firma y la ventana de 5 minutos, y devuelve el evento tipado.
event = webhooks.constructEvent(req.body, req.get('Winora-Signature'), SECRET);
} catch (e) {
if (e instanceof WinoraSignatureError) return res.status(400).end();
throw e;
}
// Responde 2xx de una vez y procesa aparte: tienes 10 segundos.
res.status(200).end();
if (await yaProcesado(event.id)) return; // el mismo evento puede llegar más de una vez
if (event.type === 'payment.approved' && event.data.transaction.mode === 'live') {
await entregarPedido(event.data.transaction);
}
await marcarProcesado(event.id);
});Reintentos
# Reembolsos
Reembolsos totales o parciales sobre un cobro APPROVED. Cuando la suma de reembolsos aprobados llega al total, la transacción pasa a REFUNDED.
curl -X POST https://api.winorapay.com/api/v1/charges/9b4a.../refunds \
-H "Authorization: Bearer wk_test_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "amount": 2500000, "reason": "Cliente solicitó devolución" }'amounten pesos enteros. Si lo omites, reembolsa el saldo disponible.- No puede exceder
transaction.amount - transaction.refundedAmount. - Cuando se aprueba dispara el evento
payment.refunded.
PSE no es reembolsable por API
transaction can't be refunded y Winora registra el intento como failed en el ledger. Los reembolsos automáticos aplican a tarjetas (Visa/MC/Amex).# Cartera · API
Para que tu sistema cargue y siga la cartera sin entrar al panel. Necesita el módulo de Cartera activo. Si eres el comercio y no el integrador, empieza por la guía de Cartera.
Montos en pesos enteros
150000 son $150.000 (no en centavos como en /charges). El modo lo decide la llave: con wk_test_ solo ves y creas deudas de prueba; con wk_live_, las reales. Una deuda de otro comercio o del otro modo responde 404, igual que una que no existe.| Método y ruta | Qué hace |
|---|---|
POST /api/v1/receivables | Crea una deuda (y su deudor, si no existe) |
POST /api/v1/receivables/batch | Crea hasta 200 deudas; cada una se valida por su lado |
GET /api/v1/receivables | Lista, con filtros y paginación |
GET /api/v1/receivables/:id | Una deuda |
PATCH /api/v1/receivables/:id | Cambia concepto, referencia, monto, vencimiento… |
POST /api/v1/receivables/:id/void | Anula la deuda (y cancela su cobro en línea abierto) |
POST /api/v1/receivables/:id/payments | Registra un pago recibido por fuera (caja, consignación) |
POST /api/v1/debtors | Crea o actualiza un deudor y da su estado de cuenta |
Crear una deuda
POST /api/v1/receivables
Authorization: Bearer wk_test_xxxxxxxx
Idempotency-Key: deuda-JUR-1001
Content-Type: application/json
{
"debtor": { "fullName": "Carlos Pérez", "docType": "CC", "docNumber": "7001002003",
"email": "carlos@correo.com", "phone": "3001234567" },
"concept": "Cuota crédito octubre",
"externalRef": "JUR-1001",
"amount": 150000,
"dueDate": "2026-11-30",
"projectCode": "sede-norte"
}Respuesta (dentro de data):
{
"id": "…", "externalRef": "JUR-1001", "concept": "Cuota crédito octubre",
"amount": 150000, "chargesTotal": 0, "balance": 150000, "currency": "COP",
"dueDate": "2026-11-30", "status": "open", "overdue": false, "mode": "test",
"debtor": { "id": "…", "fullName": "Carlos Pérez", "docType": "CC", "docNumber": "7001002003",
"email": "carlos@correo.com", "phone": "3001234567" },
"project": "sede-norte",
"form": null,
"payUrl": "https://pagos.winorapay.com/cartera/…",
"paidAt": null, "voidedAt": null, "createdAt": "…"
}payUrles el link de pago para mandarle al deudor; vienenullcuando la deuda ya no se puede pagar (pagada o anulada).status:open,partially_paid,paidovoided.externalRefes única por comercio y modo entre las deudas no anuladas: repetirla da409 collections/external-ref-taken. Distingue mayúsculas y guiones (JUR-1001≠jur1001).projectCode(opcional): el código de uno de tus proyectos; sin él, o con"principal", va al principal. Uno que no existe responde400 collections/project-not-found.- El deudor se reconoce por su documento. Con una llave de prueba no se puede usar el documento de alguien con deudas reales (
409 collections/debtor-has-live-debts): usa documentos de prueba. formviene cuando la deuda salió de un formulario de recaudo:{ id, name, answers: [{ label, value }] }, con lo que escribió quien pagó. Esas deudas se anulan solas si no se pagan en 24 horas, y reviven si el pago se aprueba tarde: no tratesvoidedcomo final en las que traenform.
Lote, listar, pagos por fuera y deudores
- Lote (
{ "items": [ … hasta 200 … ] }, hasta 512 KB): responde200aunque algunas fallen, concreated,failedy un resultado por cada una en el mismo orden. - Listar:
?externalRef=&status=&debtorDoc=&dueFrom=&dueTo=&page=1&pageSize=25(pageSizehasta 100; fechasAAAA-MM-DD). - Pago por fuera:
{ "amount": 50000, "method": "cash", "paidOn": "2026-11-01", "note": "…" }, conmethodentrecash,transfer,deposit,card_terminaluother. No puede pasar del saldo ni tener fecha futura. - Deudor: el mismo cuerpo que
debtor. Si ya existe, se actualiza su contacto; la respuesta traestatementUrl, su estado de cuenta.
# Cartera · Webhooks
Vienen apagados. Se encienden en Cartera → Configuración → Integraciones; lo que pasó mientras estaban apagados no se envía. Llegan a los mismos endpoints y con la misma firma que los de pagos (ver Webhooks salientes).
| Evento | Cuándo |
|---|---|
receivable.created | Se creó la deuda (panel, CSV, recurrente, formulario o API) |
receivable.partially_paid | Entró un abono (en línea o registrado por fuera) |
receivable.paid | La deuda quedó pagada |
receivable.overdue | Venció sin pagarse (una vez, a las 00:30 de Bogotá) |
receivable.voided | Se anuló |
receivable.reopened | El saldo volvió a subir (un reembolso, o se quitó un pago registrado) |
data.receivabletiene la forma de la respuesta de la API y trae el estado al momento de enviar;data.eventIdidentifica el cambio (el mismo en cada reintento): úsalo para descartar repetidos.- Salen en hasta un minuto; con muchos cambios se reparten en los minutos siguientes. El orden no está garantizado: guíate por
data.receivable. - Responde 2xx también a los eventos que no uses: Winora puede sumar tipos nuevos.
# Cartera · Consulta en línea
Si tu cartera vive en tu sistema, el portal de pagos le puede preguntar a él qué debe cada persona, en el momento, en vez de que la cargues. Tú expones una URL; Winora la llama cuando un deudor busca en tu portal. Se configura en Cartera → Configuración → Integraciones: el portal busca solo en lo cargado, solo en tu sistema, o en las dos (primero lo cargado). Ahí mismo está Probar consulta, que llama a tu sistema sin guardar nada.
Lo que Winora te manda
POST https://tu-sistema.com/winora/cartera
Content-Type: application/json
Authorization: Bearer <tu token> (o Basic, si lo configuraste)
Winora-Signature: t=<unix_ts>,v1=<hmac_sha256_hex>
Winora-Event: collections.lookup
{ "requestId": "…", "query": "1020304050", "lookupBy": "doc", "mode": "live" }queryllega sin espacios, puntos ni guiones, en mayúsculas.lookupByes lo que pide tu portal:doc,refoboth.modeesliveotest, según el modo del comercio.- Verifica la firma:
HMAC_SHA256(secreto, "<t>." + cuerpo)sobre el cuerpo tal cual llegó, comparado en tiempo constante conv1, y rechaza untde más de 5 minutos. El secreto (olsec_…) se muestra una sola vez al guardar la configuración o al rotarlo.
Lo que respondes
{
"debtor": { "fullName": "Ana Gómez", "docType": "CC", "docNumber": "1020304050",
"phone": "3001234567", "email": "ana@correo.com" },
"debts": [
{ "externalRef": "CR-1001", "concept": "Cuota noviembre", "balance": 120000, "dueDate": "2026-11-30" }
]
}- 200, JSON, máximo 256 KB y 50 deudas. Sin deudas con ese dato:
404, o200con"debtor": nullexplícito. Una respuesta sin la llavedebtor(por ejemplo envuelta endata) es un error de formato y no se toca nada. debtses obligatorio cuando hay deudor (manda[]si no debe nada).balancees lo que debe hoy, en pesos enteros (de $1.000 a $50.000.000); con0la línea se ignora. Una línea mal escrita se descarta y la ves como aviso en Configuración.- Buscando por documento, devuelve todas las deudas pendientes de esa persona: lo que no vuelva se da por pagado o anulado (nunca si hubo alguna línea descartada). Por referencia, basta la de esa referencia.
- El celular o el correo sirven para verificar a la persona: el portal le pide los últimos 4 del celular o su correo y los compara con lo que devolviste. Sin ese dato, no puede entrar.
- Responde dentro del tiempo que configuraste (1 a 10 s). Ante caída, tiempo agotado, 5xx o 429, Winora reintenta lo configurado (0 a 2). No sigue redirecciones.
- Antes de cobrar, si la última consulta tiene más de 2 minutos, Winora te vuelve a preguntar. Cuando el deudor paga, te llega
receivable.paid: marca el pago en tu sistema. Winora no manda recordatorios de estas deudas.
Un servidor de ejemplo
Verifica la firma, busca en tu sistema y responde el formato de arriba. buscarCliente es tu consulta a tu base de datos.
import express from 'express';
import crypto from 'node:crypto';
const app = express();
const SECRET = process.env.WINORA_LOOKUP_SECRET; // olsec_…
// La firma va sobre el cuerpo TAL CUAL llegó: léelo crudo, sin reformatear el JSON.
app.post('/winora/cartera', express.raw({ type: 'application/json' }), async (req, res) => {
const raw = req.body.toString('utf8');
const parts = Object.fromEntries(
(req.get('Winora-Signature') ?? '').split(',').map((p) => p.split('=')),
);
const t = Number(parts.t);
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${raw}`).digest('hex');
// v1 tiene que ser hex de 64: así timingSafeEqual nunca recibe largos distintos.
const valid =
Number.isInteger(t) &&
/^[0-9a-f]{64}$/.test(parts.v1 ?? '') &&
crypto.timingSafeEqual(Buffer.from(parts.v1, 'hex'), Buffer.from(expected, 'hex'));
if (!valid || Math.abs(Date.now() / 1000 - t) > 300) return res.status(401).end();
const { query, lookupBy, mode } = JSON.parse(raw);
const cliente = await buscarCliente(query, lookupBy, mode); // tu sistema
if (!cliente) return res.json({ debtor: null });
res.json({
debtor: {
fullName: cliente.nombre,
docType: 'CC',
docNumber: cliente.documento,
phone: cliente.celular,
email: cliente.correo,
},
debts: cliente.pendientes.map((d) => ({
externalRef: d.referencia, // única por deuda
concept: d.concepto,
balance: d.saldo, // pesos enteros, lo que debe HOY
dueDate: d.vence, // AAAA-MM-DD
})),
});
});
app.listen(3000);# Suscripciones · API
Cobros recurrentes con tarjeta: defines planes y Winora cobra cada período a la tarjeta que el cliente guarda al suscribirse. Necesita el módulo de Suscripciones activo en tu cuenta; sin él, crear un plan o una suscripción responde subscriptions/module-not-active.
Cómo se arma
- Crea el plan con
POST /subscription-plans. - El cliente se suscribe en la página alojada por Winora:
https://secure.winorapay.com/subscribe/<planId>. Ahí paga el primer período y guarda su tarjeta; tú no tocas datos de tarjeta. - Sigue cada cobro con los webhooks
subscription.*. - Gestiona desde tu sistema: pausar, reanudar, cambiar de plan o cancelar.
POST /subscriptions crea una suscripción directo solo si ya tienes un paymentTokenId (una tarjeta guardada del cliente en Winora). Lo normal es la página alojada.
Endpoints
Crear un plan
curl -X POST https://api.winorapay.com/api/v1/subscription-plans \
-H "Authorization: Bearer wk_test_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Plan mensual",
"amountMinor": 49900,
"interval": "month",
"intervalCount": 1
}'amountMinor va en pesos enteros
amountMinor son pesos colombianos enteros: 49900 son $49.900. El período es interval (day, week, month, year) por intervalCount (1 a 12).Estados
| Estado | Qué significa |
|---|---|
active | Al día. Winora cobra en nextBillingDate. |
past_due | Falló el último cobro. Se reintenta a los 3, 7 y 14 días (luego cada 7); a los 30 días del primer fallo se cancela. |
paused | Pausada: no se cobra hasta que la reanudes. |
canceled | Terminada. |
cancelrecibe{ "atPeriodEnd": true }por defecto: el período pagado se respeta y luego termina. Confalsetermina de una vez.change-planaplica desde el próximo período, sin prorrateo.- Winora le manda al cliente, por correo, el enlace a su portal: ahí ve su plan y puede pausar o cancelar. Tú no tienes que gestionarlo.
Manda Idempotency-Key al crear
Idempotency-Key al crear planes y suscripciones (y en pausar, reanudar, cancelar y cambiar de plan): si la red se corta y reintentas con la misma llave, Winora devuelve la que ya creó. Ante un 5xx, consulta con GET /subscriptions?planId=… antes de reintentar con una llave nueva.# Antifraude
Cada cobro se evalúa contra las reglas que configuras en Organización → Riesgo: límites por monto/día, velocity por email e IP, listas de bloqueo y permitidos, y bandera para forzar 3DS. Si una regla bloquea, el endpoint responde:
// HTTP 422 Unprocessable Entity
{
"ok": false,
"error": {
"code": "risk_blocked",
"message": "Pago rechazado por reglas de riesgo"
}
}La respuesta no dice qué regla bloqueó (eso solo le sirve a quien intenta saltársela). Lo que puede bloquear un cobro:
- Correo, IP o documento en la lista de bloqueo del comercio.
- Un monto mayor que el tope por transacción.
- Pasar el acumulado o el número máximo de pagos del día.
- Demasiados intentos seguidos desde el mismo correo o la misma IP.
El motor también puede decidir review (forzar 3DS); en ese caso el cobro no se bloquea, sólo queda marcado para revisión interna del comercio.
# Idempotencia
Para evitar cobros duplicados ante un reintento de red, envía un Idempotency-Key único por intento (o usa el campo idempotencyKey del body). Si Winora ya tiene una transacción con esa clave para tu organización, devuelve la existente en lugar de crear otra.
POST /api/v1/charges
Authorization: Bearer wk_test_xxxxxxxx
Idempotency-Key: order-1001
Content-Type: application/json# Errores
Todos los errores siguen el mismo sobre:
{ "ok": false, "error": { "code": "payment/invalid-customer-email", "message": "El correo del comprador no es válido: el dominio «gmail.con» no existe. ¿Quisiste decir gmail.com?" } }Decide en tu código por code, que no cambia. message está en español. Los 4xx son datos o estados que hay que corregir: reintentar igual da el mismo error.
Un 5xx deja el intento registrado como fallido: repetirlo con la misma Idempotency-Key te devuelve ese fallo, no un intento nuevo. Consulta el estado (GET /charges/:id, GET /charges/:id/refunds, GET /receivables, GET /subscriptions?planId=…) y, si no quedó hecho, reintenta con una llave nueva. La misma llave es para cuando no recibiste ninguna respuesta (red caída o timeout). En un reembolso, revisa su status: un ok: true puede traer un reembolso failed.
Ojo: la misma llave devuelve la respuesta original, no el estado de ahora. Si pausaste una suscripción con una llave, la reanudaste y repites esa pausa, la respuesta dice "pausada" sin volver a pausarla. Para el estado actual, consulta con GET.
Códigos
| HTTP | Código | Qué pasó | Qué hacer |
|---|---|---|---|
| 401 | api/missing-key | Falta la cabecera Authorization: Bearer <llave>. | Corregir y reenviar |
| 401 | api/invalid-key | La llave no existe o fue revocada; o la cuenta está suspendida o cerrada; o es una llave wk_live_ y la verificación del comercio no está aprobada. | Corregir y reenviar |
| 429 | api/too-many-failed-attempts | Demasiados intentos con llaves inválidas desde tu IP. | Esperar y reintentar |
| 429 | api/rate-limited | Superaste el límite de peticiones de tu llave. | Esperar y reintentar |
| 400 | validation/invalid-input | Un campo no cumple el formato. El mensaje dice cuál. | Corregir y reenviar |
| 400 | request/invalid-idempotency-key | La Idempotency-Key no tiene un formato válido (8 a 100 caracteres). | Corregir y reenviar |
| 409 | request/idempotency-mismatch | Reusaste una Idempotency-Key con datos distintos. | Corregir y reenviar |
| 409 | request/in-progress | Una petición con la misma Idempotency-Key todavía se está procesando. | Esperar y reintentar |
| 409 | request/indeterminate | Un intento anterior con esa Idempotency-Key terminó en error y no se sabe si quedó hecho (cartera). | Consultar el estado; si no quedó hecho, reintentar con una llave nueva |
| 400 | payment/invalid-customer-email | El correo del comprador no es válido (p. ej. gmail.con). El mensaje sugiere la corrección: muéstraselo al comprador. | Corregir y reenviar |
| 409 | payment/idempotency-mismatch | Ya existe un cobro con esa llave de idempotencia y otros datos o modo. | Corregir y reenviar |
| 409 | payment/already-processed | Ya existe un cobro con esa llave y ya no está pendiente. Búscalo con GET /charges?q=<referencia>: si quedó en ERROR, crea otro con una llave nueva. | Consultar el estado; si no quedó hecho, reintentar con una llave nueva |
| 422 | risk_blocked | El antifraude bloqueó el cobro. | No reintentar |
| 404 | payment/not-found | El cobro no existe o no es de tu cuenta. | No reintentar |
| 502 | payment/provider-error | La pasarela no respondió o falló. El cobro queda en ERROR. | Consultar el estado; si no quedó hecho, reintentar con una llave nueva |
| 400 | refund/invalid-amount | El monto es mayor que lo que queda por reembolsar. | Corregir y reenviar |
| 409 | refund/not-refundable | El cobro no se puede reembolsar (no está aprobado). | No reintentar |
| 409 | refund/concurrent-conflict | Hay otro reembolso del mismo cobro en curso. | Esperar y reintentar |
| 502 | refund/provider-error | La pasarela no respondió al reembolso. Revisa su status en GET /charges/:id/refunds: no basta con ok. | Consultar el estado; si no quedó hecho, reintentar con una llave nueva |
| 403 | collections/module-not-active | El módulo de Cartera no está activo en la cuenta. | No reintentar |
| 404 | collections/not-found | La deuda no existe o no es de tu cuenta. | No reintentar |
| 409 | collections/external-ref-taken | Ya hay una deuda con esa referencia externa. | No reintentar |
| 409 | collections/not-editable | La deuda ya no se puede editar (pagada, anulada o con pagos). | No reintentar |
| 409 | collections/not-voidable | La deuda ya no se puede anular. | No reintentar |
| 409 | collections/amount-exceeds-balance | El pago registrado es mayor que el saldo. | Corregir y reenviar |
| 403 | subscriptions/module-not-active | Crear un plan o una suscripción sin el módulo de Suscripciones activo. | No reintentar |
| 404 | plan/not-found | El plan no existe o no es de tu cuenta. | No reintentar |
| 409 | plan/inactive | El plan está desactivado. | No reintentar |
| 404 | subscription/not-found | La suscripción no existe o no es de tu cuenta. | No reintentar |
| 400 | subscription/invalid-status | El filtro status no es uno de active, past_due, paused o canceled. | Corregir y reenviar |
| 409 | subscription/already-canceled | Pausar una suscripción que ya está cancelada. | No reintentar |
| 409 | subscription/canceled | Cambiar de plan una suscripción cancelada. | No reintentar |
| 409 | subscription/not-paused | Solo se puede reanudar una suscripción pausada. | No reintentar |
# Buenas prácticas
Lo que más problemas evita en producción:
- No reintentes un 4xx. Un correo mal escrito o un monto fuera de rango falla igual las veces que lo mandes. En los errores de datos de quien paga (como
payment/invalid-customer-emailovalidation/invalid-input) puedes mostrarle elmessagepara que corrija; los demás, mejor tradúcelos a tu propio mensaje. - Manda Idempotency-Key en cada escritura (cobros, reembolsos, cartera y suscripciones). Si la red se corta sin respuesta y reintentas con la misma llave, Winora devuelve lo que pasó en vez de crear otro. Ante un
5xx, consulta y usa una llave nueva (ver Errores). - Responde 2xx a todo webhook, también a los eventos que no uses (por ejemplo
payment.declined). Un 500 por un evento que no esperabas se reintenta seis veces y termina en un aviso de "webhook fallando". - Responde rápido y procesa aparte. Tienes 10 segundos; si tu proceso es largo, guarda el evento, responde y procésalo después.
- Procesa cada cambio una sola vez. Un evento puede llegar repetido: los reintentos traen el mismo
idde entrega, pero cada endpoint recibe su propia entrega con otroid. Descarta repetidos por lo que cambió:- Cobros:
type + transaction.id; enpayment.refunded, súmalerefundedAmount, porque cada reembolso parcial del mismo cobro trae su propio evento. - Suscripciones:
type + invoice.iden los eventos que traen factura (cada cobro trae la suya);type + subscription.idsolo encreated,canceledypayment_method_required. - Cartera:
eventId.
- Cobros:
- Separa pruebas de producción. Los eventos de las llaves
wk_test_…llegan a los mismos endpoints y bien firmados. En producción ignora los que traenmode: "test"(entransaction,subscriptionoreceivable). - Confía en el webhook, no en el regreso del comprador. El comprador puede cerrar el navegador antes de volver a tu sitio; el webhook llega igual. Para confirmar un estado en cualquier momento, consulta
GET /charges/:id. - Verifica la firma siempre, con el cuerpo crudo, y rechaza las de más de 5 minutos.
- Prueba con llaves de prueba.
wk_test_…no mueve plata y dispara los mismos webhooks (conmode: "test"); pasa awk_live_…cuando todo funcione.
# Referencia OpenAPI
Todos los endpoints con llave de API, sus campos, respuestas y los eventos de webhook, en formato OpenAPI 3.1: para importarlo en Postman, Insomnia o generar un cliente en tu lenguaje. Sale del mismo código que valida cada petición, así que no se desactualiza.
- Referencia interactiva — cada endpoint con sus campos y ejemplos.
- openapi.json —
https://merchant.winorapay.com/docs/openapi.json
Probar en Postman
Una colección con todos los endpoints, la llave por variable y ejemplos que pasan la validación, más un entorno de pruebas:
- Colección de Winora y entorno de pruebas.
- En Postman: Import → Link y pega
https://merchant.winorapay.com/docs/winora.postman_collection.json(y el entorno con su enlace, o el archivo descargado). - Elige el entorno Winora · pruebas, pon tu llave
wk_test_…enapiKeyy envía Crear un cobro: la respuesta trae elcheckoutUrlpara pagar con datos de prueba.
© 2026 Winora · Pasarela de pagos · Hecho en Colombia.