Payment ECR
Wonder Terminal API lets you control physical terminals from your cashier application. Send commands to the terminal to take card payments, void transactions, or process refunds — your software drives the flow; the terminal handles the customer-facing card interaction.
In a physical retail environment, you have a cash register (your POS system) and a payment terminal. ECR is the protocol that connects them. Your POS sends a Sale command to the terminal → the terminal displays the amount and prompts the customer to tap or insert their card → the terminal processes the payment and returns the result to your POS. The same protocol supports voids, refunds, and pre-authorization — all with AES-256 encryption on every message.
Encryption
Every ECR message — request or response, regardless of mode — is encrypted with AES-256-CBC.
Encryption workflow
- Generate a random
salt(16 bytes) andIV(16 bytes). - Derive an
AES-256 keyviaPBKDF2(SHA-256, 100 iterations) from thepinCodeandsalt. - Convert the JSON body to
UTF-8 bytesand applyPKCS7 padding. - Encrypt with
AES-256-CBC. - Concatenate and
base64-encode:base64(salt + IV + ciphertext). - Place this
base64string in the data field of the outer envelope.
Encryption examples
| Language | Download |
|---|---|
| JaveScript | EncryptionExample.html |
| Node.js | EncryptionExample.js |
| Java | EncryptionExample.java |
| Go | EncryptionExample.go |
Three modes to connect
Cloud(via Gateway), LAN(on the same network), or RS232(serial). Pick based on whether the terminal shares your POS netword or has internet access.
Cloud mode
Your POS talks to Wonder's Gateway, and the Gateway forwards commands to the terminal over the internet. Use Cloud when the terminal is remote or cannot sit on the same local network as your POS.
End-to-end workflow
- Activate the terminal需要有help文档的超链接. Power it on. Open the Wonder App, log in, switch to the correct business, scan the terminal's QR code to bind the business to the device.
- Get the pairing code. The terminal enters the pairing page and displays a 6-digit
pinCode1(e.g.260880). This code is also encoded in the terminal's QR code. - Implement AES-256-CBC encryption in your application— PBKDF2 key derivation, random salt + IV per frame, PKCS7 padding. Code samples are available in JavaScript, Node.js, Java, and Go.
- Call get device infoAPI. POST to
/terminal/infowith apinCode1-encrypted body. You get back theterminal's deviceSn— the serial number you'll use in every subsequent request to identify which terminal to target. - Call pair command. POST to
/terminal/asyncwithaction: "Pair"(encrypted withpinCode1). The response — after decryption withpinCode1— containspinCode2, a 32-character string. From now on, usepinCode2for every command. - Send commandsAPI. POST to the same
/terminal/asyncendpoint. Every request carries HTTP headersx-p-business-id,x-device-sn(the serial from step 4), andx-request-id(UUID).
| /terminal/info API | /terminal/async API | |
|---|---|---|
| Outer layer | HTTP body = "base64-ciphertext" | HTTP body = {"version":"2.0","action":"Sale","data":"base64-ciphertext"} |
| Encrypted inner body | {"header":{"requestID","timestamp"},"body":{"pairUuid"}} | {"header":{"requestID","clientDeviceSN","timestamp"},"body":{...action-specific fields...}} |
| Encryption key | pinCode1 | pinCode1 (for Pair) or pinCode2 (for everything else) |
Get Device Info
// URL
POST /svc/payment/public/api/v1/openapi/terminal/info
// HTTP headers
x-p-business-id: ff467f02-5b69-45f3-81aa-bffcca55fe8f
x-request-id: 9c07d8d7-2a43-4a29-9c6d-6b8d8f7d44e5
Content-Type: application/json
// ── Outer layer: HTTP body IS the ciphertext ──
"eK7sD3fG9hJ2mN5pR8tW1yB4vC6xZ0aL..."
// ── Request (after decrypting with pinCode1) ──
{
"header": {
"requestID": "9c07d8d7-2a43-4a29-9c6d-6b8d8f7d44e5",
"timestamp": "2025-11-12T10:12:04+00:00"
},
"body": {
"pairUuid": "3f37e6c0-bf6e-4c00-b1fa-b2bd5e1d6a3b"
}
}
// ── Response (after decrypting with pinCode1) ──
{
"header": {
"responseID": "2c7f32e1-b9e4-4b34-96cc-15c51289f69b",
"clientDeviceSN": "NEXGO-N96-1170270945",
"timestamp": "2025-11-12T10:12:14+00:00"
},
"body": {
"ip": "192.168.1.100",
"port": "4499",
"deviceSn": "NEXGO-N96-1170270945"
}
}Send Command to Terminal
// URL
POST /svc/payment/public/api/v1/openapi/terminal/async
// HTTP headers
x-p-business-id: ff467f02-5b69-45f3-81aa-bffcca55fe8f
x-device-sn: NEXGO-N96-1170270945
x-request-id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Content-Type: application/json
// ── Outer layer: JSON envelope with version + action in the clear ──
{
"version": "2.0",
"action": "Sale",
"data": "N8wRhMEy+l2h0oj6XV6liUP31/2Yi10NNqtOicoV2sr10hIjHQK/sAf..."
}
// ── Request (after decrypting the data field with pinCode2) ──
{
"header": {
"requestID": "5debf769-49d7-4c9b-b6f4-8a9d90e1a874",
"clientDeviceSN": "126498561093",
"timestamp": "2025-11-12T10:11:04+00:00"
},
"body": {
"referenceID": "f8b13b22-16ca-4a87-95a4-df4bebf09ee1",
"currency": "HKD",
"amount": "10.20"
}
}
// ── Response (after decrypting the data field with pinCode2) ──
{
"header": {
"responseID": "5debf769-49d7-4c9b-b6f4-8a9d90e1a874",
"serverDeviceSN": "NEXGO-N96-1170270945",
"timestamp": "2025-11-12T10:11:10+00:00"
},
"body": {
"status": "Success",
"referenceID": "f8b13b22-16ca-4a87-95a4-df4bebf09ee1",
"currency": "HKD",
"amount": "10.20",
"consumerIdentifyHash": "a1b2c3...",
"acquirerType": "Visa"
}
}Void / Refund can only behaviour in Cloud mode: Void and Refund commands are not forwarded to the terminal in any mode. The Gateway processes them server-side. This means you can void or refund a transaction even when the terminal is powered off, offline, or on a different network. All other commands (Sale, Pair, Unpair, etc.) are forwarded to the terminal over the internet.
Lan mode
Your POS communicates with the terminal directly over HTTP on the local network. This is the lowest-latency option and is recommended when the terminal and POS are in the same store.
End-to-end flow
- Activate the terminal需要有help文档的超链接API. Same as Cloud — Wonder App → scan QR → business bound to device.
- Get the pairing code. Same as Cloud - Get 6-digit
pinCode1. - Enter the
pinCode1into your application. Use it to generate an AES-256 key and encrypt a pair command. - Call get device info APIAPI.POST to
/terminal/infowith apinCode1-encrypted body. You get back theterminal's deviceSnIPandport. - Send the pair command by entering the terminal's
IPandportdirectly (shown on the terminal screen). - Pair response returns
pinCode2. StorepinCode2and all subsequent commands are POSTed directly to the following terminal's LAN endpoint:
// LAN endpoint — same for all business commands
POST http://{terminal_ip}:{terminal_port}/v2/wonder/payment/ecr
// The body is the same { version, action, data } envelope as Cloud mode
// data is encrypted with pinCode2
{
"version": "2.0",
"action": "Sale",
"data": "base64(salt + IV + AES-CBC ciphertext)"
}
// No HTTP auth headers needed — the AES encryption is the authentication layerRS232 mode
For legacy cash registers, transportation, and environments where neither LAN nor internet is available. The terminal connects via a physical serial cable, and data is transmitted as binary frames with CRC integrity checks.
Frame format
Unlike Cloud and LAN (which use JSON over HTTP), RS232 wraps the same {version, action, data} JSON inside a binary frame. The terminal reads raw bytes from the serial port and uses this header to know where a message starts, how long it is, and whether the data is intact.
| Field | Size | Value | Role |
|---|---|---|---|
| Start symble | 1 byte | 0x55 | Frame delimiter. The terminal scans for this byte to know a message has begun. |
| CMD | 2 bytes | 0x0101 | Command type identifier. Fixed for ECR communication. |
| Data size | 2 bytes | variable | Length of the Data field in bytes. Tells the terminal how many more bytes to read. |
| Type | 1 byte | 0x02 | Protocol type identifier. |
| Header CRC | 4 bytes | CRC32 | Checksum of the first 6 bytes (Start + CMD + Size + Type). If this fails, the frame is discarded — no attempt is made to parse the Data. |
| Data | n bytes | JSON hex | The {version, action, data} JSON string converted to hex bytes. This is the same JSON envelope used in Cloud and LAN. |
| Data CRC | 4 bytes | CRC32 | Checksum of the Data field only. Independent of the Header CRC — a second integrity check on the payload. |
End-to-end flow
- Build the JSON envelope.{"version":"2.0","action":"Sale","data":"pinCode2-encrypted base64"}.
- Convert the JSON string to hex bytes.
- Assemble the header. 0x55 0x0101 [dataSize] 0x02.
- Compute
Header CRC (CRC32 over those 6 bytes)and append. - Append the hex data bytes.
- Compute
Data CRC (CRC32 over the data bytes)and append. - Write the complete binary frame to the serial port.
- Read bytes. From the serial port until you have a complete frame (same structure in reverse), then decrypt the data field with
pinCode2.
// Building an RS232 frame — conceptual pseudo-code
const json = '{"version":"2.0","action":"Sale","data":"N8wRhMEy..."}'
const dataHex = stringToHex(json) // UTF-8 bytes → "7B 22 76 65 ..."
const dataSize = dataHex.length / 3 // bytes (each hex byte is 2 chars + space)
const header = [0x55, 0x01, 0x01, hi(dataSize), lo(dataSize), 0x02]
const headerCRC = crc32(header)
const dataCRC = crc32(dataHex)
const frame = concat(header, headerCRC, dataHex, dataCRC)
serialPort.write(frame)Two RS232 variants
| Variant | Use case | Extra commands |
|---|---|---|
| Retail | Shop counter, restaurant POS | Standard set only. |
| Transit | Buses | Action:Scan,Action:CancelScan,Action:SaleDirectly. |
ECR interface protocol — command reference
Below describe the body fields inside the encrypted data payload for each action. Unless stated otherwise, all commands below work in all three modes.
Pair action: Pair
Pair the terminal with your business. Encrypted with pinCode1. On success, the response contains pinCode2 — use it for all subsequent commands. This is the only command (along with DeviceInfo) that uses pinCode1.
Ack action: Ack
Acknowledge receipt of a message from the terminal.Encrypted with pinCode2.
Unpair action:Unpair
Disconnect the terminal from the current business. Encrypted with pinCode2.
Device info action:DeviceInfo
Retrieve the terminal's serial number and LAN address. Encrypted with pinCode1.
| Response body field | Description |
|---|---|
| DeviceSn | The terminal's serial number. |
| Ip | LAN IP address of the terminal. |
| Port | LAN port the terminal listens on. |
Sale - (one-step, all modes) action:Sale
Tell the terminal to display an amount and wait for the customer to present their card. You provide the referenceID — it's your idempotency key, not linked to any prior Scan. The terminal prompts the customer, processes the payment, and returns the result. Encrypted with pinCode2.
| Request body field | Required | Description |
|---|---|---|
| ReferenceID | Yes | You generate this. UUID v4 per transaction. The terminal uses it for idempotency — repeating the same ID won't create a duplicate charge. |
| PaymentMethod | Yes | Payment methods: credit_card,fps,octopus,consumer_presented_qr_code,all. |
| RestrictedPaymentMethods | No | Payment methods: credit_card,fps,octopus,consumer_presented_qr_code,all. |
| Currency | Yes | ISO currency code (e.g. HKD). |
| Amount | Yes | The sale amount. |
| Remark | No | Note attached to the transaction. |
| Timeout | No | Seconds before the terminal cancels the prompt. Default 0 = wait indefinitely. |
| Response body field | Description |
|---|---|
| Status | Success or Failed. |
| ErrorCode / ErrorMessage | Present on failure. |
| ReferenceID | Echoed from the request. |
| Currency / Amount | Echoed from the request. |
| PaymentMethod | The payment method that used for this request |
| PaymentEntryType | Payment EntriesDOC(e.g.contactless). |
| TransactionUuid | Transaction UUID. |
| AllowVoid | true or false. |
| AllowRefund | true or false. |
| ConsumerCountryCode | Consumer country code. |
| Rrn | Receiver Reference Number. |
| Brn | Bindo Reference Number. |
| TransactionType | Transaction type: Sale / PreAuth / Void / Refund. |
| TransactionTime | Transaction time. |
| CreditCard | Credit card details. |
Sale - RS232 Transit mode onlyaction:Scan -> Sale
Used in transport scenarios. This is a two-step flow: first Scan and captures the card details, then charges the final amount using the terminal-generated referenceID.
| Step | Action | Key fields you send | Key fields you receive |
|---|---|---|---|
| 1.Scan | action:Scan | currency,amount(estimate),waitTime(200-3000ms, default 500ms) | referenceID.terminal-generated, linked internally to the card that was read. Card info valid for 1000ms. |
| 2.Sale | action:Sale | referenceID(from Scan response), final(currency+amount) | status,final amount |
Scan session rules: The card reader stays open as long as Scan commands arrive within 5 minutes of each other. If no Scan command is received for 5 minutes, the reader closes. The card information read during Scan is valid for 1000ms — after that it expires and cannot be returned.
CancelScan - RS232 Transit mode onlyaction:CancelScan
Close the card reader that was opened by Scan. Request body has no fields beyond the standard header. Response returns status: "Success". Encrypted with pinCode2.
Voidaction:Void
Cancel a previously completed transaction. Cloud-only — processed server-side without forwarding to the terminal. Works even when the terminal is offline. Encrypted with pinCode2.
Refundaction:Refund
Return funds for a previously completed transaction. Cloud-only — processed server-side. Encrypted with pinCode2.
Abortaction:Abort
Interrupt an ongoing Sale command — cancel the current payment prompt on the terminal before the customer completes the transaction. Encrypted with pinCode2.
Query transaction status action:Query
Check the current status of a previously submitted transaction by referenceID. Encrypted with pinCode2.
Related APIs
Retrieve a terminal's serial number, LAN IP, and port before pairing. The HTTP body is the pinCode1-encrypted ciphertext directly — no {version, action} wrapper. Returns deviceSn (required for all subsequent /terminal/async requests), ip, and port. Cloud mode only.
Send command to terminal for Cloud modeAPI
Forward an encrypted ECR command to a specific terminal via the Gateway. The HTTP body wraps the ciphertext in {"version":"2.0","action":"...","data":"..."} so the Gateway can route by action. Commands include Pair (pinCode1), Sale/Void/Refund/Abort/Unpair/Query (pinCode2).