Async services and webhooks

Start payments without blocking your POS and receive the full transaction lifecycle over webhook, one event at a time

Start charges without blocking your point of sale and receive the full transaction lifecycle by webhook, one event at a time

A card-present charge depends on a person: someone has to tap the card, pick installments, type a PIN. That time routinely exceeds the 15 seconds most point-of-sale architectures tolerate.

Async services solve that problem. Your system sends the payment intent, receives an immediate acknowledgement and is released. Kushki ONE Connect notifies you of every state change by webhook, through the acquirer’s final answer.

Requirements

How it works

1. POST /async/charge → we answer TERMINAL_ACKNOWLEDGED immediately
2. POST to your events_webhook_url → TERMINAL_ACKNOWLEDGED (first event, 1 to 2 s later)
3. The terminal operates the card → CARD_PRESENTED, APPROVAL_REQUESTED
4. Last event → APPROVAL, DECLINED, TERMINAL_REJECTED or TERMINAL_CANCELED,
with the operation detail

Enable the events

Send events_webhook_url in the request body of any async operation.

{
"events_webhook_url": "https://api.yourbusiness.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": "customer@example.com",
"device": "SUNMI-T2"
}
}

The response and the events do not share one shape

You receive two kinds of message, and you should deserialize them separately:

  • The HTTP response to your async call. Always status: TERMINAL_ACKNOWLEDGED with an empty previous_status. It echoes your request in operation and carries a terminal object in camelCase.
  • Each webhook delivery. It starts with its own TERMINAL_ACKNOWLEDGED (with an event_id different from the response’s), continues with the state changes and ends with the acquirer’s result. Here terminal is in snake_case, and operation and raw_response arrive only in the final event.

HTTP response to a 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": "customer@example.com", "device": "SUNMI-T2" },
"events_webhook_url": "https://api.yourbusiness.com/webhook/terminal-events"
}
}

In the response, operation.type takes the values charge, pre-auth, re-auth and capture. On re-authorization and capture, operation also includes the transaction_reference you sent.

The errors the call can return (400, 401, 409, 422, 500) are in the Kushki One API reference for your country: Mexico · Colombia · Peru · Chile.

Lifecycle states

Kushki ONE models a payment as seven states. Five come from the terminal; only APPROVAL and DECLINED come from the acquirer.

statusOriginWhat it means
TERMINAL_ACKNOWLEDGEDTerminalThe terminal received the payment intent. Always the first state
TERMINAL_CANCELEDTerminalThe cardholder canceled on the terminal, or your POS sent an abort
CARD_PRESENTEDTerminalThe cardholder operated the card. reading_type tells you how it was read
TERMINAL_REJECTEDTerminalRejected locally before reaching the acquirer: wait time exhausted, maximum retries or validation. failure_reason tells you the cause
APPROVAL_REQUESTEDTerminalThe transaction was sent to the acquirer for authorization
DECLINEDAcquirerThe acquirer declined the transaction
APPROVALAcquirerThe acquirer approved the transaction

Transitions

TERMINAL_ACKNOWLEDGED ─┬─→ CARD_PRESENTED ──┬─→ APPROVAL_REQUESTED ─┬─→ APPROVAL
│ │ ▲ │ └─→ DECLINED
│ │ └───┘ card presented again
│ └─→ TERMINAL_CANCELED
├─→ TERMINAL_CANCELED
└─→ TERMINAL_REJECTED

A card rejected at the terminal can be presented again and produce a new CARD_PRESENTED. That is why the lifecycle is a graph, not a straight line: do not assume a fixed number of events per transaction.

DECLINED and TERMINAL_REJECTED are the two final rejection states. Treat them the same way in your consumer and read the cause in failure_reason.

An approved charge with no incidents produces four events. In certification tests, the full cycle took between 8 and 33 seconds, a rejection 11 seconds and a cancellation at the terminal 2.5 seconds; the longest wait is always the cardholder’s, between TERMINAL_ACKNOWLEDGED and CARD_PRESENTED.

Event structure

An approved contactless charge produces the first four events. The first three are light; the final one carries the operation and, if approved, the acquirer’s response. The last two tabs show the final events of a rejection and a cancellation.

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": "customer@example.com", "device": "SUNMI-T2" },
"query_deferred": false,
"default_deferred": "9",
"show_decimal_places": false,
"events_webhook_url": "https://api.yourbusiness.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": "Example Bank 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": "customer@example.com", "device": "SUNMI-T2" },
"query_deferred": false,
"default_deferred": "9",
"show_decimal_places": false,
"events_webhook_url": "https://api.yourbusiness.com/webhook/terminal-events"
}
}

