Payment ECR
Payment ECR (Electronic Cash Register) integration with payment terminals enables seamless data exchange between cash register systems and payment devices to streamline checkout processes.
- Sync transaction data (amount, item details) between ECR and payment terminals in real time.
- Automate reconciliation by unifying payment records (card, mobile pay) with ECR sales data.
- Enhance checkout efficiency by eliminating manual data entry and reducing human errors.
Integration Methods
- Local Lan connections: Local LAN network connections for flexible terminal placement in retail/store environments.
- Cloud-based connections: API-driven sync via cloud platforms for multi-location or remote management.
- Wired connections: Serial ports (RS232), Ethernet, or USB for stable, low-latency communication.
Local Lan connections
If you are using local communication integration, API Requests will be sent directly from your application to the IP address of the terminal device. The terminal listens for POST requests to /wonder-terminal on port 8443.
For example, if your terminal has the IP address 198.168.0.100, you would make API requests to: http://182.168.0.100:8443/wonder-terminal
You should either use DHCP for the terminal IP addresses, or manually configure static IP addresses. This helps to prevent connection issues when the terminal or your network reboots.
Cloud-based connections
When you use cloud communication, you will use the Wonder cloud API to send commands to terminal devices, and the cloud will forward the commands to the terminal devices to complete the work. Whether using local communication or cloud communication, the message format is exactly the same, so you can easily switch between different integration methods.
| Env | Endpoint |
|---|---|
| Sandbox | https://gateway-stg.wonder.app/api/oms/b2b/open/terminal/async |
| Production | https://gateway.wonder.app/api/oms/b2b/open/terminal/async |
In cloud integration, if a transaction is voided or refunded, the payment terminal is not required to be online. The cloud will directly perform the relevant processing to support 24/7 operations on transactions.
Wired connections
Encrypted Method for HTTP-based integration
| Key | Remark | Example |
|---|---|---|
| x-p-business-id | get it from Business Information | c3cc84fe-b1c3-11ec-a3d9-42010aaa001d |
| x-device-sn | The Payment Device SN | PAX-A930-1171851001 |
| x-app-slug | Get it from Dashboard-Settings-Api Credential | 1wU7Au |
| x-app-key | Get it from Dashboard-Settings-Api Credential | ca82cb30-44af-a211-98c0403d1f9f |
| authorization | Get it from Dashboard-Settings-Api Credential | Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBfa2V5IjoiY2E4MmNiMzAtNDRhZ |
Preparation of Encryption Materials:
In your application, you are generating encryption materials using a Shared Key with the PBKDF2-HMAC-SHA1 algorithm. The parameters for the PBKDF2-HMAC-SHA1 algorithm are as follows:
- Salt: WMSaltV1
- Salt Length: 8
- Rounds: 4000
- Key Length: 80
Using the PBKDF2-HMAC-SHA1 algorithm, you will eventually obtain three results for HMAC KEY / CIPHER KEY / IV for subsequent encryption, following these rules:
- Bytes 0-31: HMAC KEY (32 bytes)
- Bytes 32-63: CIPHER KEY (32 bytes)
- Bytes 64-83: IV (16 bytes)
- Generate a random 16-byte IV.
Encrypt the message body using the AES256-CBC algorithm:
- Perform XOR operation between the IV generated in step #2 and the IV generated in step #1 (resulting in 'a').
- Encrypt the message body using the CIPHER KEY generated in step #1 and the IV ('a') obtained in step 3.1, resulting in ciphertext.
- Calculate the HASH of the original message using the HMAC KEY generated in step #1 and the SHA256 algorithm.
Finally, generate an encrypted message body and transmit this message.
Encrypted data flow
//Suppose the encryted map is {"a":1}
//PBKDF2NS
//hmacKey:
[165, 138, 241, 50, 162, 203, 20, 42, 15, 10, 129, 234, 91, 89, 216, 213, 54, 156, 17, 145, 252, 246, 97, 56, 79, 254, 159, 155, 195, 102, 221, 214]
//cipherKey:
[124, 16, 196, 209, 149, 179, 163, 231, 22, 222, 229, 9, 69, 253, 158, 112, 161, 10, 5, 57, 4, 59, 181, 224, 116, 159, 128, 19, 26, 176, 103, 124]
//iv:
[97, 99, 31, 10, 239, 170, 238, 34, 14, 249, 144, 174, 230, 229, 97, 15]
//make the randomly generated iv2 fixed to verify wether encryption requirement is met
[255, 88, 159, 70, 231, 201, 10, 60, 95, 140, 221, 100, 82, 152, 239, 115]
//XOR的 iv
[158, 59, 128, 76, 8, 99, 228, 30, 81, 117, 77, 202, 180, 125, 142, 124]
//encrypted
xd+O7byVDPt6YCm9FagPmQ==
//keyhex:
a58af132a2cb142a0f0a81ea5b59d8d5369c1191fcf661384ffe9f9bc366ddd6
//hash:
[68, 206, 161, 97, 35, 65, 25, 67, 195, 186, 190, 10, 218, 98, 97, 76, 138, 1, 72, 81, 164, 30, 124, 193, 250, 129, 2, 41, 48, 4, 111, 231]
return SaleToPoiResponse.fromJson({
"SecurityTrailer": {
"KeyVersion": "",
"KeyIdentifier": "",
"Hmac": hash.toBase64(),
"Nonce": iv_2.toBase64(),
"WonderCryptoVersion": 1
//Encryted
},
"WonderBlob": encrypted //Base64 encoding of the encrypted ciphertext
});
{
"SaleToPOIRequest": {
"MessageHeader": {}, // MessageHeader does not need to be encrypted
"SecurityTrailer": {
"KeyVersion": "",
"KeyIdentifier": "",
"Hmac": "", // HMAC KEY obtained by #1 is encoded in standard base64
"Nonce": "", // #2 generates IV
"WonderCryptoVersion": 1 // Encrypt
},
"WonderBlob": "" // Base64 encoding of the encrypted ciphertext
}
}
Sample Codes
Dart
- import thrid packages
- encrypt: 5.0.1
- pbkdf2ns: 0.0.2
class EncryptionUtils {
static const salt = 'WMSaltV1';
static const int rounds = 4000;
static const int keyLength = 80;
static KeyMaterial keyMaterial = KeyMaterial('123456');
static init(String passphrase) {
keyMaterial = KeyMaterial(passphrase);
}
static SaleToPoiResponse encryptWonderBlob(Map<String, dynamic>? map) {
if (map == null) return SaleToPoiResponse();
var str = json.encode(map);
//generate a random 16 bytes list
var iv_2 = List<int>.generate(16, (i) => Random.secure().nextInt(256));
//iv of XOR
var iv = List<int>.generate(16, (i) => keyMaterial.iv[i] ^ iv_2[i]);
//Encrpted response
final encrypter = Encrypter(
AES(Key(Uint8List.fromList(keyMaterial.cipherKey)), mode: AESMode.cbc));
final encrypted =
encrypter.encrypt(str, iv: IV(Uint8List.fromList(iv))).base64;
String keyhex = keyMaterial.hmacKey
.map((i) => i.toRadixString(16).padLeft(2, '0'))
.join("");
var hash = Hmac(sha256, utf8.encode(keyhex)).convert(str.codeUnits).bytes;
return SaleToPoiResponse.fromJson({
"SecurityTrailer": {
"KeyVersion": "",
"KeyIdentifier": "",
"Hmac": hash.toBase64(),
"Nonce": iv_2.toBase64(),
"WonderCryptoVersion": 1
//Encrypted
},
"WonderBlob": encrypted //Base64 encoding of the encrypted ciphertext
});
}
static decryptWonderBlob(String wonderBlob, SecurityTrailer securityTrailer) {
try {
var hmac_key = base64.decode(securityTrailer.hmac!);
var iv_2 = base64.decode(securityTrailer.nonce!);
var iv = List<int>.generate(16, (i) => keyMaterial.iv[i] ^ iv_2[i]);
final key = Key(Uint8List.fromList(keyMaterial.cipherKey));
final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
var result = encrypter.decrypt(Encrypted.fromBase64(wonderBlob),
iv: IV(Uint8List.fromList(iv)));
String keyhex = keyMaterial.hmacKey
.map((i) => i.toRadixString(16).padLeft(2, '0'))
.join("");
var hash =
Hmac(sha256, utf8.encode(keyhex)).convert(result.codeUnits).bytes;
if (hmac_key.toBase64() != hash.toBase64()) {
throw DecryptException('');
}
return result;
} catch (e) {
throw DecryptException('Decryption failure');
}
}
}
class KeyMaterial {
late List<int> hmacKey;
late List<int> cipherKey;
late List<int> iv;
KeyMaterial(String passphrase) {
PBKDF2NS gen = PBKDF2NS(hash: sha1);
List<int> key = gen.generateKey(passphrase, EncryptionUtils.salt,
EncryptionUtils.rounds, EncryptionUtils.keyLength);
hmacKey = key.sublist(0, 32);
cipherKey = key.sublist(32, 64);
iv = key.sublist(64, 80);
}
}
Golang
- import thrid packages
package encrypt
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/forgoer/openssl"
"golang.org/x/crypto/pbkdf2"
)
type SecretResp struct {
Data struct {
EnableEcr bool `json:"enable_ecr"`
EcrKey string `json:"ecr_key"`
} `json:"data"`
Code int `json:"code"`
Message string `json:"message"`
}
type KeyMaterial struct {
HmacKey []byte
CipherKey []byte
IV []byte
}
type SecurityTrailer struct {
KeyVersion string `json:"KeyVersion"`
KeyIdentifier string `json:"KeyIdentifier"`
Hmac string `json:"Hmac"`
Nonce string `json:"Nonce"`
WonderCryptoVersion int `json:"WonderCryptoVersion"`
}
type SaleToPoiResponse struct {
SecurityTrailer SecurityTrailer `json:"SecurityTrailer"`
WonderBlob string `json:"WonderBlob"`
WonderCryptoVersion int `json:"WonderCryptoVersion"`
}
type Encryption struct {
salt string
rounds int
keyLength int
}
func NewEncryption() *Encryption {
return &Encryption{
salt: "WMSaltV1",
rounds: 4000,
keyLength: 80,
}
}
func (e *Encryption) GenerateKeyMaterial(passphrase string) KeyMaterial {
var keyMaterial KeyMaterial
key := pbkdf2.Key([]byte(passphrase), []byte(e.salt), e.rounds, e.keyLength, sha1.New)
keyMaterial.HmacKey = key[:32]
keyMaterial.CipherKey = key[32:64]
keyMaterial.IV = key[64:80]
return keyMaterial
}
func (e *Encryption) EncryptWonderBlob(src interface{}, keyMaterial KeyMaterial) (*SaleToPoiResponse, error) {
if src == nil {
return nil, errors.New("")
}
plaintext, err := json.Marshal(src)
if err != nil {
return nil, err
}
//rand.Seed(time.Now().UnixNano())
//iv2 := make([]byte, 16)
//for i := range iv2 {
// iv2[i] = byte(rand.Intn(256))
//}
iv2 := []byte{255, 88, 159, 70, 231, 201, 10, 60, 95, 140, 221, 100, 82, 152, 239, 115}
iv := make([]byte, 16)
for i := range iv {
iv[i] = keyMaterial.IV[i] ^ iv2[i]
}
ciphertext, err := openssl.AesCBCEncrypt(plaintext, keyMaterial.CipherKey, iv, openssl.PKCS7_PADDING)
if err != nil {
return nil, err
}
fmt.Println(base64.StdEncoding.EncodeToString(ciphertext))
hash, err := calculateHash(plaintext, keyMaterial.HmacKey)
if err != nil {
return nil, err
}
fmt.Println(hash)
return &SaleToPoiResponse{
SecurityTrailer: SecurityTrailer{
Hmac: base64.StdEncoding.EncodeToString(hash),
Nonce: base64.StdEncoding.EncodeToString(iv2),
WonderCryptoVersion: 1,
},
WonderBlob: base64.StdEncoding.EncodeToString(ciphertext),
}, nil
}
func (e *Encryption) DecryptWonderBlob(ciphertext []byte, securityTrailer SecurityTrailer, keyMaterial KeyMaterial) ([]byte, error) {
iv2, err := base64.StdEncoding.DecodeString(securityTrailer.Nonce)
if err != nil {
return nil, err
}
iv := make([]byte, 16)
for i := range iv {
iv[i] = keyMaterial.IV[i] ^ iv2[i]
}
plaintext, err := openssl.AesCBCDecrypt(ciphertext, keyMaterial.CipherKey, iv, openssl.PKCS7_PADDING)
if err != nil {
return nil, err
}
fmt.Println(string(plaintext))
hash, err := calculateHash(plaintext, keyMaterial.HmacKey)
if err != nil {
return nil, err
}
if securityTrailer.Hmac != base64.StdEncoding.EncodeToString(hash) {
return nil, errors.New("HMAC verification failed")
}
return plaintext, nil
}
func calculateHash(message []byte, hmacKey []byte) ([]byte, error) {
var keyHex string
for _, b := range hmacKey {
keyHex += fmt.Sprintf("%02x", b)
}
mac := hmac.New(sha256.New, []byte(keyHex))
_, err := mac.Write(message)
if err != nil {
return nil, err
}
return mac.Sum(nil), nil
}
Encrypted method for RS232-based integration
API Structure
For HTTP-based integration
Our Terminal API communicates with the terminal using JSON messages. All requests and responses have the following message header-body structure:
- Message header: identifies the type of transaction, the terminal being used, and unique transaction identifiers.
- Body: a request or response object, depending on the type of transaction. For example, when you make a payment request this is a
PaymentRequestobject, and when you receive a payment response this is aPaymentResponseobject.
Requests
Each Terminal API request you make is contained in a SaletoPOIRequest object. In this, you need to provide a:
- Message Header. The Message Header defines metadata that is unrelated to transaction behavior, such as terminal device and protocol version.
- Body. Different transaction types have different formats for the Body and Response. You need to use PaymentRequest when creating a Payment.
| Field | Type | Comments |
|---|---|---|
| ProtocolVersion | String | Version number of the communication protocol. The latest version is 1.0 |
| MessageClass | Enum | It should all be Services in your integration with no specific requirement |
| MessageCategory | Enum | Transaction Type |
| MessageType | Enum | Only Request is supported at present |
| ServiceID | UUID | The unique ID for this message. |
| SaleID | String | The unique ID for transactions in your application |
| POIID | String | The unique ID of the terminal. Format: [Device Model]-[Serial Number] |
| BusinessID | String | Merchant’s Business ID |
{
"ProtocolVersion" : "1.0",
"MessageClass" : "Service",
"MessageCategory" : "Payment",
"MessageType" : "Request",
"ServiceID": "3d8294b3-012b-4c5c-bbae-fd53057f7733",
"SaleID" : "ba68ab30-986b-4819-b65f-ef79ec5bb33e",
"POIID" : "PAX920-123456789",
"BusinessID": "7e942219-d95d-4a31-bbc1-13a7c3bc0796"
}
Responses
Similar to Request, Response also includes Message Header and Body structures. The Message Header is consistent with the header you passed in the Request, and the Body varies with different transaction behaviors.
Management ECR Keys
a. Get the ECR Status and Secret Key
- GET
https://gateway.wonder.app/api/registry/terminal/open/secrets
b.Enable ECR
- PATCH
https://gateway.wonder.app/api/registry/terminal/open/secrets--data-raw'{ "enable_ecr":true }'
c.Reset ECR Secret Key
- GET
https://gateway.wonder.app/api/registry/terminal/open/secrets