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)
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
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:
key | value | Remark |
---|---|---|
ProtocolVersion | 1.0 | Agreement Version(Fixed to be “1.0") |
MessageClass | Service | Agreement(Fixed to be “Service") |
MessageCategory | Pair | Pair Payment TransactionStatus Reversal Void Abort Push -Six functions supported |
MessageType | Request | Fixed to be "Request" |
ServiceID | Suggest:uuid | It is used to distinguish each request and prevent repeated processing PostMan "{{$guid}}" |
POIID | Device SN | Eg:PAX-A930-1191851002 |
BusinessID | Business ID | eg: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
- can be null, if you want to create an order , should fill AppID too
- more details refer to https://developer.wonder.today/openapi/api_references/10-create-order
About “extra business data”
- can be null
- show a QRCode or BarCode at Receipt
- barcode type is "code39"
- like this
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
- can be null, if you want create a order,should fill AppID too
- more details refer to https://developer.wonder.today/openapi/api_references/10-create-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
- We recommend you use it paymentlink 的 Refund to replace it
// 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)
- We recommend you use it paymentlink Void to replace it
"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):
- 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 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
});