No raw_response: the transaction did not reach the acquirer. failure_reason carries the rejection code and message; interpret it with the Error catalog.

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": "customer@example.com", "device": "SUNMI-T2" },
"query_deferred": false,
"default_deferred": "9",
"show_decimal_places": false,
"events_webhook_url": "https://api.yourbusiness.com/webhook/terminal-events"
}
}

No reading_type (the card was never presented), no failure_reason and no raw_response.

FieldPresentDescription
event_idAlwaysUnique event identifier. Use it to discard duplicates
occurred_atAlwaysUTC date and time, ISO 8601 with milliseconds
statusAlwaysCurrent state, one of the seven above
merchant_idAlwaysKushki merchant identifier
client_transaction_idAlwaysThe one you sent in the request. Use it to correlate
terminalAlwaysThe device’s serial_number, brand and model
previous_statusFrom the 2nd eventPrevious state. The first event does not include it
reading_typeFrom CARD_PRESENTEDCHIP, CONTACTLESS or MAGNETIC_STRIPE
operationFinal eventThe operation as the terminal processed it: type (CHARGE, PREAUTH, REAUTHORIZATION, CAPTURE), amount, metadata, events_webhook_url and the operation flags. Arrives on APPROVAL, TERMINAL_REJECTED and TERMINAL_CANCELED
operation.transaction_referenceFinal event, on re-authorization and captureThe original reference you sent in the request (the pre-auth’s)
failure_reasonTERMINAL_REJECTED and DECLINEDtype, code and message of the rejection. Interpret it with the Error catalog. TERMINAL_CANCELED does not include it
raw_responseOnly on APPROVALThe acquirer’s response: transaction_reference, ticket_number, transaction_status, transaction_type, authorized_amount, kushki_response, franchise, card_type, card_data and ISO 8583 fields (F11, F38, F39, F55)
raw_response.transaction_referenceOnly on APPROVALThe new reference of this operation. Store it: it is the one you need to capture, re-authorize, void or refund

Delivery and retries

Your endpoint must acknowledge receipt with any 2xx code. Any other response is evaluated against this policy:

OutcomeBehavior
2xxSuccessful delivery. No retries
Timeout, closed connection or DNS failureRetries
408, 429, 500, 502, 503, 504Retries
400, 401, 403, 404, 409, 422No retry: treated as a permanent rejection

Retries use exponential backoff with random jitter, on a 2-second base:

wait = min(60s, base * 2^attempt) + random(0..base)

Delivery stops after 10 attempts or 15 minutes, whichever comes first.

Build your consumer

Because events are retried and replayed from the queue, your endpoint must be idempotent.

  1. Discard duplicates by event_id. Retries and replays repeat the same identifier. Store the ones you have already processed.
  2. Correlate by client_transaction_id. Every event of a transaction shares it. terminal.serial_number tells you which device generated it, but it is not a correlation key.
  3. Detect gaps with previous_status. If it does not match the last state you recorded for that transaction, an event is missing or arrived out of order. Reconcile with the transaction search endpoint (Kushki One API reference for your country: Mexico · Colombia · Peru · Chile).
  4. Respond fast and process later. Return 2xx immediately and queue the payload in your internal system. A slow endpoint triggers retries, and retries cost you duplicates.
  5. Treat APPROVAL, DECLINED, TERMINAL_REJECTED and TERMINAL_CANCELED as final. No more events follow. On APPROVAL, persist raw_response.transaction_reference and raw_response.ticket_number; on rejections, record failure_reason.
  • Javascript
  • Python
app.post("/webhook/terminal-events", async (req, res) => {
const e = req.body;
res.sendStatus(200); // acknowledge first
if (await alreadyProcessed(e.event_id)) return; // idempotency
await markProcessed(e.event_id);
await queue.publish({
transaction: e.client_transaction_id,
state: e.status,
previous: e.previous_status ?? null, // absent on the first event
terminal: e.terminal.serial_number,
reference: e.raw_response?.transaction_reference ?? null, // final event only
reason: e.failure_reason ?? null,
});
});
@app.post("/webhook/terminal-events")
async def events(event: dict, response: Response):
response.status_code = 200 # acknowledge first
if await already_processed(event["event_id"]): # idempotency
return
await mark_processed(event["event_id"])
await queue.publish({
"transaction": event["client_transaction_id"],
"state": event["status"],
"previous": event.get("previous_status"), # absent on the first event
"terminal": event["terminal"]["serial_number"],
"reference": event.get("raw_response", {}).get("transaction_reference"), # final event only
"reason": event.get("failure_reason"),
})
Take payments with Kushki ONE

Review every available payment flow and its sync variants.

Error catalog

Interpret the failure_reason object of the TERMINAL_REJECTED and DECLINED states.