Skip to content

Android integration (Kotlin) ​

Who this is for ​

Native Android apps (Kotlin) that need to show the amount due and collection details in their own UI and refresh the order state once the buyer has paid. The example uses Retrofit + OkHttp.

Before you start ​

  • You have an API KEY (see Get started).
  • You have configured a collection address and set it active.
  • You have your own server — a mobile app cannot hold the API KEY.

The KEY cannot live inside an APK

APKs can be decompiled, so any string baked into the package is public. Order requests from the app go to your own server, which calls the platform's Open API with the KEY.

Install ​

kotlin
// app/build.gradle.kts
dependencies {
    implementation("com.squareup.retrofit2:retrofit:2.11.0")
    implementation("com.squareup.retrofit2:converter-gson:2.11.0")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
}

Minimal working example ​

Two pieces: your server calls the platform with the KEY, and the app talks only to your server.

1. Your server (Kotlin / Ktor or OkHttp) ​

kotlin
// Server side: call the platform's Open API with OkHttp
val client = OkHttpClient.Builder()
    .callTimeout(15, TimeUnit.SECONDS)
    .build()

fun createOrder(orderNo: String, amount: String): String {
    val json = """
        {
          "merchantOrderNo": "$orderNo",
          "chainId": 2,
          "tokenId": 1,
          "amount": "$amount",
          "subject": "Order $orderNo",
          "notifyUrl": "https://your-shop.com/pay/notify"
        }
    """.trimIndent()

    val req = Request.Builder()
        .url("${System.getenv("API_BASE")}/api/pay/open/createOrder")
        .addHeader("X-Pay-Api-Key", System.getenv("PAY_API_KEY"))   // server side only
        .addHeader("Content-Type", "application/json")
        .post(json.toRequestBody("application/json".toMediaType()))
        .build()

    return client.newCall(req).execute().use { it.body!!.string() }
}

2. App side: Retrofit talks only to your backend ​

kotlin
data class CheckoutReq(val orderNo: String, val amount: String)

data class CheckoutResp(
    val code: Int,
    val orderNo: String?,
    val payAddress: String?,
    val payAmountDisplay: String?,
    val payAmountMinimal: String?,
    val chainKey: String?,
    val msg: String?,
)

data class StatusResp(
    val code: Int,
    val status: Int,          // 1..6; only 3/4 mean paid
    val statusText: String?,
    val txHash: String?,
)

interface ShopApi {
    @POST("/api/checkout")
    suspend fun checkout(@Body body: CheckoutReq): CheckoutResp

    @GET("/api/order-status")
    suspend fun status(@Query("orderNo") orderNo: String): StatusResp
}
kotlin
class CheckoutViewModel : ViewModel() {
    private val api = Retrofit.Builder()
        .baseUrl("https://your-shop.com")
        .addConverterFactory(GsonConverterFactory.create())
        .build()
        .create(ShopApi::class.java)

    private val _state = MutableStateFlow<UiState>(UiState.Loading)
    val state: StateFlow<UiState> = _state

    fun start(orderNo: String, amount: String) = viewModelScope.launch {
        try {
            val o = api.checkout(CheckoutReq(orderNo, amount))
            if (o.code != 0) {
                _state.value = UiState.Error(o.msg ?: "create failed")
                return@launch
            }
            _state.value = UiState.Ready(o)

            // Poll until confirmed / finished
            while (true) {
                delay(3_000)
                val s = api.status(o.orderNo!!)
                when (s.status) {
                    1 -> _state.value = UiState.Ready(o)          // waiting for payment
                    2 -> _state.value = UiState.Pending(o)        // submitted, awaiting confirmations
                    3, 4 -> { _state.value = UiState.Paid(o); break }
                    5 -> { _state.value = UiState.Error("Order expired"); break }
                    6 -> { _state.value = UiState.Error("Manual review required — contact support"); break }
                }
            }
        } catch (e: Exception) {
            _state.value = UiState.Error(e.message ?: "network error")
        }
    }
}

You can also skip building the checkout UI in the app and open the platform's payment counter: {cashierBaseUrl}/pay/cashier/m/{merchantNo}, with both values from the merchant-side GET /api/pay/getMyProfile.

Callback handling ​

Callbacks are received by your server (the app is not involved). Verify with OkHttp on the server:

kotlin
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

fun verify(rawBody: String, ts: String, sig: String, apiKey: String): Boolean {
    val mac = Mac.getInstance("HmacSHA256")
    mac.init(SecretKeySpec(apiKey.toByteArray(), "HmacSHA256"))
    mac.update(ts.toByteArray())        // timestamp and body concatenated as strings
    mac.update(rawBody.toByteArray())   // raw body, never re-serialised
    val want = mac.doFinal().joinToString("") { "%02x".format(it) }
    return java.security.MessageDigest.isEqual(want.toByteArray(), sig.toByteArray()) // constant time
}

Handling notes:

  • Idempotent: callbacks retry; deduplicate on merchantOrderNo so you never ship twice.
  • Return fast: about a 10-second timeout; move slow work to a background task.
  • Query is authoritative: verify with Query order first.

Wallet hand-off ​

Wallet hand-off on Android relies on wallet apps' URL schemes: handing a link such as https://your-shop.com/pay/... to the system chooser opens an installed wallet. Show the collection address and amount as text alongside the QR code so users without a wallet can still pay.

FAQ ​

QuestionAnswer
Can the KEY go in BuildConfig?No — the APK can be decompiled
Should I use a WebView?Optional. You can also open the platform checkout in the system browser and maintain one fewer UI
Polling interval?3–5 seconds; pause when the app goes to background and resume on return
How do I know it is paid?Only states 3 / 4 — never compare numeric values
Will retries after a network drop create duplicate orders?No — the same merchantOrderNo is idempotently reused

Web3 payment infrastructure for global merchants