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 inmediato
2. 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_REQUESTED
4. Ú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_ACKNOWLEDGED y previous_status vacío. Incluye operation como eco de tu request y un objeto terminal en camelCase.
  • Cada entrega de webhook. Empieza con un TERMINAL_ACKNOWLEDGED propio (con event_id distinto al de la respuesta), sigue con los cambios de estado y termina con el resultado del adquirente. Aquí terminal viene en snake_case, y operation y raw_response solo 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.

statusOrigenQué significa
TERMINAL_ACKNOWLEDGEDTerminalLa terminal recibió la intención de cobro. Siempre es el primer estado
TERMINAL_CANCELEDTerminalEl tarjetahabiente canceló en la terminal, o tu caja envió un abort
CARD_PRESENTEDTerminalEl tarjetahabiente operó la tarjeta. reading_type indica cómo se leyó
TERMINAL_REJECTEDTerminalRechazo local antes de llegar al adquirente: tiempo de espera agotado, máximo de reintentos o validación. failure_reason indica la causa
APPROVAL_REQUESTEDTerminalLa transacción se envió al adquirente para su autorización
DECLINEDAdquirenteEl adquirente rechazó la transacción
APPROVALAdquirenteEl 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.

CampoPresenciaDescripción
event_idSiempreIdentificador único del evento. Úsalo para descartar duplicados
occurred_atSiempreFecha y hora en UTC, formato ISO 8601 con milisegundos
statusSiempreEstado actual, uno de los siete anteriores
merchant_idSiempreIdentificador de comercio en Kushki
client_transaction_idSiempreEl que enviaste en el request. Úsalo para correlacionar
terminalSiempreserial_number, brand y model del dispositivo
previous_statusDesde el 2.º eventoEstado anterior. El primer evento no lo incluye
reading_typeDesde CARD_PRESENTEDCHIP, CONTACTLESS o MAGNETIC_STRIPE
operationEvento finalLa 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_referenceEvento final, en reautorización y capturaLa referencia original que enviaste en el request (la del preauth)
failure_reasonTERMINAL_REJECTED y DECLINEDtype, code y message del rechazo. Interprétalo con el Catálogo de errores. TERMINAL_CANCELED no lo incluye
raw_responseSolo en APPROVALRespuesta 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_referenceSolo en APPROVALLa 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:

ResultadoComportamiento
2xxEntrega exitosa. No hay reintentos
Timeout, conexión cerrada o fallo de DNSReintenta
408, 429, 500, 502, 503, 504Reintenta
400, 401, 403, 404, 409, 422No 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.

  1. Descarta duplicados por event_id. Los reintentos y los reenvíos repiten el mismo identificador. Guarda los que ya procesaste.
  2. Correlaciona por client_transaction_id. Todos los eventos de una transacción lo comparten. El terminal.serial_number te dice qué dispositivo lo generó, pero no sirve como clave de correlación.
  3. 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).
  4. Responde rápido y procesa después. Devuelve 2xx de inmediato y encola el payload en tu sistema interno. Un endpoint lento provoca reintentos, y los reintentos te cuestan duplicados.
  5. Trata APPROVAL, DECLINED, TERMINAL_REJECTED y TERMINAL_CANCELED como finales. No llegan más eventos después. En APPROVAL, persiste raw_response.transaction_reference y raw_response.ticket_number; en los rechazos, registra failure_reason.
  • Javascript
  • Python
app.post("/webhook/terminal-events", async (req, res) => {
const e = req.body;
res.sendStatus(200); // confirma primero
if (await yaProcesado(e.event_id)) return; // idempotencia
await marcarProcesado(e.event_id);
await cola.publicar({
transaccion: e.client_transaction_id,
estado: e.status,
anterior: e.previous_status ?? null, // ausente en el primer evento
terminal: e.terminal.serial_number,
referencia: e.raw_response?.transaction_reference ?? null, // solo en el evento final
motivo: e.failure_reason ?? null,
});
});
@app.post("/webhook/terminal-events")
async def eventos(evento: dict, respuesta: Response):
respuesta.status_code = 200 # confirma primero
if await ya_procesado(evento["event_id"]): # idempotencia
return
await 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.