Skip to content

Android

Android is the only platform where the UPI app hands your app something back. Used properly it is the strongest confirmation signal in the product, because it does not depend on the amount at all.

The Kotlin SDK is a single AAR with no dependencies of its own — JSON comes from the framework’s org.json, HTTP from HttpURLConnection — so there is nothing else to add to your build.

Download moneylanded-sdk.aar, drop it into app/libs/, and add one line:

dependencies {
implementation(files("libs/moneylanded-sdk.aar"))
}

There is no registry coordinate to depend on instead — when there is one, this paragraph will say so. Everything below also works without the library, against the same endpoints.

  1. Your backend creates the order and returns page_token to the app.
  2. The app reserves through POST /p/{token}/reserve.
  3. The app launches the UPI intent with startActivityForResult.
  4. The UPI app returns a result containing ApprovalRefNo — the UTR.
  5. The app sends that UTR to your backend, which posts it to POST /v1/intents/{id}/receipt.
  6. When the bank’s credit alert arrives carrying that reference, the order is confirmed with evidence receipt.

Your API key stays on your server at every step. The app only ever holds the page token, which is good for one order.

val res = client.newCall(
Request.Builder()
.url("https://moneylanded.com/p/$pageToken/reserve")
.post("""{"app":"phonepe"}""".toRequestBody("application/json".toMediaType()))
.build()
).execute().use { JSONObject(it.body!!.string()) }
val upiUri = res.getString("upi_uri")
val paid = res.getInt("paid_paise") // what to show the customer
val off = res.getInt("discount_paise") // 0 most of the time
private val upi = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
// The UPI app also returns a Status field. We accept it and never trust it:
// only the bank's own alert confirms a payment. Read the reference and let
// the server decide.
val utr = r.data?.getStringExtra("response")
?.split("&")
?.map { it.split("=", limit = 2) }
?.firstOrNull { it.size == 2 && it[0].equals("ApprovalRefNo", ignoreCase = true) }
?.get(1)
if (utr != null && utr.matches(Regex("""\d{12}"""))) {
// Your own backend, which holds the API key.
reportReceiptToYourServer(intentId, utr)
}
// No UTR? Nothing is lost. The amount path confirms it the moment the
// bank alert lands, exactly as it does on every other platform.
}
fun pay(upiUri: String) {
val i = Intent(Intent.ACTION_VIEW, Uri.parse(upiUri))
upi.launch(Intent.createChooser(i, "Pay with"))
}

Then on your server:

Terminal window
curl -sS -X POST https://moneylanded.com/v1/intents/int_9f2c1a77b40e6d3a5c81/receipt \
-H "Authorization: Bearer $ML_KEY" \
-H "Content-Type: application/json" \
-d '{"utr":"530112345678"}'
{ "status": "pending" }

pending means we have stored the reference and are waiting for the bank. confirmed means a credit with that reference had already arrived and was sitting unplaced — it has now been attached to the order.

A receipt confirmation matches on the bank’s own reference number, not on the amount. So it survives the case the amount path cannot: the customer changed the amount, or paid from somewhere the amount walk did not anticipate. It is also immediate rather than waiting on an amount comparison.

Not every UPI app returns a UTR for a personal VPA. When none comes back, nothing degrades — the amount path confirms the order the same way it does for every web and iOS customer.

  • Do not ask the customer to read a reference off their screen. The number a payer’s app shows them is not always the number the payee’s bank reports, and asking is the kind of friction this product exists to remove. The UTR here comes from the UPI app to your app, machine to machine.
  • Do not trust the Status field. We accept it on the endpoint and throw it away. A UPI app reporting SUCCESS is not money in your account.
  • Do not ship your API key in the APK. It can be extracted from any installed app in about a minute.