Skip to main content

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

info

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.

EnvEndpoint
Sandboxhttps://gateway-stg.wonder.app/api/oms/b2b/open/terminal/async
Productionhttps://gateway.wonder.app/api/oms/b2b/open/terminal/async
info

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

KeyRemarkExample
x-p-business-idget it from Business Informationc3cc84fe-b1c3-11ec-a3d9-42010aaa001d
x-device-snThe Payment Device SNPAX-A930-1171851001
x-app-slugGet it from Dashboard-Settings-Api Credential1wU7Au
x-app-keyGet it from Dashboard-Settings-Api Credentialca82cb30-44af-a211-98c0403d1f9f
authorizationGet it from Dashboard-Settings-Api CredentialBearer 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

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 PaymentRequest object, and when you receive a payment response this is a PaymentResponse object.

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.
FieldTypeComments
ProtocolVersionStringVersion number of the communication protocol. The latest version is 1.0
MessageClassEnumIt should all be Services in your integration with no specific requirement
MessageCategoryEnumTransaction Type
MessageTypeEnumOnly Request is supported at present
ServiceIDUUIDThe unique ID for this message.
SaleIDStringThe unique ID for transactions in your application
POIIDStringThe unique ID of the terminal. Format: [Device Model]-[Serial Number]
BusinessIDStringMerchant’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