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 inmediato
2. La terminal opera la tarjeta
3. POST a tu events_webhook_url → un evento por cada cambio de estado
4. Ú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_ACKNOWLEDGED y previous_status vací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.

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: timeout, máximo de reintentos o validación
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, 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"
}
}
}
CampoPresenciaDescripción
event_idSiempreIdentificador único del evento. Úsalo para descartar duplicados
previous_statusSiempreEstado anterior. Cadena vacía en el primer evento
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
terminalSiempreserialNumber, model, wifiMac, room
operationSiempreCopia de la operación que originó el evento
interaction_attemptCondicionalDesde CARD_PRESENTED. Cuenta las veces que se operó la tarjeta
reading_typeCondicionalDesde CARD_PRESENTED: CHIP, CONTACTLESS o MAGNETIC_STRIPE
failure_reasonCondicionalSolo en TERMINAL_REJECTED y DECLINED. Contiene type, code y message
operation.transactionReferenceCondicionalEn 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:

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 serialNumber 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.
  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 y DECLINED como 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 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,
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 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["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.