Servicios asíncronos y webhooks
Inicia cobros sin bloquear tu sistema de caja y recibe el ciclo de vida completo de la transacción por webhook, evento por evento
Un cobro con tarjeta presente depende de una persona: alguien tiene que acercar la tarjeta, elegir cuotas, digitar su PIN. Ese tiempo supera con frecuencia los 15 segundos que tolera la mayoría de las arquitecturas de caja.
Los servicios asíncronos resuelven ese problema. Tu sistema envía la intención de cobro, recibe un acuse inmediato y se libera. Kushki ONE Connect te notifica cada cambio de estado por webhook, hasta la respuesta final del adquirente.
Requisitos
Cómo funciona
1. POST /async/charge → respondemos TERMINAL_ACKNOWLEDGED de inmediato2. La terminal opera la tarjeta3. POST a tu events_webhook_url → un evento por cada cambio de estado4. Último evento → APPROVAL o DECLINED
Activa los eventos
Envía events_webhook_url en el cuerpo del request de cualquier operación asíncrona.
{"events_webhook_url": "https://api.tunegocio.com/webhook/terminal-events","amount": {"iva": 0,"subtotal_iva": 0,"subtotal_iva0": 12000,"extra_taxes": { "airport_tax": 0, "iac": 0, "ice": 0, "travel_agency": 0 }},"client_transaction_id": "c5a3f3be-9d6f-4d39-8af5-58dbb589af79"}
Un sobre, dos usos
Existe un solo esquema de evento y llega en dos momentos:
- La respuesta HTTP a tu llamada asíncrona. Siempre con
status: TERMINAL_ACKNOWLEDGEDyprevious_statusvacío. - Cada entrega de webhook, con los cambios de estado posteriores hasta el resultado del adquirente.
Esa simetría es intencional: escribe un solo deserializador y úsalo para ambos casos.
Estados del ciclo de vida
Kushki ONE modela el cobro como siete estados. Cinco los genera la terminal; solo APPROVAL y DECLINED provienen del adquirente.
status | Origen | Qué significa |
|---|---|---|
TERMINAL_ACKNOWLEDGED | Terminal | La terminal recibió la intención de cobro. Siempre es el primer estado |
TERMINAL_CANCELED | Terminal | El tarjetahabiente canceló en la terminal, o tu caja envió un abort |
CARD_PRESENTED | Terminal | El tarjetahabiente operó la tarjeta. reading_type indica cómo se leyó |
TERMINAL_REJECTED | Terminal | Rechazo local antes de llegar al adquirente: timeout, máximo de reintentos o validación |
APPROVAL_REQUESTED | Terminal | La transacción se envió al adquirente para su autorización |
DECLINED | Adquirente | El adquirente rechazó la transacción |
APPROVAL | Adquirente | El adquirente aprobó la transacción |
Transiciones
TERMINAL_ACKNOWLEDGED ─┬─→ CARD_PRESENTED ──┬─→ APPROVAL_REQUESTED ─┬─→ APPROVAL│ │ ▲ │ └─→ DECLINED│ │ └───┘ tarjeta presentada de nuevo│ └─→ TERMINAL_CANCELED├─→ TERMINAL_CANCELED└─→ TERMINAL_REJECTED
Una tarjeta rechazada en la terminal puede presentarse otra vez, lo que genera un nuevo CARD_PRESENTED con interaction_attempt incrementado. Por eso el ciclo es un grafo y no una línea recta: no asumas una cantidad fija de eventos por transacción.
Estructura del evento
{"event_id": "c08211a1-344c-4f1c-850b-41e33fb08cca","previous_status": "TERMINAL_ACKNOWLEDGED","occurred_at": "2026-08-03T20:53:34.859Z","status": "CARD_PRESENTED","merchant_id": "20000000104598300000","client_transaction_id": "c5a3f3be-9d6f-4d39-8af5-58dbb589af79","interaction_attempt": 1,"reading_type": "CHIP","terminal": {"serialNumber": "TJ54241P20911","model": "P2SE-BPKT","wifiMac": "","room": "3.0.10"},"operation": {"type": "charge","amount": {"iva": 0.0,"subtotalIva": 0.0,"subtotalIva0": 12000.0,"extraTaxes": { "airportTax": 0.0, "iac": 0.0, "ice": 0.0, "travelAgency": 0.0 }},"clientTransactionId": "c5a3f3be-9d6f-4d39-8af5-58dbb589af79","eventsWebHook": "https://api.tunegocio.com/webhook/terminal-events","metadata": {"customerEmail": "cliente@ejemplo.com","device": "SUNMI-T2","reference": "ABC12345"}}}
| Campo | Presencia | Descripción |
|---|---|---|
event_id | Siempre | Identificador único del evento. Úsalo para descartar duplicados |
previous_status | Siempre | Estado anterior. Cadena vacía en el primer evento |
occurred_at | Siempre | Fecha y hora en UTC, formato ISO 8601 con milisegundos |
status | Siempre | Estado actual, uno de los siete anteriores |
merchant_id | Siempre | Identificador de comercio en Kushki |
client_transaction_id | Siempre | El que enviaste en el request. Úsalo para correlacionar |
terminal | Siempre | serialNumber, model, wifiMac, room |
operation | Siempre | Copia de la operación que originó el evento |
interaction_attempt | Condicional | Desde CARD_PRESENTED. Cuenta las veces que se operó la tarjeta |
reading_type | Condicional | Desde CARD_PRESENTED: CHIP, CONTACTLESS o MAGNETIC_STRIPE |
failure_reason | Condicional | Solo en TERMINAL_REJECTED y DECLINED. Contiene type, code y message |
operation.transactionReference | Condicional | En captura, re-autorización, post propina y anulación |
Entrega y reintentos
Tu endpoint debe confirmar la recepción con cualquier código 2xx. Cualquier otra respuesta se evalúa según esta política:
| Resultado | Comportamiento |
|---|---|
2xx | Entrega exitosa. No hay reintentos |
| Timeout, conexión cerrada o fallo de DNS | Reintenta |
408, 429, 500, 502, 503, 504 | Reintenta |
400, 401, 403, 404, 409, 422 | No reintenta: se considera un rechazo permanente |
El reintento usa retroceso exponencial con variación aleatoria, sobre una base de 2 segundos:
espera = min(60s, base * 2^intento) + aleatorio(0..base)
La entrega se detiene tras 10 intentos o 15 minutos, según lo que ocurra primero.
Construye tu consumidor
Como los eventos se reintentan y se reenvían desde la cola, tu endpoint debe ser idempotente.
- Descarta duplicados por
event_id. Los reintentos y los reenvíos repiten el mismo identificador. Guarda los que ya procesaste. - Correlaciona por
client_transaction_id. Todos los eventos de una transacción lo comparten. ElserialNumberte dice qué dispositivo lo generó, pero no sirve como clave de correlación. - Detecta huecos con
previous_status. Si no coincide con el último estado que registraste para esa transacción, falta un evento o llegó fuera de orden. - Responde rápido y procesa después. Devuelve
2xxde inmediato y encola el payload en tu sistema interno. Un endpoint lento provoca reintentos, y los reintentos te cuestan duplicados. - Trata
APPROVALyDECLINEDcomo finales. No llegan más eventos después.
- Javascript
- Python
app.post("/webhook/terminal-events", async (req, res) => {const e = req.body;res.sendStatus(200); // confirma primeroif (await yaProcesado(e.event_id)) return; // idempotenciaawait marcarProcesado(e.event_id);await cola.publicar({transaccion: e.client_transaction_id,estado: e.status,anterior: e.previous_status,terminal: e.terminal.serialNumber,motivo: e.failure_reason ?? null,});});
@app.post("/webhook/terminal-events")async def eventos(evento: dict, respuesta: Response):respuesta.status_code = 200 # confirma primeroif await ya_procesado(evento["event_id"]): # idempotenciareturnawait marcar_procesado(evento["event_id"])await cola.publicar({"transaccion": evento["client_transaction_id"],"estado": evento["status"],"anterior": evento["previous_status"],"terminal": evento["terminal"]["serialNumber"],"motivo": evento.get("failure_reason"),})
Acepta cobros con Kushki ONE
Revisa todos los flujos de cobro disponibles y sus variantes síncronas.
Catálogo de errores
Interpreta el objeto failure_reason de los estados TERMINAL_REJECTED y DECLINED.
Colombia
Ecuador
Mexico
Peru