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
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. POST a tu events_webhook_url → TERMINAL_ACKNOWLEDGED (primer evento, 1 a 2 s después)3. La terminal opera la tarjeta → CARD_PRESENTED, APPROVAL_REQUESTED4. Último evento → APPROVAL, DECLINED, TERMINAL_REJECTED o TERMINAL_CANCELED,con el detalle de la operación
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": 12100,"extra_taxes": { "airport_tax": 0, "iac": 0, "ice": 0, "travel_agency": 0 }},"client_transaction_id": "82517631-2789-4efb-8018-6234f35be2b6","metadata": {"reference": "ABC12345","customer_email": "cliente@ejemplo.com","device": "SUNMI-T2"}}
La respuesta y los eventos no tienen la misma forma
Recibes dos tipos de mensaje, y conviene deserializarlos por separado:
- La respuesta HTTP a tu llamada asíncrona. Siempre con
status: TERMINAL_ACKNOWLEDGEDyprevious_statusvacío. Incluyeoperationcomo eco de tu request y un objetoterminalencamelCase. - Cada entrega de webhook. Empieza con un
TERMINAL_ACKNOWLEDGEDpropio (conevent_iddistinto al de la respuesta), sigue con los cambios de estado y termina con el resultado del adquirente. Aquíterminalviene ensnake_case, yoperationyraw_responsesolo llegan en el evento final.
Respuesta HTTP a un POST /async/charge:
{"event_id": "9cc0ee68-6454-4288-a8fc-4514abe6c453","previous_status": "","occurred_at": "2026-10-12T21:40:43.311Z","status": "TERMINAL_ACKNOWLEDGED","merchant_id": "20000000104598300000","client_transaction_id": "82517631-2789-4efb-8018-6234f35be2b6","terminal": {"serialNumber": "TJ54241P20911","model": "P2SE-BPKT","wifiMac": "","room": "3.0.10","terminal_id": "00000001"},"operation": {"type": "charge","amount": {"iva": 0,"subtotal_iva": 0,"subtotal_iva0": 12100,"extra_taxes": { "airport_tax": 0, "iac": 0, "ice": 0, "travel_agency": 0 }},"client_transaction_id": "82517631-2789-4efb-8018-6234f35be2b6","metadata": { "reference": "ABC12345", "customer_email": "cliente@ejemplo.com", "device": "SUNMI-T2" },"events_webhook_url": "https://api.tunegocio.com/webhook/terminal-events"}}
En la respuesta, operation.type toma los valores charge, pre-auth, re-auth y capture. En reautorización y captura, operation incluye además el transaction_reference que enviaste.
Los errores que puede devolver la llamada (400, 401, 409, 422, 500) están en la referencia de API de Kushki One de tu país: México · Colombia · Perú · Chile.
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: tiempo de espera agotado, máximo de reintentos o validación. failure_reason indica la causa |
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 y generar un nuevo CARD_PRESENTED. Por eso el ciclo es un grafo y no una línea recta: no asumas una cantidad fija de eventos por transacción.
DECLINED y TERMINAL_REJECTED son los dos estados finales de rechazo. Trátalos igual en tu consumidor y lee la causa en failure_reason.
En un cobro aprobado sin incidentes recibirás cuatro eventos. En las pruebas de certificación, el ciclo completo tomó entre 8 y 33 segundos, un rechazo 11 segundos y una cancelación en la terminal 2,5 segundos; la espera más larga es siempre la del tarjetahabiente, entre TERMINAL_ACKNOWLEDGED y CARD_PRESENTED.
Estructura del evento
Un cobro aprobado con lectura contactless produce los cuatro primeros eventos. Los tres primeros son livianos; el final trae la operación y, si fue aprobado, la respuesta del adquirente. Las dos últimas pestañas muestran los eventos finales de un rechazo y de una cancelación.
1 · TERMINAL_ACKNOWLEDGED
{"event_id": "e7fc8be2-8186-418f-9515-99a8f638ecea","occurred_at": "2026-10-12T21:40:44.800Z","status": "TERMINAL_ACKNOWLEDGED","merchant_id": "20000000104598300000","client_transaction_id": "82517631-2789-4efb-8018-6234f35be2b6","terminal": { "serial_number": "TJ54241P20911", "brand": "SUNMI", "model": "P2SE-BPKT" }}
2 · CARD_PRESENTED
{"event_id": "c5119765-79c3-4708-bfed-68634d388e07","previous_status": "TERMINAL_ACKNOWLEDGED","occurred_at": "2026-10-12T21:41:06.317Z","status": "CARD_PRESENTED","merchant_id": "20000000104598300000","client_transaction_id": "82517631-2789-4efb-8018-6234f35be2b6","reading_type": "CONTACTLESS","terminal": { "serial_number": "TJ54241P20911", "brand": "SUNMI", "model": "P2SE-BPKT" }}
3 · APPROVAL_REQUESTED
{"event_id": "904681a6-0e56-4ff5-b324-a01f1202be11","previous_status": "CARD_PRESENTED","occurred_at": "2026-10-12T21:41:06.853Z","status": "APPROVAL_REQUESTED","merchant_id": "20000000104598300000","client_transaction_id": "82517631-2789-4efb-8018-6234f35be2b6","reading_type": "CONTACTLESS","terminal": { "serial_number": "TJ54241P20911", "brand": "SUNMI", "model": "P2SE-BPKT" }}
4 · APPROVAL
{"event_id": "32acd9c8-7f6e-45ac-9d8a-25409c9ffd17","previous_status": "APPROVAL_REQUESTED","occurred_at": "2026-10-12T21:41:17.485Z","status": "APPROVAL","merchant_id": "20000000104598300000","client_transaction_id": "82517631-2789-4efb-8018-6234f35be2b6","reading_type": "CONTACTLESS","terminal": { "serial_number": "TJ54241P20911", "brand": "SUNMI", "model": "P2SE-BPKT" },"operation": {"type": "CHARGE","amount": {"currency": "CLP","subtotal_iva0": 12100,"tip": 0,"extra_taxes": { "airport_tax": 0, "iac": 0, "ice": 0, "travel_agency": 0 }},"transaction_mode": "Authorization","client_transaction_id": "82517631-2789-4efb-8018-6234f35be2b6","country": "CHL","is_deferred": false,"is_cashback": false,"cashback_amount": 0,"omit_card": false,"metadata": { "reference": "ABC12345", "customer_email": "cliente@ejemplo.com", "device": "SUNMI-T2" },"query_deferred": false,"default_deferred": "9","show_decimal_places": false,"events_webhook_url": "https://api.tunegocio.com/webhook/terminal-events"},"raw_response": {"additional_amounts": { "account_type": "", "amount": 0, "amount_type": "", "card_type": "", "currency_code": "" },"authorized_amount": 12100,"card_type": "credit","cvm_type": "none","franchise": "MASTERCARD","kushki_response": { "code": "000", "message": "Approved or completed successfully." },"message_fields": { "F38": "143172", "F39": "00", "F55": "9110..." },"transaction_reference": "39356293-3de6-4612-b176-ae8e4a496416","transaction_status": "APPROVAL","transaction_type": "charge","ticket_number": "111930334964482297","card_data": { "bank": "Banco Ejemplo S.A.", "brand": "mastercard", "country": "COL", "type": "credit" }}}
TERMINAL_REJECTED
{"event_id": "e9f47c7f-512c-444e-9855-730e7d02712f","previous_status": "CARD_PRESENTED","occurred_at": "2026-10-13T15:50:24.475Z","status": "TERMINAL_REJECTED","merchant_id": "20000000104598300000","client_transaction_id": "89947d39-ad3c-4a68-a528-d2d199d9fe0a","reading_type": "CHIP","failure_reason": { "type": "TERMINAL-SUNMI", "code": "MAN-30005", "message": "Input PIN timeout" },"terminal": { "serial_number": "TJ54241P20911", "brand": "SUNMI", "model": "P2SE-BPKT" },"operation": {"type": "CHARGE","amount": {"currency": "CLP","subtotal_iva0": 1000,"tip": 0,"extra_taxes": { "airport_tax": 0, "iac": 0, "ice": 0, "travel_agency": 0 }},"transaction_mode": "Authorization","client_transaction_id": "89947d39-ad3c-4a68-a528-d2d199d9fe0a","country": "CHL","is_deferred": false,"is_cashback": false,"cashback_amount": 0,"omit_card": false,"metadata": { "reference": "ABC12345", "customer_email": "cliente@ejemplo.com", "device": "SUNMI-T2" },"query_deferred": false,"default_deferred": "9","show_decimal_places": false,"events_webhook_url": "https://api.tunegocio.com/webhook/terminal-events"}}
Sin raw_response: la transacción no llegó al adquirente. failure_reason trae el código y el mensaje del rechazo; interprétalo con el Catálogo de errores.
TERMINAL_CANCELED
{"event_id": "af44062d-5e21-4a52-8e02-7ea24340efcf","previous_status": "TERMINAL_ACKNOWLEDGED","occurred_at": "2026-10-13T15:52:40.073Z","status": "TERMINAL_CANCELED","merchant_id": "20000000104598300000","client_transaction_id": "11ce8fd4-5d6c-4b88-b9d3-5fa79492f57a","terminal": { "serial_number": "TJ54241P20911", "brand": "SUNMI", "model": "P2SE-BPKT" },"operation": {"type": "CHARGE","amount": {"currency": "CLP","subtotal_iva0": 10000,"tip": 0,"extra_taxes": { "airport_tax": 0, "iac": 0, "ice": 0, "travel_agency": 0 }},"transaction_mode": "Authorization","client_transaction_id": "11ce8fd4-5d6c-4b88-b9d3-5fa79492f57a","country": "CHL","is_deferred": false,"is_cashback": false,"cashback_amount": 0,"omit_card": false,"metadata": { "reference": "ABC12345", "customer_email": "cliente@ejemplo.com", "device": "SUNMI-T2" },"query_deferred": false,"default_deferred": "9","show_decimal_places": false,"events_webhook_url": "https://api.tunegocio.com/webhook/terminal-events"}}
Sin reading_type (la tarjeta nunca se presentó), sin failure_reason y sin raw_response.
| Campo | Presencia | Descripción |
|---|---|---|
event_id | Siempre | Identificador único del evento. Úsalo para descartar duplicados |
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 | serial_number, brand y model del dispositivo |
previous_status | Desde el 2.º evento | Estado anterior. El primer evento no lo incluye |
reading_type | Desde CARD_PRESENTED | CHIP, CONTACTLESS o MAGNETIC_STRIPE |
operation | Evento final | La operación tal como la procesó la terminal: type (CHARGE, PREAUTH, REAUTHORIZATION, CAPTURE), amount, metadata, events_webhook_url y banderas de la operación. Llega en APPROVAL, TERMINAL_REJECTED y TERMINAL_CANCELED |
operation.transaction_reference | Evento final, en reautorización y captura | La referencia original que enviaste en el request (la del preauth) |
failure_reason | TERMINAL_REJECTED y DECLINED | type, code y message del rechazo. Interprétalo con el Catálogo de errores. TERMINAL_CANCELED no lo incluye |
raw_response | Solo en APPROVAL | Respuesta del adquirente: transaction_reference, ticket_number, transaction_status, transaction_type, authorized_amount, kushki_response, franchise, card_type, card_data y campos ISO 8583 (F11, F38, F39, F55) |
raw_response.transaction_reference | Solo en APPROVAL | La referencia nueva de esta operación. Guárdala: es la que necesitas para capturar, reautorizar, anular o devolver |
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. Elterminal.serial_numberte 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. Concilia con el endpoint de consulta de transacciones (referencia de API de Kushki One de tu país: México · Colombia · Perú · Chile). - 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
APPROVAL,DECLINED,TERMINAL_REJECTEDyTERMINAL_CANCELEDcomo finales. No llegan más eventos después. EnAPPROVAL, persisteraw_response.transaction_referenceyraw_response.ticket_number; en los rechazos, registrafailure_reason.
- 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 ?? null, // ausente en el primer eventoterminal: e.terminal.serial_number,referencia: e.raw_response?.transaction_reference ?? null, // solo en el evento finalmotivo: 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.get("previous_status"), # ausente en el primer evento"terminal": evento["terminal"]["serial_number"],"referencia": evento.get("raw_response", {}).get("transaction_reference"), # solo en el evento final"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