Wonder

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.

TIP

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

  1. Generate a random salt (16 bytes) and IV (16 bytes).
  2. Derive an AES-256 key via PBKDF2 (SHA-256, 100 iterations) from the pinCode and salt.
  3. Convert the JSON body to UTF-8 bytes and apply PKCS7 padding.
  4. Encrypt with AES-256-CBC.
  5. Concatenate and base64-encode: base64(salt + IV + ciphertext).
  6. Place this base64 string in the data field of the outer envelope.

Encryption examples

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

  1. 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.
  2. 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.
  3. 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.
  4. Call get device infoAPI. POST to /terminal/info with a pinCode1-encrypted body. You get back the terminal's deviceSn — the serial number you'll use in every subsequent request to identify which terminal to target.
  5. Call pair command. POST to /terminal/async with action: "Pair" (encrypted with pinCode1). The response — after decryption with pinCode1 — contains pinCode2, a 32-character string. From now on, use pinCode2 for every command.
  6. Send commandsAPI. POST to the same /terminal/async endpoint. Every request carries HTTP headers x-p-business-id, x-device-sn (the serial from step 4), and x-request-id (UUID).
/terminal/info API/terminal/async API
Outer layerHTTP 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 keypinCode1pinCode1 (for Pair) or pinCode2 (for everything else)
text
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"
  }
}
text
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"
  }
}
INFO

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

  1. Activate the terminal需要有help文档的超链接API. Same as Cloud — Wonder App → scan QR → business bound to device.
  2. Get the pairing code. Same as Cloud - Get 6-digit pinCode1.
  3. Enter the pinCode1 into your application. Use it to generate an AES-256 key and encrypt a pair command.
  4. Call get device info APIAPI.POST to /terminal/info with a pinCode1-encrypted body. You get back the terminal's deviceSn IP and port.
  5. Send the pair command by entering the terminal's IP and port directly (shown on the terminal screen).
  6. Pair response returns pinCode2. Store pinCode2 and all subsequent commands are POSTed directly to the following terminal's LAN endpoint:
text
// 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 layer

RS232 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.

FieldSizeValueRole
Start symble1 byte0x55Frame delimiter. The terminal scans for this byte to know a message has begun.
CMD2 bytes0x0101Command type identifier. Fixed for ECR communication.
Data size2 bytesvariableLength of the Data field in bytes. Tells the terminal how many more bytes to read.
Type1 byte0x02Protocol type identifier.
Header CRC4 bytesCRC32Checksum of the first 6 bytes (Start + CMD + Size + Type). If this fails, the frame is discarded — no attempt is made to parse the Data.
Datan bytesJSON hexThe {version, action, data} JSON string converted to hex bytes. This is the same JSON envelope used in Cloud and LAN.
Data CRC4 bytesCRC32Checksum of the Data field only. Independent of the Header CRC — a second integrity check on the payload.

End-to-end flow

  1. Build the JSON envelope.{"version":"2.0","action":"Sale","data":"pinCode2-encrypted base64"}.
  2. Convert the JSON string to hex bytes.
  3. Assemble the header. 0x55 0x0101 [dataSize] 0x02.
  4. Compute Header CRC (CRC32 over those 6 bytes) and append.
  5. Append the hex data bytes.
  6. Compute Data CRC (CRC32 over the data bytes) and append.
  7. Write the complete binary frame to the serial port.
  8. Read bytes. From the serial port until you have a complete frame (same structure in reverse), then decrypt the data field with pinCode2.
text
// 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

VariantUse caseExtra commands
RetailShop counter, restaurant POSStandard set only.
TransitBusesAction: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 fieldDescription
DeviceSnThe terminal's serial number.
IpLAN IP address of the terminal.
PortLAN 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 fieldRequiredDescription
ReferenceIDYesYou generate this. UUID v4 per transaction. The terminal uses it for idempotency — repeating the same ID won't create a duplicate charge.
PaymentMethodYesPayment methods: credit_card,fps,octopus,consumer_presented_qr_code,all.
RestrictedPaymentMethodsNoPayment methods: credit_card,fps,octopus,consumer_presented_qr_code,all.
CurrencyYesISO currency code (e.g. HKD).
AmountYesThe sale amount.
RemarkNoNote attached to the transaction.
TimeoutNoSeconds before the terminal cancels the prompt. Default 0 = wait indefinitely.
Response body fieldDescription
StatusSuccess or Failed.
ErrorCode / ErrorMessagePresent on failure.
ReferenceIDEchoed from the request.
Currency / AmountEchoed from the request.
PaymentMethodThe payment method that used for this request
PaymentEntryTypePayment EntriesDOC(e.g.contactless).
TransactionUuidTransaction UUID.
AllowVoidtrue or false.
AllowRefundtrue or false.
ConsumerCountryCodeConsumer country code.
RrnReceiver Reference Number.
BrnBindo Reference Number.
TransactionTypeTransaction type: Sale / PreAuth / Void / Refund.
TransactionTimeTransaction time.
CreditCardCredit 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.

StepActionKey fields you sendKey fields you receive
1.Scanaction:Scancurrency,amount(estimate),waitTime(200-3000ms, default 500ms)referenceID.terminal-generated, linked internally to the card that was read. Card info valid for 1000ms.
2.Saleaction:SalereferenceID(from Scan response), final(currency+amount)status,final amount
INFO

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.

get device info APIAPI

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).