| Channel | Enabled by | Fires on | Delivery |
|---|---|---|---|
| Transaction Webhook | events_webhook_url in the request body | Every payment lifecycle state change | Retried with backoff |
| Print Webhook | webhookUrl in the print job body | Print job reaching COMPLETED or FAILED | Fire-and-forget |
POST with Content-Type: application/json, and both require HTTPS in production.events_webhook_url in the request body of an async payment operation:POST /terminal/v1/async/charge ← Local Network
POST /terminal/v1/{terminalSerial}/async/charge ← Cloud{
"events_webhook_url": "https://api.negocio.pe/webhook/terminal-events",
"amount": { "iva": 0, "subtotal_iva": 0, "subtotal_iva0": 12100 },
"client_transaction_id": "82517631-2789-4efb-8018-6234f35be2b6",
"metadata": { "reference": "ABC12345", "customer_email": "user@example.com", "device": "SUNMI-T2" }
}events_webhook_url on an async call is valid — the transaction still runs, but you receive no state notifications and no final outcome.status: TERMINAL_ACKNOWLEDGED with previous_status: "". It echoes your request in operation and carries a terminal object in camelCase (serialNumber, model, wifiMac, room, terminal_id). This is an acknowledgement, not a result.TERMINAL_ACKNOWLEDGED (a different event_id from the response's, 1 to 2 s later), continues with every state change and ends with the final event. Here terminal is in snake_case (serial_number, brand, model), and operation and raw_response arrive only in the final event.operation.type takes the values charge, pre-auth, re-auth and capture; on re-authorization and capture it also echoes the transaction_reference you sent. The response example and the call's error responses (400, 401, 409, 422, 500) are on each async operation under Cloud Services and Local Network Services.APPROVAL and DECLINED come from the acquirer.status | Origin | Meaning |
|---|---|---|
TERMINAL_ACKNOWLEDGED | Terminal | Payment intent received. Processing started. Always the first state. |
TERMINAL_CANCELED | Terminal | Canceled on the terminal by the cardholder, or aborted by the POS. |
CARD_PRESENTED | Terminal | The cardholder presented the card. See reading_type. |
TERMINAL_REJECTED | Terminal | Rejected locally before reaching the acquirer — wait time exhausted, max retries, or validation. failure_reason tells you the cause. |
APPROVAL_REQUESTED | Terminal | Sent to the acquirer for authorization. |
DECLINED | Acquirer | The acquirer declined. |
APPROVAL | Acquirer | The acquirer approved. |
TERMINAL_ACKNOWLEDGED ─┬─→ CARD_PRESENTED ──┬─→ APPROVAL_REQUESTED ─┬─→ APPROVAL
│ │ ▲ │ └─→ DECLINED
│ │ └───┘ card re-presented after rejection
│ └─→ TERMINAL_CANCELED
├─→ TERMINAL_CANCELED
└─→ TERMINAL_REJECTEDAPPROVAL_REQUESTED is the point of no return. Once the transaction reaches the acquirer it can no longer be aborted — you must wait for the final event. If it is approved, reverse it with /void — within the same calendar day it is a cancellation; from midnight onwards it enters the refund cycle.CARD_PRESENTED. This 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.TERMINAL_REJECTED (with failure_reason) or TERMINAL_CANCELED, and if your POS must give up earlier, send abort. If you lose connectivity while waiting, reconcile with Transaction Search.TERMINAL_ACKNOWLEDGED and CARD_PRESENTED.{
"event_id": "e7fc8be2-8186-418f-9515-99a8f638ecea",
"occurred_at": "2026-10-12T21:40:44.800Z",
"status": "TERMINAL_ACKNOWLEDGED",
"merchant_id": "20000000103802320000",
"client_transaction_id": "82517631-2789-4efb-8018-6234f35be2b6",
"terminal": { "serial_number": "TJ54241P20911", "brand": "SUNMI", "model": "P2SE-BPKT" }
}{
"event_id": "c5119765-79c3-4708-bfed-68634d388e07",
"previous_status": "TERMINAL_ACKNOWLEDGED",
"occurred_at": "2026-10-12T21:41:06.317Z",
"status": "CARD_PRESENTED",
"merchant_id": "20000000103802320000",
"client_transaction_id": "82517631-2789-4efb-8018-6234f35be2b6",
"reading_type": "CONTACTLESS",
"terminal": { "serial_number": "TJ54241P20911", "brand": "SUNMI", "model": "P2SE-BPKT" }
}{
"event_id": "904681a6-0e56-4ff5-b324-a01f1202be11",
"previous_status": "CARD_PRESENTED",
"occurred_at": "2026-10-12T21:41:06.853Z",
"status": "APPROVAL_REQUESTED",
"merchant_id": "20000000103802320000",
"client_transaction_id": "82517631-2789-4efb-8018-6234f35be2b6",
"reading_type": "CONTACTLESS",
"terminal": { "serial_number": "TJ54241P20911", "brand": "SUNMI", "model": "P2SE-BPKT" }
}{
"event_id": "32acd9c8-7f6e-45ac-9d8a-25409c9ffd17",
"previous_status": "APPROVAL_REQUESTED",
"occurred_at": "2026-10-12T21:41:17.485Z",
"status": "APPROVAL",
"merchant_id": "20000000103802320000",
"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": "user@example.com", "device": "SUNMI-T2" },
"query_deferred": false,
"default_deferred": "9",
"show_decimal_places": false,
"events_webhook_url": "https://api.negocio.pe/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" }
}
}{
"event_id": "e9f47c7f-512c-444e-9855-730e7d02712f",
"previous_status": "CARD_PRESENTED",
"occurred_at": "2026-10-13T15:50:24.475Z",
"status": "TERMINAL_REJECTED",
"merchant_id": "20000000103802320000",
"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": "user@example.com", "device": "SUNMI-T2" }, "query_deferred": false, "default_deferred": "9", "show_decimal_places": false, "events_webhook_url": "https://api.negocio.pe/webhook/terminal-events" }
}raw_response: the transaction did not reach the acquirer; read failure_reason.code against the Error Catalog. A TERMINAL_CANCELED final event has the same shape, without reading_type, failure_reason or raw_response.| Field | Type | Present | Description |
|---|---|---|---|
event_id | string (UUID) | ✅ | Unique per event. Use this to deduplicate. |
occurred_at | string | ✅ | ISO-8601 UTC with milliseconds. |
status | string | ✅ | Current lifecycle state. One of the seven above. |
merchant_id | string | ✅ | Kushki merchant identifier. |
client_transaction_id | string (UUID) | ✅ | From your request. Use this to correlate events. |
terminal | object | ✅ | { serial_number, brand, model }. |
previous_status | string | ⬦ | State before this event. Absent on the first event. |
reading_type | string | ⬦ | CHIP | CONTACTLESS | MAGNETIC_STRIPE. From CARD_PRESENTED onward. |
operation | object | ⬦ | Final event only (APPROVAL, TERMINAL_REJECTED, TERMINAL_CANCELED). The operation as the terminal processed it: type (CHARGE, PREAUTH, REAUTHORIZATION, CAPTURE), amount, metadata, events_webhook_url and the operation flags. |
operation.transaction_reference | string (UUID) | ⬦ | Final event, on re-authorization and capture. The original reference you sent (the pre-auth's). |
failure_reason | object | ⬦ | { type, code, message }. On TERMINAL_REJECTED and DECLINED. Not on TERMINAL_CANCELED. |
raw_response | object | ⬦ | APPROVAL only. 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 | string (UUID) | ⬦ | APPROVAL only. The new reference of this operation. Store it — you need it for capture, re_authorization, void and refunds. |
operation.transaction_reference repeats the pre-auth reference you sent, and raw_response.transaction_reference carries the new reference of that capture or re-authorization. To reconcile and void, always use the one in raw_response.terminal, operation and raw_response — is snake_case (serial_number, subtotal_iva0, events_webhook_url). The immediate HTTP response is the exception: its terminal object is camelCase (serialNumber, wifiMac). Deserialize each message as it arrives; do not normalize.operation.amount in the final event omits iva and subtotal_iva when they are zero and adds currency and tip. Never reuse an amount taken from an event to build a new request — see Building the amount.franchise, card_type and, in raw_response.card_data, the issuing bank, brand and country of issue. If you need more, read it from the sync response or from Transaction Search.2xx. Anything else is evaluated against this policy:| Outcome | Behavior |
|---|---|
2xx | Success. Delivery complete. |
| Timeout, connection reset, DNS failure | Retry |
408, 429, 500, 502, 503, 504 | Retry |
400, 401, 403, 404, 409, 422 | No retry — treated as permanent rejection |
base = 2s:delay = min(60s, base * 2^attempt) + random(0..base)event_id. Retries and queue replays repeat the same event_id. Persist the ones you have processed and discard repeats.client_transaction_id. All events of one transaction share it. terminal.serial_number tells you which device, but is not a correlation key on its own.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 via Transaction Search.2xx immediately and hand the payload to an internal queue. Slow endpoints trigger retries, which cost you duplicates.APPROVAL, DECLINED, TERMINAL_REJECTED and TERMINAL_CANCELED as final. No further events follow. On APPROVAL, persist raw_response.transaction_reference and raw_response.ticket_number. On rejections, read failure_reason.code against the Error Catalog.COMPLETED or FAILED.webhookUrl when creating the print job. The URL must be reachable from the internet (Cloud) or from the terminal's local network (Local).webhookUrl — not events_webhook_url. The two channels use different parameter names.| Field | Type | Present | Description |
|---|---|---|---|
printJobId | string | ✅ | ID of the finished job. |
status | string | ✅ | COMPLETED | FAILED. |
externalReference | string | ✅ | Reference supplied when enqueuing. Empty string if none. |
errorCode | string | ⬦ | Only when status is FAILED. See printer error codes. |
errorMessage | string | ⬦ | Only when status is FAILED. Human-readable description. |
{
"printJobId": "RECEIPT-20240317-001",
"status": "COMPLETED",
"externalReference": "POS-ORDER-7788"
}{
"printJobId": "RECEIPT-20240317-001",
"status": "FAILED",
"externalReference": "POS-ORDER-7788",
"errorCode": "COVER_OPEN",
"errorMessage": "The thermal printer cover was opened abruptly."
}PENDING and IN_PROGRESS transitions are never delivered. Poll Get Print Job Status if you need them.| Transaction | ||
|---|---|---|
Respond 2xx immediately, queue the work | ✅ | ✅ |
| Serve the endpoint over HTTPS | ✅ | ✅ |
Deduplicate by event_id | ✅ | — |
Deduplicate by printJobId | — | ✅ |
Correlate by client_transaction_id | ✅ | — |
Validate terminal.serial_number against your fleet | ✅ | — |
Detect gaps via previous_status | ✅ | — |
| Expect retries and replays — be idempotent | ✅ | — |
| Build a polling fallback | recommended | required |
Got a suggestion on this documentation? Contact us.