Skip to main content

ECR Payment Json

Send ECR Request to mPOS

Process:

a.Ensure that the terminal interface is the POS integration interface of QR code (open ECR through API, re-enter the app or refresh the status to enter this page)

1704943370817

b.Pair the terminal with "Pair" (the value of MessageCategory is "Pair"). After it is successfully paired, the page for Waiting Ecr Instructions is displayed

Refer to the encryption demo at the bottom****

Communication Methods Supported

OnlineAsync:

  • POS and mPOS can not be on the same LAN and are not limited by distance

  • No need to wait after the communication is initiated, and the query results can be polled later (or callback via url).

https://gateway.wonder.app/api/oms/b2b/open/terminal/async

OfflineAsync:

  • POS and mPOS are on the same LAN

  • Communication is faster and more stable

  • No need to wait after the communication is initiated, and the query results can be polled later (or callback via url).

172.16.91.29:8443/wonder-terminal/async

OfflineSync: Synchronous Communication under the same LAN

  • POS and mPOS are on the same LAN

  • Communication is faster and more stable

  • Easy to use

172.16.91.29:8443/wonder-terminal/sync


API Illustration

Currently Supported Functions

  • Pair
  • Payment
  • PreAuth
  • TransactionStatus
  • Reversal
  • Void
  • Abort
  • Push
  • SwitchBusiness

Request Format

 {
"SaleToPOIRequest": {
"MessageHeader": {
"ProtocolVersion": "1.0",
"MessageClass": "Service",
"MessageCategory": "Pair", //Payment. TransactionStatus,Reversal Abort,Pair,Void ,Push ,SwitchBusiness
"MessageType": "Request",
"ServiceID": "4e412543v1v14e3",//The random number generated by the POS terminal is used to identify the request
"POIID": "PAX-A930-1170270945",// The device sn, get it from the QR code page when binding the device
"BusinessID": "ff467f02-5b69-45f3-81aa-bffcca55fe8f" //Business id get it from binding thr QR code page
},
///------Here is the encrypted parameter
"SecurityTrailer": {
"KeyVersion": "1",
"KeyIdentifier": "",
"Hmac": "", // The HMAC KEY obtained by #1 is encoded in standard base64
"Nonce": "", // #2 generates IV
"WonderCryptoVersion": 0 // 1 Encryte 0 2. Don't encryte
},
///---------Here is the encryted content
"WonderBlob":"",



/// This is a special case
"PushRequest": {
"Action": "ECR_CANCEL" //ECR_CANCEL Cancel and then it will return to the page ECR is waiting for binding the QR code
},
///----------------------------------------------------------------
///The content below won't show up while encrypting

/// Switch to another store(Some special merchants need to support multi stores simutanously and it requires an API to switch)
"SwitchBusinessRequest":{
"BusinessID":"a5467f02-2914-5d1a-21bb-bffcca55fe94"
},
"TransactionStatusRequest": {
"MessageReference": {
"ServiceID": "59b90600-13cf-11ee-bab2-99e6a6ef5392",////POS query TransactionID(A/B/C) POS stored in the refund void and payment transactions
"MessageCategory": "Payment"
}
},
"ReversalRequest": {
"SaleData": {
"SaleToAcquirerData": "currency=HKD"
},
"OriginalPOITransaction": {
"POITransactionID": {
"TransactionID": "59b90600-13cf-11ee-bab2-99e6a6ef5049", //POS generate refund 的 uuid A
"OriginBRN": "3267887011708553217", //refund by this value(from payment response)
"ReferenceNumber":"8daf4dc0-6ad6-44b1-8256-a0f1549c0fa6"//Make sure that the value is the same as the ReferenceNumber of the PaymentRequest
}
},
"ReversedAmount": 2.0
},
"VoidRequest": {
"OriginalPOITransaction": {
"POITransactionID": {
"TransactionID": "{{$guid}}", //the uuid C of refund generated by POS
"OriginBRN": "3267887011708553217", //refund by this value(from payment response)
"ReferenceNumber":"8daf4dc0-6ad6-44b1-8256-a0f1549c0fa6"//Make sure that the value is the same as the ReferenceNumber of the PaymentRequest
}
}
},
"PaymentRequest": {
"SaleData": {
"SaleTransactionID": {
"TransactionID": "59b90600-13cf-11ee-bab2-99e6a6ef5392", ////POS generate the Payment 的uuid B
"TimeStamp": "2019-03-07T10:11:04+00:00",
"CallBackUrl":"https://www.baidu.com/transactionID=27908",//(eg.) // If a callback address is configured, it will be invoked to submit the content of the response using a post request The callbackurl of void or ReversalRequest will then call this address Details refer to (https://developer.wonder.today/payment_link/api_reference/#order-webhook-notification)
"ReferenceNumber":"8daf4dc0-6ad6-44b1-8256-a0f1549c0fa6",//Make sure that the id is unique, and that the ID is the value that ultimately marks the transaction
"AppKey":"ca82cb30-44af-4c1d-a211-98c0403d1f9f",//x-app-slug
"AppSlug":"1wU7Au"//x-app-key The above two values are used to encrypt the callback
"AppID": "865740e8-d81b-4951-9130-104756eeb25b"//If you want create a order , should fill it
}
},
"PaymentTransaction": {
"AmountsReq": {
"Currency": "HKD",
"RequestedAmount": "11.2"
}
},
"Order": { // can be null, if you want create a order,should fill AppID too
"charge_fee": "0.00",
"tips": "0.00",
"currency": "hkd",
"reference_number": "8daf4dc0-6ad6-44b1-8256-a0f1549c0fa6",
"due_date": "string",
"note": "string",
"line_items": [
{
"uuid": "string",
"purchasable_type": "RearTips",
"purchase_id": "string",
"price": "string",
"quantity": "string",
"total": "string",
"label": "string"
}
],
"callback_url": "string",
"redirect_url": "string"
}
},
"AbortRequest": {
"AbortReason": "MerchantAbort",
"MessageReference": {
"MessageCategory": "Payment",// Previously only Payment interrupts are supported

"ServiceID": "59b90600-13cf-11ee-bab2-99e6a6ef5392" //corresponding to TransactionID uuid B
}
}
}
}

