Bonaventure OgetoBy Bonaventure Ogeto|

Accept Payments with Paystack in Android with Kotlin

Android apps cannot call the Paystack API with a secret key. Initialize the transaction on your backend and return the authorization_url. Open it in a WebView Activity in Android. Add a WebViewClient that intercepts the callback URL redirect, extracts the reference, and sends it to your backend for verification via Retrofit or OkHttp.

Setup: Permissions and Dependencies

Add INTERNET permission to AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

Add dependencies to build.gradle.kts (app module):

dependencies {
    implementation("com.squareup.retrofit2:retrofit:2.11.0")
    implementation("com.squareup.retrofit2:converter-gson:2.11.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
    // WebView is included in Android SDK
}

Backend API Service

// data/PaymentApiService.kt
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query

data class InitializeRequest(val email: String, val amount: Double)
data class InitializeResponse(
    val authorization_url: String,
    val access_code: String,
    val reference: String
)
data class VerifyResponse(
    val verified: Boolean,
    val amount: Double?,
    val currency: String?,
    val reference: String?
)

interface PaymentApiService {
    @POST("api/pay/initialize")
    suspend fun initialize(@Body request: InitializeRequest): InitializeResponse

    @GET("api/pay/verify")
    suspend fun verify(@Query("reference") reference: String): VerifyResponse
}

// RetrofitClient.kt
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitClient {
    private val retrofit = Retrofit.Builder()
        .baseUrl("https://your-backend.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val paymentApi: PaymentApiService = retrofit.create(PaymentApiService::class.java)
}

Payment WebView Activity

// PaymentActivity.kt
import android.annotation.SuppressLint
import android.os.Bundle
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch

class PaymentActivity : AppCompatActivity() {
    private lateinit var webView: WebView

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        webView = WebView(this)
        setContentView(webView)

        webView.settings.javaScriptEnabled = true
        webView.settings.domStorageEnabled = true

        val authUrl = intent.getStringExtra("authorization_url") ?: run {
            finish()
            return
        }

        webView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(
                view: WebView?,
                request: WebResourceRequest?
            ): Boolean {
                val url = request?.url?.toString() ?: return false

                // Detect callback URL
                if (url.startsWith("https://yourapp.com/payment/callback")) {
                    val uri = android.net.Uri.parse(url)
                    val reference = uri.getQueryParameter("reference")
                        ?: uri.getQueryParameter("trxref")

                    if (reference != null) {
                        verifyPayment(reference)
                    }
                    return true
                }
                return false
            }
        }

        webView.loadUrl(authUrl)
    }

    private fun verifyPayment(reference: String) {
        lifecycleScope.launch {
            try {
                val result = RetrofitClient.paymentApi.verify(reference)
                val intent = android.content.Intent()
                intent.putExtra("reference", reference)
                intent.putExtra("verified", result.verified)
                intent.putExtra("amount", result.amount ?: 0.0)
                intent.putExtra("currency", result.currency)
                setResult(
                    if (result.verified) RESULT_OK else RESULT_CANCELED,
                    intent
                )
            } catch (e: Exception) {
                setResult(RESULT_CANCELED)
            }
            finish()
        }
    }
}

Register the Activity in AndroidManifest.xml:

<activity android:name=".PaymentActivity" android:exported="false" />

Launch Payment from Your Activity

// MainActivity.kt or CheckoutFragment.kt
class CheckoutActivity : AppCompatActivity() {
    private val paymentLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        if (result.resultCode == RESULT_OK) {
            val reference = result.data?.getStringExtra("reference")
            val amount = result.data?.getDoubleExtra("amount", 0.0)
            val currency = result.data?.getStringExtra("currency")
            showPaymentSuccess(reference, amount, currency)
        } else {
            showPaymentFailed()
        }
    }

    private fun startPayment(email: String, amountNGN: Double) {
        lifecycleScope.launch {
            try {
                val response = RetrofitClient.paymentApi.initialize(
                    InitializeRequest(email, amountNGN)
                )
                val intent = Intent(this@CheckoutActivity, PaymentActivity::class.java)
                intent.putExtra("authorization_url", response.authorization_url)
                paymentLauncher.launch(intent)
            } catch (e: Exception) {
                showError("Failed to initialize payment")
            }
        }
    }
}

Learn More

Key Takeaways

  • Never put the Paystack secret key in your Android APK. Always initialize transactions from a backend server.
  • Open the Paystack authorization_url in a WebView Activity. Add a WebViewClient to intercept the callback URL.
  • Extract the reference from the callback URL query parameter and verify it on your backend.
  • Add INTERNET permission in AndroidManifest.xml for WebView to load Paystack pages.
  • Use Retrofit or OkHttp to call your backend API from Kotlin coroutines.
  • Amounts are in the smallest currency unit (kobo for NGN). This conversion should happen on the backend.

Frequently Asked Questions

Does Paystack have an official Android SDK?
Paystack has a community Android library (co.paystack.android) but it is not actively maintained as of 2026. The WebView approach in this guide is more reliable and gives you full control. It also means you depend only on the stable Paystack REST API, not a third-party SDK.
Can I use Chrome Custom Tabs instead of WebView for Paystack checkout?
Yes. Chrome Custom Tabs (CustomTabsIntent) open the Paystack authorization URL in a Chrome browser overlay with better performance and security than WebView. To detect the callback, use an intent filter with your app scheme as the callback URL and handle it as a deep link.
How do I store the Paystack secret key securely on Android?
You do not store it on Android at all. The secret key lives on your backend server. Your Android app only knows the Paystack public key (if you need it for any client-side Paystack features). All API calls requiring the secret key go through your backend.
What is the best way to call my backend API from Kotlin?
Retrofit with coroutines is the standard choice. Define suspend functions in your interface and call them inside lifecycleScope.launch or viewModelScope.launch. Alternatively, use the Ktor client for a Kotlin-native approach. OkHttp alone works but is more verbose.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon