This repository describes how to integrate a third-party Android application with the MultiSafepay Pay App using Android Intents (App-to-App communication).
The Pay App supports two execution environments:
- SmartPOS → Devices with a payment kernel (most Sunmi terminals)
- SoftPOS (Tap to Pay) → Devices without a payment kernel, using the device's own NFC reader — this covers pure Android/COTS (commercial off-the-shelf) hardware, but also certain Sunmi models that ship without kernel support (e.g. the L3)
Both use App-to-App communication via Android Intents, but they are launched and called back differently — see Selecting the Pay App, Callback Handling, and SoftPOS Integration.
Note: "Sunmi" is not synonymous with "has a kernel." Some Sunmi models (e.g. the L3) don't have payment kernel support and must use SoftPOS instead of SmartPOS.
com.multisafepay.pos.sunmi→ SmartPOS (kernel devices)com.phonepos.mspsoftposapp→ SoftPOS / Tap to Pay (non-kernel devices)
The manufacturer alone doesn't tell you whether a device has a payment kernel — some Sunmi models (e.g. the L3) don't. Use manufacturer as a coarse pre-filter, but gate the actual decision on whether the SmartPOS Pay App is installed:
private boolean isSunmiDevice() {
return Build.MANUFACTURER != null && Build.MANUFACTURER.equalsIgnoreCase("sunmi");
}
private boolean isPackageInstalled(String packageName) {
try {
getPackageManager().getPackageInfo(packageName, 0);
return true;
} catch (Exception e) {
return false;
}
}- If the device is Sunmi and
com.multisafepay.pos.sunmiis installed → launch SmartPOS directly (see Payment Flows). - Otherwise (non-Sunmi hardware, or a Sunmi model without kernel support / without the SmartPOS Pay App installed) → launch SoftPOS (see SoftPOS Integration).
The two Pay Apps are not interchangeable drop-in replacements for each other: SmartPOS is launched with getLaunchIntentForPackage + setClassName, while SoftPOS uses an explicit action and component. Treat them as two distinct integrations sharing the same general App-to-App model.
<manifest>
<queries>
<package android:name="com.multisafepay.pos.sunmi" />
<package android:name="com.phonepos.mspsoftposapp" />
</queries>
</manifest>SmartPOS reports the result back to your app through onNewIntent. SoftPOS reports it through onActivityResult instead — see below.
@Override
protected void onNewIntent(Intent intent) {
processMSPMiddlewareResponse(intent);
super.onNewIntent(intent);
}
private void processMSPMiddlewareResponse(@NonNull Intent intent) {
if (intent.hasExtra("status")) {
int status = intent.getIntExtra("status", 0);
String message = intent.getStringExtra("message");
handleMiddlewareCallback(status, message);
}
}
private void handleMiddlewareCallback(int status, String message) {
switch (status) {
case 875:
receivedCallbackIntent("EXCEPTION " + message);
break;
case 471:
receivedCallbackIntent("COMPLETED " + message);
break;
case 17:
receivedCallbackIntent("CANCELLED " + message);
break;
case 88:
receivedCallbackIntent("DECLINED " + message);
break;
}
}
private void receivedCallbackIntent(String message) {
Intent intent = new Intent(this, PaymentActivity.class);
intent.putExtra("message", message);
intent.putExtra("description", "pass data to this Activity");
startActivity(intent);
}SoftPOS is launched with startActivityForResult and never calls onNewIntent — it returns its result directly to onActivityResult:
private static final int REQUEST_CODE_SOFTPOS = 1001;
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_SOFTPOS && resultCode == RESULT_OK && data != null) {
String status = data.getStringExtra("result_status"); // "success" | "cancelled" | "declined"
String message = data.getStringExtra("message");
String description = data.getStringExtra("description");
// update UI / clear cart based on status
}
}The flows below use the SmartPOS (Sunmi) launch pattern —
getLaunchIntentForPackage+setClassName+startActivity. For SoftPOS, see SoftPOS Integration, which uses a different Intent action/component andstartActivityForResult.
JSONArray jsonArray = new JSONArray();
try {
JSONObject item1 = new JSONObject();
item1.put("name", "Product 1");
item1.put("unit_price", "0.10");
item1.put("quantity", "1");
item1.put("merchant_item_id", "749857");
item1.put("tax", "3.90");
JSONObject item2 = new JSONObject();
item2.put("name", "Product 2");
item2.put("unit_price", "0.20");
item2.put("quantity", "1");
item2.put("merchant_item_id", "749857");
item2.put("tax", "1.40");
jsonArray.put(item1);
jsonArray.put(item2);
} catch (JSONException e) {
e.printStackTrace();
}String packageName = "com.multisafepay.pos.sunmi";
Intent intent = getPackageManager().getLaunchIntentForPackage(packageName);
if (intent != null) {
intent.setClassName(packageName,
"com.multisafepay.pos.middleware.IntentActivity");
// amount must be long and expressed in minor units (for example: cents)
long amountInCents = amount;
intent.putExtra("items", jsonArray.toString());
intent.putExtra("order_id", getOrderId());
intent.putExtra("order_description", "info about the order");
intent.putExtra("currency", "EUR");
intent.putExtra("amount", amountInCents);
intent.putExtra("package_name", getPackageName());
startActivity(intent);
}JSONArray jsonArray = new JSONArray();
try {
JSONObject socks = new JSONObject();
socks.put("name", "Socks");
socks.put("description", "One pair of black socks");
socks.put("merchant_item_id", "001-M");
socks.put("unit_price", 0.105785124);
socks.put("quantity", 3);
socks.put("tax_table_selector", "21_percent");
JSONObject shipping = new JSONObject();
shipping.put("name", "Shipping");
shipping.put("description", "Domestic shipping (zone 1)");
shipping.put("merchant_item_id", "msp-shipping");
shipping.put("unit_price", 0.15);
shipping.put("quantity", 1);
jsonArray.put(socks);
jsonArray.put(shipping);
} catch (JSONException e) {
e.printStackTrace();
}String packageName = "com.multisafepay.pos.sunmi";
Intent intent = getPackageManager().getLaunchIntentForPackage(packageName);
if (intent != null) {
intent.setClassName(packageName,
"com.multisafepay.pos.middleware.IntentActivity");
setCheckoutOptions(intent);
// amount must be long and expressed in minor units (for example: cents)
long amountInCents = amount;
intent.putExtra("items", jsonArray.toString());
intent.putExtra("order_id", getOrderId());
intent.putExtra("description", "info about the order");
intent.putExtra("currency", "EUR");
intent.putExtra("amount", amountInCents);
intent.putExtra("reference", "Ref-" + System.currentTimeMillis());
intent.putExtra("auto_close", false);
intent.putExtra("package_name", getPackageName());
startActivity(intent);
}private void setCheckoutOptions(Intent intent) {
try {
JSONObject checkoutOptions = new JSONObject();
checkoutOptions.put("validate_cart", true);
JSONObject taxTables = new JSONObject();
checkoutOptions.put("tax_tables", taxTables);
JSONObject defaultTaxTable = new JSONObject();
defaultTaxTable.put("rate", 0);
taxTables.put("default", defaultTaxTable);
intent.putExtra("checkout_options", checkoutOptions.toString());
} catch (JSONException e) {
e.printStackTrace();
}
}private void sendRefundIntent(long amountInCents) {
String packageName = "com.multisafepay.pos.sunmi";
Intent intent = getPackageManager().getLaunchIntentForPackage(packageName);
if (intent != null) {
intent.setClassName(packageName, "com.multisafepay.pos.middleware.IntentActivity");
intent.putExtra("order_id", "REFUND_" + System.currentTimeMillis());
intent.putExtra("amount", amountInCents);
intent.putExtra("refund", true);
intent.putExtra("package_name", getPackageName());
startActivity(intent);
}
}Note: Unreferenced refunds are currently only supported through the SmartPOS (Sunmi) Pay App. If the device isn't a Sunmi terminal, or the Sunmi Pay App isn't installed, hide or disable the refund action — SoftPOS does not support refunds yet.
com.phonepos.mspsoftposapp is MultiSafepay's SoftPOS (Tap to Pay) Pay App — used on any device that doesn't have a payment kernel, turning the device's own NFC reader into a card terminal (COTS). This includes pure Android/COTS hardware as well as certain Sunmi models without kernel support (e.g. the L3). It's a different integration from SmartPOS: launched with an explicit action/component instead of getLaunchIntentForPackage, and it returns its result via onActivityResult instead of onNewIntent.
private static final int REQUEST_CODE_SOFTPOS = 1001;
private static final String SOFTPOS_PACKAGE = "com.phonepos.mspsoftposapp";
private static final String SOFTPOS_ACTION_MANUAL_PAYMENT = "com.phonepos.mspsoftposapp.ACTION_MANUAL_PAYMENT";
private static final String SOFTPOS_COMPONENT = "com.phonepos.mspsoftposapp.ManualPayInputActivity";
Intent softpos = new Intent(SOFTPOS_ACTION_MANUAL_PAYMENT);
softpos.setClassName(SOFTPOS_PACKAGE, SOFTPOS_COMPONENT);
// amount is a decimal string here, NOT a long in minor units
softpos.putExtra("amount", String.format(Locale.US, "%.2f", amountInCents / 100.0));
softpos.putExtra("order_id", getOrderId());
softpos.putExtra("skip_manual_input", true);
softpos.putExtra("package_name", getPackageName());
softpos.putExtra("items", jsonArray.toString()); // optional
softpos.putExtra("callback_activity", getClass().getName());
if (getPackageManager().resolveActivity(softpos, 0) != null) {
startActivityForResult(softpos, REQUEST_CODE_SOFTPOS);
}Important:
amountis sent as a decimal string (e.g."1.50") — unlike SmartPOS, whereamountis alongin minor units (cents).- SoftPOS must be launched with
startActivityForResult, since it returns its result directly rather than calling back via a newIntent(see SoftPOS Callback). - Always check
resolveActivity(...)before launching — if it returnsnull, SoftPOS isn't installed. - Unreferenced refunds are not currently supported on SoftPOS (see Unreferenced Refund Flow).
To start a card-on-file subscription (the shopper's card is tokenized for future off-session charges), send the same target-app Intent as a normal payment, plus recurring_model and reference extras.
// SmartPOS (Sunmi)
String packageName = "com.multisafepay.pos.sunmi";
Intent intent = getPackageManager().getLaunchIntentForPackage(packageName);
intent.setClassName(packageName, "com.multisafepay.pos.middleware.IntentActivity");
intent.putExtra("amount", amountInCents);
intent.putExtra("currency", "EUR");
intent.putExtra("order_id", getOrderId());
intent.putExtra("description", "First subscription payment");
intent.putExtra("package_name", getPackageName());
intent.putExtra("recurring_model", "cardOnFile");
intent.putExtra("reference", shopperReference); // links future off-session charges to this payment
startActivity(intent);// SoftPOS
Intent softpos = new Intent(SOFTPOS_ACTION_MANUAL_PAYMENT);
softpos.setClassName(SOFTPOS_PACKAGE, SOFTPOS_COMPONENT);
softpos.putExtra("amount", String.format(Locale.US, "%.2f", amountInCents / 100.0));
softpos.putExtra("currency", "EUR");
softpos.putExtra("order_id", getOrderId());
softpos.putExtra("description", "First subscription payment");
softpos.putExtra("skip_manual_input", true);
softpos.putExtra("package_name", getPackageName());
softpos.putExtra("callback_activity", getClass().getName());
softpos.putExtra("recurring_model", "cardOnFile");
softpos.putExtra("reference", shopperReference);
startActivityForResult(softpos, REQUEST_CODE_SOFTPOS);referenceis the shopper reference used to link future off-session charges to this first payment.- The result of this first payment is delivered through the same callback mechanism as a normal payment —
onNewIntentfor SmartPOS,onActivityResultfor SoftPOS.