MessageHeader:

keyvalueRemark
ProtocolVersion1.0Agreement Version(Fixed to be “1.0")
MessageClassServiceAgreement(Fixed to be “Service")
MessageCategoryPairPair
Payment
TransactionStatus
Reversal
Void
Abort
Push
-Six functions supported
MessageTypeRequestFixed to be  "Request"
ServiceIDSuggest:uuidIt is used to distinguish each request and prevent repeated processing
PostMan "{{$guid}}"
POIIDDevice SNEg:PAX-A930-1191851002
BusinessIDBusiness IDeg:ff467f02-5b69-45f3-81aa-bffcca55fe21
You can obtain it on the binding QR code page

WonderBlob:

WonderBlob is an encrypted string, and the source data corresponds to the data in **Request. Developers need to decrypt for their own use

PushRequest

PushRequest is a special case and it doesn't need to be encryted. If it is needed for use, need to reserve it into the request structure.


Response Result Returning Format

{
"MessageHeader": {
"BusinessID": "ff467f02-5b69-45f3-81aa-bffcca55fe8f",
"MessageCategory": "TransactionStatus",
"MessageClass": "Service",
"MessageType": "Response",
"POIID": "PAX-A930-1170270945",
"ProtocolVersion": "1.0",
"ServiceID": "31fd1gg4fg6211"
},
"SecurityTrailer": {
"KeyVersion": "", //Encryted Version
"KeyIdentifier": "", //
"Hmac": "", // The HMAC KEY obtained by #1 is encoded in standard base64
"Nonce": "", // #2 generates IV,
"WonderCryptoVersion": 0 // 1 encipher
},
"WonderBlob": null,


///The following also does not appear when encryption is enabled

"AbortResponse": null,
"PairResponse": null,
"PaymentResponse": null,
"PushResponse": null,
"ReversalResponse": null,
"SecurityTrailer": null,
"SwitchBusinessResponse":null,
"TransactionStatusResponse": {
"RepeatedMessageResponse": {
"MessageHeader": {
"BusinessID": "ff467f02-5b69-45f3-81aa-bffcca55fe8f",
"MessageCategory": "TransactionStatus",
"MessageClass": "Service",
"MessageType": "Response",
"POIID": "PAX-A930-1170270945",
"ProtocolVersion": "1.0",
"ServiceID": "31fd1gg4fg6211"
},
"RepeatedResponseMessageBody": {
"PaymentResponse": null,
"ReversalResponse": {
"POIData": null,
"PaymentReceipt": null,
"Response": {
"AdditionalResponse": "UUID Conflict",
"ErrorCondition": null,
"Result": "Failure"
},
"ReversedAmount": null
}
}
},
"Response": {
"AdditionalResponse": null,
"ErrorCondition": null,
"Result": "Success"
}
}
}
  • WonderBlob is an encrypted string, and the source data corresponds to the data in **Response. You can use the type of MessageCategory to find the corresponding ****Response to decrypt it.
  • PushResponse will also keep the structure but will not be encrypted
  • "MessageType": Response corresponds to Request
  • Every Response corresponds to Request
  • Response returns in two formats
    • Sync. When these two requests come, the service does not return immediately. Instead, it wait for the operation to finish and then return the result. TransactionStatus(querying payment status) is not supported in this mode.
    • Async .Under this model, all request return immediatly (Payment,Reversal)but it will not return the actual payment result, instead, it will receive a structure body "Request Received". The real payment result requires the client to query the result through TransactionStatus in the mode of rotation training.

Payment

"PaymentRequest": {
"SaleData": {
"SaleTransactionID": {
"TransactionID": "59b90600-13cf-11ee-bab2-99e6a6ef5392", ////POS generate the Payment 的uuid B
"TimeStamp": "2019-03-07T10:11:04+00:00",
"CallBackUrl":"https://www.baidu.com/transactionID=27908",//(eg.) // If a callback address is configured, it will be invoked to submit the content of the response using a post request The callbackurl of void or ReversalRequest will then call this address Details refer to (https://developer.wonder.today/payment_link/api_reference/#order-webhook-notification)
"ReferenceNumber":"8daf4dc0-6ad6-44b1-8256-a0f1549c0fa6",//Make sure that the id is unique, and that the ID is the value that ultimately marks the transaction
"AppKey":"ca82cb30-44af-4c1d-a211-98c0403d1f9f",//x-app-slug
"AppSlug":"1wU7Au",//x-app-key The above two values are used to encrypt the callback
"PaymentType":"Optional", // Optional: "Card" "Wallets" "Octopus" "QRCodePresent" "Optional" or "Optional" default:"Optional"
"AppID": "865740e8-d81b-4951-9130-104756eeb25b",// If you want create a order , should fill it
//---------extra business data can be null --------
"CodeType":"qrcode" //can be null eg: "qrcode" or "barcode" (code39)
"CodeData":"https://www.google.com/transactionID=27908" // qrcode or barcode data et.123456789
"PrinterMsg":"MCC ID: 84930838505\n" // More AD Description
}
},
"PaymentTransaction": {
"AmountsReq": {
"Currency": "HKD",
"RequestedAmount": "10.2"//String Type two decimal / rounding off
}
},
"Order": { // can be null, if you want create a order,should fill AppID too
"charge_fee": "0.00",
"tips": "0.00",
"currency": "hkd",
"reference_number": "8daf4dc0-6ad6-44b1-8256-a0f1549c0fa6",
"due_date": "string",
"note": "string",
"line_items": [
{
"uuid": "string",
"purchasable_type": "RearTips",
"purchase_id": "string",
"price": "string",
"quantity": "string",
"total": "string",
"label": "string"
}
],
"callback_url": "string",
"redirect_url": "string"
}
}

//async return immediately
"PaymentResponse": {
"POIData": null,
"PaymentReceipt": null,
"Response": {
"AdditionalResponse": null,
"ErrorCondition": null,
"Result": "Success"
}
}


//sync 1.Abnormal aborted
"PaymentResponse": {
"POIData": {
"POITransactionID": {
"TransactionID": "59b90600-13cf-11ee-bab2-99e6a6ef5392"
}
},
"Response": {
"Result": "Failure",
"ErrorCondition": "Aborted",
"AdditionalResponse": null
},
"PaymentReceipt": null
}
//sync 2.Normal case aborted PaymentReceipt the data is used for printing
"PaymentResponse": {
"POIData": {
"POITransactionID": {
"TransactionID": "59b90600-13cf-11ee-bab2-99e6a6ef5392"
}
},
"Response": {
"Result": "Success",
"ErrorCondition": null,
"AdditionalResponse": null
},
"PaymentReceipt": {
"amount": "10.20",
"tipsAmount": "0.00",
"currency": "HKD",
"payment_method": "fps",
"transaction_state": "success",
"date_time": "2023-06-30T09:08:52+00:00",
"merchant_id": "3333",
"terminal_id": null,
"rrn": "3263492852830699521",
"brn": "3263492852495159296",
"acquirer_type": "fps",
"receiptData": {
"reference_number": "W-158637380062",
"sale_transactions": [
{
"success": true,
"amount": "10.2",
"payment_method": 22,
"payment": "FPS",
"payment_data": {
"credit_card_type": "fps",
"new_gateway_txn_id": "3263492852495159296",
"rrn": "3263492852830699521",
"merchant_id": "3333",
"terminal_id": "",
"created_at": "2023-06-30T09:08:52+00:00",
"reference_id": "3263492852495159296",
"auth_code": "3263492852830699522"
}
}
],
"initial_total": "10.2",
"initial_tips": null,
"subtotal": "10.2"
}
}
}

About PaymentType:

  • can be null, if null, then the PaymentType will be "Optional"
  • "Card": use Card payment like: "Visa", "MasterCard", "JCB", "American Express", "Diners Club", "Discover", "Union Pay", "Apple Pay","Google Pay","Huawei Pay","Samsung Pay"
  • "Wallets": use Wallets payment like: "Alipay", "WechatPay", "Xpay","Other" et. need client show their QRCode by smartphone
  • "Octopus": use Octopus payment like: "Octopus"
  • "QRCodePresent": use QRCodePresent payment like: "FPS" et. need client scan the QRCode by smartphone
  • "Optional": No Specified Payment Type. The above modes need to be specified manually
  • before use the above modes, need to check the merchant's payment method support that

About Order

About “extra business data”

  • can be null
  • show a QRCode or BarCode at Receipt
  • barcode type is "code39"
  • like this img_1.png

Preauth

"PreauthRequest": {
"SaleData": {
"SaleTransactionID": {
"TransactionID": "59b90600-13cf-11ee-bab2-99e6a6ef5392", ////POS generates the uuid of this Pre-auth
"TimeStamp": "2019-03-07T10:11:04+00:00",
"ReferenceNumber":"8daf4dc0-6ad6-44b1-8256-a0f1549c0fa6",//Please make sure it is unique, this id is the value ultimately marks the transaction
"CallBackUrl":"https://www.baidu.com/transactionID=27908",//(for example) //can be null, if has callback address, then it will go to this address, the response is submitted using a post request, and a void or ReversalRequest callbackurl will call this address, details refer to (https://developer.wonder.today/payment_link/api_reference/#order-webhook-notification)
"AppKey":"ca82cb30-44af-4c1d-a211-98c0403d1f9f",//x-app-slug
"AppSlug":"1wU7Au"//x-app-key Above two values are used for callback encryption
"AppID": "865740e8-d81b-4951-9130-104756eeb25b"//If you want create a order , should fill it
}
},
"PaymentTransaction": {
"AmountsReq": {
"Currency": "HKD",
"RequestedAmount": "10.2"//String type two decimal /will do round up
}
},
"Order": { // can be null, if you want create a order,should fill AppID too
"charge_fee": "0.00",
"tips": "0.00",
"currency": "hkd",
"reference_number": "8daf4dc0-6ad6-44b1-8256-a0f1549c0fa6",
"due_date": "string",
"note": "string",
"line_items": [
{
"uuid": "string",
"purchasable_type": "RearTips",
"purchase_id": "string",
"price": "string",
"quantity": "string",
"total": "string",
"label": "string"
}
],
"callback_url": "string",
"redirect_url": "string"
}
}

//async ruturn immediately
"PreauthResponse": {
"Response": {
"AdditionalResponse": null,
"ErrorCondition": null,
"Result": "Success"
}
}


//sync 1.abnormal termination
"PreauthResponse": {
"POIData": {
"POITransactionID": {
"TransactionID": "59b90600-13cf-11ee-bab2-99e6a6ef5392"
}
},
"Response": {
"Result": "Failure",
"ErrorCondition": "Aborted",
"AdditionalResponse": null
}
}
//sync 2.abnormal termination
"PreauthResponse": {
"POIData": {
"POITransactionID": {
"TransactionID": "59b90600-13cf-11ee-bab2-99e6a6ef5392"
}
},
"Response": {
"Result": "Success",
"ErrorCondition": null,
"AdditionalResponse": null
},
"PaymentReceipt": {
"amount": "10.20",
"tipsAmount": "0.00",
"currency": "HKD",
"payment_method": "fps",
"transaction_state": "success",
"date_time": "2023-06-30T09:08:52+00:00",
"merchant_id": "3333",
"terminal_id": null,
"rrn": "3263492852830699521",
"brn": "3263492852495159296",
"acquirer_type": "fps",
"receiptData": {
"reference_number": "W-158637380062",
"sale_transactions": [
{
"success": true,
"amount": "10.2",
"payment_method": 22,
"payment": "FPS",
"payment_data": {
"credit_card_type": "fps",
"new_gateway_txn_id": "3263492852495159296",
"rrn": "3263492852830699521",
"merchant_id": "3333",
"terminal_id": "",
"created_at": "2023-06-30T09:08:52+00:00",
"reference_id": "3263492852495159296",
"auth_code": "3263492852830699522"
}
}
],
"initial_total": "10.2",
"initial_tips": null,
"subtotal": "10.2"
}
}
}

Using Preauth of paymentlink for capture operation

About Order

Abort

       "AbortRequest": {
"AbortReason": "MerchantAbort",
"MessageReference": {
"MessageCategory": "Payment", // Or Preauth
"ServiceID": "59b90600-13cf-11ee-bab2-99e6a6ef5392" //corresponding to TransactionID
}
}

  • AbortReason: can be empty
  • MessageCategory: Support Payment/Preauth
  • ServiceID: TransactionID uuid of PaymentRequest/PreauthRequest

Reversal

// Refund request can do paritial refund
"ReversalRequest": {
"SaleData": {
"SaleToAcquirerData": "currency=HKD" //Currency
},
"OriginalPOITransaction": {
"OriginalPOITransaction": {
"POITransactionID": {
"TransactionID": "59b90600-13cf-11ee-bab2-99e6a6ef5049", //POS generate refund 的 uuid B
"OriginBRN": "3267887011708553217", //refund by this value(from payment response)
"ReferenceNumber":"8daf4dc0-6ad6-44b1-8256-a0f1549c0fa6"//Make sure that the value is the same as the ReferenceNumber of the PaymentRequest
}
},
},
"ReversedAmount": 2.0 //double type two decimals /will rounding off
}

//async return immediately
"ReversalResponse": {
"POIData": null,
"PaymentReceipt": null,
"Response": {
"AdditionalResponse": null,
"ErrorCondition": null,
"Result": "Success"
},
"ReversedAmount": null
}

Void( The same as Reversal)

"VoidRequest": {
"OriginalPOITransaction": {
"POITransactionID": {
"TransactionID": "{{$guid}}", //the uuid C of refund generated by POS
"OriginBRN": "3267887011708553217", //refund by this value(from payment response)
"ReferenceNumber":"8daf4dc0-6ad6-44b1-8256-a0f1549c0fa6"//Make sure that the value is the same as the ReferenceNumber of the PaymentRequest
}
}
},

TransactionStatus(Payment/Void/Reversal/Preauth)

//Query Transaction Status
"TransactionStatusRequest": {
"MessageReference": {
"ServiceID": "59b90600-13cf-11ee-bab2-99e6a6ef5392",////POS Query the TransactionID stored in refund/payment/void/preauth transaction request (A/B/C)
"MessageCategory": "Payment" //currently list as Reversal/Payment/Void
}
}
//waiting
"TransactionStatusResponse": {
"RepeatedMessageResponse": null,
"Response": {
"AdditionalResponse": "TransactionStatusResponse is inProgress",
"ErrorCondition": "InProgress",
"Result": "Failure"
}
},
//Cancel
"TransactionStatusResponse": {
"RepeatedMessageResponse": {
"MessageHeader": {
"BusinessID": "ff467f02-5b69-45f3-81aa-bffcca55fe8f",
"MessageCategory": "TransactionStatus",
"MessageClass": "Service",
"MessageType": "Response",
"POIID": "PAX-A930-1170270945",
"ProtocolVersion": "1.0",
"ServiceID": "31fdrg23gg45人d34f4fg6211"
},
"RepeatedResponseMessageBody": {
"PaymentResponse": {
"POIData": {
"POITransactionID": {
"TransactionID": "59b90600-13cf-11ee-bab2-99e6a6ef5392"
}
},
"PaymentReceipt": null,
"Response": {
"AdditionalResponse": null,
"ErrorCondition": "Aborted",
"Result": "Failure"
}
},
"ReversalResponse": null
}
},
"Response": {
"AdditionalResponse": null,
"ErrorCondition": null,
"Result": "Success"
}
}
//Successful Transaction
"TransactionStatusResponse": {
"RepeatedMessageResponse": {
"MessageHeader": {
"BusinessID": "ff467f02-5b69-45f3-81aa-bffcca55fe8f",
"MessageCategory": "TransactionStatus",
"MessageClass": "Service",
"MessageType": "Response",
"POIID": "PAX-A930-1170270945",
"ProtocolVersion": "1.0",
"ServiceID": "31fdrg23gg4f42a5人d34f4fg6211"
},
"RepeatedResponseMessageBody": {
"PaymentResponse": {
"POIData": {
"POITransactionID": {
"TransactionID": "59b90600-13cf-11ee-bab2-99e6a6ef5392"
}
},
"PaymentReceipt": {
"acquirer_type": "fps",
"amount": "10.20",
"brn": "3263590179977302016",
"currency": "HKD",
"date_time": "2023-06-30T10:45:33+00:00",
"merchant_id": "3333",
"payment_method": "fps",
"receiptData": {
"initial_tips": null,
"initial_total": "10.2",
"reference_number": "W-142631801078",
"sale_transactions": [
{
"amount": "10.2",
"payment": "FPS",
"payment_data": {
"auth_code": "3263590180312842242",
"created_at": "2023-06-30T10:45:33+00:00",
"credit_card_type": "fps",
"merchant_id": "3333",
"new_gateway_txn_id": "3263590179977302016",
"reference_id": "3263590179977302016",
"rrn": "3263590180312842241",
"terminal_id": ""
},
"payment_method": 22,
"success": true
}
],
"subtotal": "10.2"
},
"rrn": "3263590180312842241",
"terminal_id": null,
"tipsAmount": "0.00",
"transaction_state": "success"
},
"Response": {
"AdditionalResponse": null,
"ErrorCondition": null,
"Result": "Success"
}
},
"ReversalResponse": null
}
},
"Response": {
"AdditionalResponse": null,
"ErrorCondition": null,
"Result": "Success"
}
}

The Result of Response are “Success” and “Failure”

ErrorCondition:

  • InProgress(The task is underworking)
  • Aborted(The task is cancelled)
  • NotFound(Can not find the transaction)
  • UnKnown(Unknown error)

AdditionalResponse: Normally it has no content. If any, this is for reminding the developer.

TransactionStatus(Reversal)

"TransactionStatusRequest": {
"MessageReference": {
"ServiceID": "59b90600-13cf-11ee-bab2-99e6a6ef5941",//// POS Query TransactionID(A/B/C) stored in refund/void/payment transaction request
"MessageCategory": "Reversal" //Currently list as Reversal/Payment/Void
}
}

//query the result
"TransactionStatusResponse": {
"RepeatedMessageResponse": {
"MessageHeader": {
"BusinessID": "ff467f02-5b69-45f3-81aa-bffcca55fe8f",
"MessageCategory": "TransactionStatus",
"MessageClass": "Service",
"MessageType": "Response",
"POIID": "PAX-A930-1170270945",
"ProtocolVersion": "1.0",
"ServiceID": "31fdrg234g4g4f42a5人d34f4fg6211"
},
"RepeatedResponseMessageBody": {
"PaymentResponse": null,
"ReversalResponse": {
"POIData": {
"POITransactionID": {
"OriginBRN": null,
"TimeStamp": "2023-06-30T18:47:12.686525",
"TransactionID": "59b90600-13cf-11ee-bab2-99e6a6ef5941"
}
},
"PaymentReceipt": {
"acquirer_auth_code": "3263590180312842242",
"acquirer_rrn": "3263590180312842241",
"acquirer_settlement_at": null,
"adjust_times": 0,
"amount": "10.2",
"closed_at": null,
"consumer_identify": "",
"created_at": "2023-06-30T10:45:33.38Z",
"credit_card_fall_back": false,
"currency": "HKD",
"deleted_at": null,
"fee_amount": "0.1",
"fee_currency": "HKD",
"id": "3263590179977302016",
"initial_total": 0,
"mid": "3333",
"oms_transaction_id": 75780958,
"order": {
"additional_documents": null,
"auth_code": "bzoRKzdmKypmwPQ",
"business_name": "Bindo Labs Limited Test 2",
"confidence_store": null,
"correspondence_state": "paid",
"created_at": "2023-06-30T10:45:33Z",
"created_by": "Wonder POS",
"currency": "HKD",
"custom_attributes": null,
"customer_country_code": null,
"customer_dial_code": null,
"customer_email": null,
"customer_linked_source_type": null,
"customer_name": null,
"customer_phone": null,
"customer_reference": null,
"delivery_date": null,
"discount_total": 0,
"due_date": "2023-07-01 23:59:59",
"fail_transactions": [],
"files": null,
"from": 11,
"id": 15619648,
"initial_normal_tax": 0,
"initial_tax": 0,
"initial_tips": 0,
"initial_total": 10.2,
"inventory_status": null,
"line_items": [
{
"discount_total": 0,
"id": 18096861,
"image_url": "",
"label": "Charge",
"price": 10.2,
"purchasable_type": "Charge",
"purchase_id": null,
"quantity": 1,
"total": 10.2
}
],
"note": null,
"number": "202306301845331287548306",
"oms_delivery_note": {
"address1": null,
"address2": null,
"city": null,
"country": null,
"email": null,
"name": null,
"note": null,
"phone": null
},
"paid_total": "10.2",
"payment_link_url": "https://s-stg.wonder.app/oLTqQ",
"reference_number": "W-142631801078",
"refund_transactions": [],
"sale_transactions": [
{
"3ds": {
"enrolled": "",
"redirect_url": ""
},
"acquirer_response_body": "",
"allow_refund": false,
"allow_void": true,
"amount": 10.2,
"created_at": "2023-06-30T10:45:34Z",
"currency": "HKD",
"extra": {
"credit_card_type": null,
"first_6_digits": null,
"holder_name": null,
"last_4_digits": null,
"number": null
},
"from": 16,
"id": 75780958,
"is_pending": false,
"note": null,
"payment": "HKFPS",
"payment_data": {
"acquirer_name": "fake",
"acquirer_type": "fps",
"auth_code": "3263590180312842242",
"batch_no": "",
"brn": "75780958",
"device_id": "PAX-A930-1170270945",
"merchant_id": "3333",
"new_gateway_txn_id": "3263590179977302016",
"payment_inst": "",
"payment_method": "fps",
"rrn": "3263590180312842241",
"trace_no": "",
"transaction_state": "success"
},
"payment_method": 22,
"reference_id": "3263590179977302016",
"refunded_amount": 0,
"success": true,
"tips_amount": null,
"voided_at": null
}
],
"sale_type": "invoice_sale",
"signatures": [],
"state": "completed",
"subtotal": 10.2,
"surcharge_fee": 0,
"type": "Invoice",
"unpaid_total": "0",
"updated_at": "2023-06-30T10:45:34Z",
"void_line_items": [],
"void_transactions": []
},
"order_info": {
"customer_reference": null,
"reference_number": "W-142631801078"
},
"order_num": "202306301845331287548306",
"org_txn_id": "0",
"p_business_id": "ff467f02-5b69-45f3-81aa-bffcca55fe8f",
"payment_entry_type": "merchant_presented_qr_code",
"payment_method": "fps",
"payment_type": "sale",
"remark": "",
"settlement_amount": "10.1",
"settlement_currency": "HKD",
"signature": "",
"store_id": "538047",
"tag_length_values": null,
"tid": "",
"tid_batch_num": 0,
"tid_trace_num": 0,
"tipsAmount": "0",
"tipsTransaction": null,
"total_amount": "10.2",
"transacted_on": "0001-01-01T00:00:00Z",
"updated_at": "2023-06-30T10:45:33.905Z",
"void_at": null
},
"Response": {
"AdditionalResponse": "Success",
"ErrorCondition": null,
"Result": "Success"
},
"ReversedAmount": 2
}
}
},
"Response": {
"AdditionalResponse": null,
"ErrorCondition": null,
"Result": "Success"
}
}

SwitchBusiness

       /// switch to another business(some merchants havw special requirement that need to support multi stores, need to switch from API level)
"SwitchBusinessRequest": {
"BusinessID": "a5467f02-2914-5d1a-21bb-bffcca55fe94"
}
/// switch to another business(some merchants havw special requirement that need to support multi stores, need to switch from API level)

"SwitchBusinessResponse": {
"Response": {
"Result":"Success",
"AdditionalResponse":null, //"String"
"ErrorCondition":null//"String"
}

}


Attentions

  • If you need to use SwitchBusiness, call SwitchBusiness every time you call another API
  • all the slug/key/authorization/businessId both use store data with device bind(Except PaymentRequest)
  • We recommend that AppKey/AppSlug in PaymentRequest use the value corresponding to the (A/B/C/D) store

Encrypted Code(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);
}
}

Encrypted Code(go):

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 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
});