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 immediately2. POST to your events_webhook_url → TERMINAL_ACKNOWLEDGED (first event, 1 to 2 s later)3. The terminal operates the card → CARD_PRESENTED, APPROVAL_REQUESTED4. 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_ACKNOWLEDGEDwith an emptyprevious_status. It echoes your request inoperationand carries aterminalobject incamelCase. - Each webhook delivery. It starts with its own
TERMINAL_ACKNOWLEDGED(with anevent_iddifferent from the response’s), continues with the state changes and ends with the acquirer’s result. Hereterminalis insnake_case, andoperationandraw_responsearrive 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.
status | Origin | What it means |
|---|---|---|
TERMINAL_ACKNOWLEDGED | Terminal | The terminal received the payment intent. Always the first state |
TERMINAL_CANCELED | Terminal | The cardholder canceled on the terminal, or your POS sent an abort |
CARD_PRESENTED | Terminal | The cardholder operated the card. reading_type tells you how it was read |
TERMINAL_REJECTED | Terminal | Rejected locally before reaching the acquirer: wait time exhausted, maximum retries or validation. failure_reason tells you the cause |
APPROVAL_REQUESTED | Terminal | The transaction was sent to the acquirer for authorization |
DECLINED | Acquirer | The acquirer declined the transaction |
APPROVAL | Acquirer | The 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.
| Field | Present | Description |
|---|---|---|
event_id | Always | Unique event identifier. Use it to discard duplicates |
occurred_at | Always | UTC date and time, ISO 8601 with milliseconds |
status | Always | Current state, one of the seven above |
merchant_id | Always | Kushki merchant identifier |
client_transaction_id | Always | The one you sent in the request. Use it to correlate |
terminal | Always | The device’s serial_number, brand and model |
previous_status | From the 2nd event | Previous state. The first event does not include it |
reading_type | From CARD_PRESENTED | CHIP, CONTACTLESS or MAGNETIC_STRIPE |
operation | Final event | The 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_reference | Final event, on re-authorization and capture | The original reference you sent in the request (the pre-auth’s) |
failure_reason | TERMINAL_REJECTED and DECLINED | type, code and message of the rejection. Interpret it with the Error catalog. TERMINAL_CANCELED does not include it |
raw_response | Only on APPROVAL | The 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_reference | Only on APPROVAL | The 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:
| Outcome | Behavior |
|---|---|
2xx | Successful delivery. No retries |
| Timeout, closed connection or DNS failure | Retries |
408, 429, 500, 502, 503, 504 | Retries |
400, 401, 403, 404, 409, 422 | No 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.
- Discard duplicates by
event_id. Retries and replays repeat the same identifier. Store the ones you have already processed. - Correlate by
client_transaction_id. Every event of a transaction shares it.terminal.serial_numbertells you which device generated it, but it is not a correlation key. - 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). - Respond fast and process later. Return
2xximmediately and queue the payload in your internal system. A slow endpoint triggers retries, and retries cost you duplicates. - Treat
APPROVAL,DECLINED,TERMINAL_REJECTEDandTERMINAL_CANCELEDas final. No more events follow. OnAPPROVAL, persistraw_response.transaction_referenceandraw_response.ticket_number; on rejections, recordfailure_reason.
- Javascript
- Python
app.post("/webhook/terminal-events", async (req, res) => {const e = req.body;res.sendStatus(200); // acknowledge firstif (await alreadyProcessed(e.event_id)) return; // idempotencyawait markProcessed(e.event_id);await queue.publish({transaction: e.client_transaction_id,state: e.status,previous: e.previous_status ?? null, // absent on the first eventterminal: e.terminal.serial_number,reference: e.raw_response?.transaction_reference ?? null, // final event onlyreason: e.failure_reason ?? null,});});
@app.post("/webhook/terminal-events")async def events(event: dict, response: Response):response.status_code = 200 # acknowledge firstif await already_processed(event["event_id"]): # idempotencyreturnawait 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.
Colombia
Ecuador
Mexico
Peru