Kiosk
Integrating External Apps with the Ecart Pay Kiosk
An external developer can integrate an Android app with the Ecartpay SK700 kiosk to charge a card and to use the printer, scanner, and camera, while keeping their own UI, login, and endpoints. The kiosk handles each request and returns the result to the app.
Your app and the kiosk are installed on the same SK700. Your app sends the request. The kiosk handles it and responds. Communication is an Android Intent with extras. There is no network call, socket, or pairing step.
Your UI, your login, your endpoints
Operates the hardware and runs the charge
The only device that reads the card
Receives result and status callback
Two mechanisms are used:
| Operation | How to invoke it |
|---|---|
| Charge, Print | With the UnifiedAPI jar, through transApi.startTrans(...) |
| Scan, Photo | With a dedicated Intent and startActivityForResult(...), with no library |
The PAX Open SDK has no messages for reading a code or taking a photo. Its scan messages are payment methods, not a read operation. Scan and photo use a contract defined by the kiosk
The contract always returns a result, on success and on error.
IMPORTANTCharge and print require
UnifiedAPI-2.00.00_20240730.jar, which you request from the Ecartpay team. The embedded IM30 must be linked to the merchant account before a charge can succeed. Camera permission is granted on the SK700 by the operator. Your app cannot grant it.
Prerequisites
- An SK700 with the
Ecartpay Kioskapp installed (com.pax.ecartpay.kiosk). - To charge: the embedded IM30 must be linked to the merchant account. If it is not, the charge returns
The payment terminal has no account linkedand the kiosk shows the linking QR. Request this from the Ecartpay team. It cannot be completed from your app. - For the camera: the first time, the operator must grant the permission on the SK700. Your app cannot grant it.
- The
UnifiedAPI-2.00.00_20240730.jarfile, requested from the Ecartpay team. Place it inapp/libs/and declare it:
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))- Declare the three actions in your manifest. From
targetSdk 30, Android filters which other apps yours can see. Without this,resolveActivity()returnsnulleven when the kiosk is installed.startTranschecks that before launching, so it returnsfalsewith no obvious cause. The SK700 runs Android 10 (API 29), where this filter does not apply yet. A newer device does require it:
<queries>
<intent>
<action android:name="android.pax.payment.entry" />
</intent>
<intent>
<action android:name="com.pax.ecartpay.kiosk.action.SCAN_CODE" />
</intent>
<intent>
<action android:name="com.pax.ecartpay.kiosk.action.CAPTURE_PHOTO" />
</intent>
</queries>- Nothing else to declare. Your app does not need permissions for any of the four operations. The kiosk operates the hardware with its own permissions.
minSdk 24is enough.
Charge
private val transApi = TransAPIFactory.createTransAPI()
fun charge(amountInCents: Long) {
val request = SaleMsg.Request().apply {
amount = amountInCents // 1500 = $15.00
currencyCode = "MXN" // optional, MXN by default
tipAmount = 0
packageName = "com.pax.ecartpay.kiosk"
}
// The context must be an Activity: the SDK uses it for startActivityForResult.
if (!transApi.startTrans(this, request)) {
// The kiosk is not installed, or the intent could not be resolved
}
}The response arrives in onActivityResult:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val response = transApi.onResult(requestCode, resultCode, data) ?: return
if (response.rspCode == SdkRspCode.SUCC) {
// approved: response.authCode, refNo, cardNo, amount, voucherNo…
val extras = response.extraBundle
val customerVoucher = extras?.getString("clientVoucher")
val merchantVoucher = extras?.getString("merchantVoucher")
val orderId = extras?.getString("orderId")
} else {
// declined or interrupted: response.rspMsg carries the reason, in English
}
}rspCode == SUCC is the proof of success. No other field needs to be inspected.
Values returned in extraBundle
extraBundle| Key | Contents |
|---|---|
clientVoucher | Customer receipt, ready to print |
merchantVoucher | Merchant receipt |
amountDisplay | Formatted amount, for example $15.00 MXN |
orderId | Order identifier |
enterMode | How the card was read |
terminalCode | Failures only: the original terminal code |
terminalMessage | Failures only: the terminal text, in the language shown to its operator. Use it when you want a local operator to see the same message that appeared on the reader screen |
| Store the voucher if you will offer printing. That value is what you send to the kiosk in the next step. |
Print
Send what you want to print, not a file path. Two forms are supported:
fun printReceipt(voucher: String) {
val request = PrintBitmapMsg.Request().apply {
bitmap = voucher // the voucher exactly as it arrived from the charge
packageName = "com.pax.ecartpay.kiosk"
}
transApi.startTrans(this, request)
}What you send in bitmap | What the kiosk does |
|---|---|
The clientVoucher or merchantVoucher from a charge | Renders it and prints it. Do not modify it. The kiosk resolves the format and the paper width |
| A Base64 image | Prints it as received |
| Plain text is not supported yet. Content that has neither voucher tags nor Base64 is returned as an invalid payload. If your use case needs plain text, request it from the Ecartpay team. | |
The response travels the same way as a charge: rspCode == SUCC when the paper comes out. Otherwise rspMsg says whether there is no printer or the content was not valid. |
Keep the size in mind. Everything travels inside the Intent, and Android limits the transaction to about 1 MB. A receipt is tens of KB. A full camera image does not fit.
Scan a Code
No library is required. The kiosk opens the laser scanner and returns what it reads:
private val SCAN_REQUEST = 200
fun scan() {
val scan = Intent("com.pax.ecartpay.kiosk.action.SCAN_CODE")
if (packageManager.resolveActivity(scan, 0) == null) {
// the kiosk is not installed
return
}
startActivityForResult(scan, SCAN_REQUEST)
}// in onActivityResult, when requestCode == SCAN_REQUEST
val scanned = data?.getStringExtra("com.pax.ecartpay.kiosk.extra.RESULT")
val error = data?.getStringExtra("com.pax.ecartpay.kiosk.extra.ERROR")
when {
resultCode == RESULT_OK && scanned != null -> { /* use the contents */ }
else -> { /* error explains why: no scanner, or nothing was read */ }
}Possible reasons are This terminal has no scanner and No code was read.
It always responds, even when it reads nothing. After 7 seconds it returns RESULT_CANCELED with the reason. Your app never stays waiting.
Capture a Photo
The photo does not travel in the Intent. It does not fit. The kiosk stores it and gives you a content:// URI you can read, plus a thumbnail for an immediate preview.
private val CAMERA_REQUEST = 300
fun capture() {
val capture = Intent("com.pax.ecartpay.kiosk.action.CAPTURE_PHOTO")
startActivityForResult(capture, CAMERA_REQUEST)
}// in onActivityResult, when requestCode == CAMERA_REQUEST
val uri = data?.getParcelableExtra<Uri>("com.pax.ecartpay.kiosk.extra.PHOTO_URI")
val thumbnail = data?.getParcelableExtra<Bitmap>("com.pax.ecartpay.kiosk.extra.THUMBNAIL")
val error = data?.getStringExtra("com.pax.ecartpay.kiosk.extra.ERROR")
if (resultCode == RESULT_OK && uri != null) {
imageView.setImageBitmap(thumbnail) // immediate preview
val photo = contentResolver.openInputStream(uri)?.use { // the full photo
BitmapFactory.decodeStream(it)
}
}Possible reasons are Camera permission was denied, The photo could not be taken, and The photo could not be stored.
The read permission lasts only as long as your app's task. If you store the URI and open it again the next day, you receive a
SecurityException. The file lives in the kiosk until the next capture, which replaces it. If you need to keep the photo, copy it when you receive it.
Response Codes
For charge and print, read response.rspCode:
| Code | rspMsg |
|---|---|
SUCC (0) | OK |
ERR_TRANS_CANCEL (-2) | Cancelled on the payment terminal |
ERR_TRANS_TIMEOUT (-1) | The payment terminal did not answer in time |
ERR_CONNECT_MPOS (-4) | The payment terminal is not connected |
ERR_TRANS_FAIL (-3) | The payment terminal has no account linked, The payment terminal has no internet connection, or The charge was declined |
On print, ERR_TRANS_FAIL can also carry Printer error: <code>, Payload is neither a voucher nor Base64, or Operation not supported. | |
When the decline originates on the payment terminal, its original text — in the language its operator sees — travels separately in the terminalMessage extra, together with its code in terminalCode. |
Contract Rules
- Every call returns a result. No operation stays silent. If something fails, the reason arrives. If
startTransreturnsfalse, the intent was not resolved. That is almost always because the kiosk is not installed, or because the manifest is missing<queries>. - The kiosk can be closed. The call starts it on its own. You do not need to open it or leave it in the background.
- The charge is performed by the IM30, not by the SK700. That is why some error messages say "the payment terminal": they refer to the embedded reader.
- The kiosk renders the voucher. Send it unchanged. If you reformat it, the receipt prints incorrectly.
- Always set
packageNameon Open SDK messages. That makes the kiosk handle the request instead of another payment app installed on the device.
What to Request from the Ecartpay Team
- The
UnifiedAPI-2.00.00_20240730.jarfile. - The SK700 IM30 linked to the merchant account that will receive the charges.
- The
Ecartpay Kioskapp installed on the device.
Updated about 9 hours ago