Bonaventure OgetoBy Bonaventure Ogeto|

Build Paystack Subscriptions in Android with Kotlin

Subscribe a user by calling your backend to initialize a transaction with a Paystack plan code. Open the authorization_url in your WebView Activity. After checkout, Paystack creates the subscription. Your backend receives the subscription.create webhook. Check subscription status from Android by calling your backend status API, not Paystack directly.

Important: Google Play Billing Policy

If you distribute your app on the Google Play Store and sell digital subscriptions through it, Google requires you to use Google Play Billing for in-app purchases. Paystack subscriptions work best for:

  • Apps distributed outside the Play Store (APK sideloading)
  • Business/B2B apps
  • Hybrid apps where the subscription is for a web service (which may qualify as an exemption)

For consumer apps on the Play Store with digital subscriptions, consult the Google Play policies before using Paystack for subscription billing. This guide is most applicable to non-Play Store distribution or B2B apps.

Subscription ViewModel

// viewmodel/SubscriptionViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

data class SubscriptionStatus(
    val active: Boolean = false,
    val planName: String? = null,
    val nextBillingDate: String? = null,
    val subscriptionCode: String? = null,
    val loading: Boolean = true,
    val error: String? = null
)

class SubscriptionViewModel : ViewModel() {
    private val _status = MutableStateFlow(SubscriptionStatus())
    val status: StateFlow<SubscriptionStatus> = _status

    fun checkStatus(userEmail: String) {
        viewModelScope.launch {
            _status.value = SubscriptionStatus(loading = true)
            try {
                val result = RetrofitClient.subscriptionApi.getStatus(userEmail)
                _status.value = SubscriptionStatus(
                    active = result.active,
                    planName = result.planName,
                    nextBillingDate = result.nextBillingDate,
                    subscriptionCode = result.subscriptionCode,
                    loading = false
                )
            } catch (e: Exception) {
                _status.value = SubscriptionStatus(loading = false, error = e.message)
            }
        }
    }

    fun cancelSubscription(subscriptionCode: String, emailToken: String) {
        viewModelScope.launch {
            try {
                RetrofitClient.subscriptionApi.cancel(subscriptionCode, emailToken)
                _status.value = _status.value.copy(active = false)
            } catch (e: Exception) {
                _status.value = _status.value.copy(error = "Cancel failed: ${e.message}")
            }
        }
    }
}

Subscription Status Screen

// SubscriptionActivity.kt
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch

class SubscriptionActivity : AppCompatActivity() {
    private val viewModel: SubscriptionViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_subscription)

        val userEmail = intent.getStringExtra("email") ?: return finish()
        val statusText = findViewById<TextView>(R.id.statusText)
        val planText = findViewById<TextView>(R.id.planText)
        val nextBillingText = findViewById<TextView>(R.id.nextBillingText)
        val cancelButton = findViewById<Button>(R.id.cancelButton)
        val subscribeButton = findViewById<Button>(R.id.subscribeButton)

        lifecycleScope.launch {
            viewModel.status.collect { state ->
                if (state.loading) {
                    statusText.text = "Loading..."
                    return@collect
                }

                if (state.active) {
                    statusText.text = "Active"
                    planText.text = state.planName ?: "Unknown plan"
                    nextBillingText.text = "Next billing: ${state.nextBillingDate}"
                    cancelButton.visibility = View.VISIBLE
                    subscribeButton.visibility = View.GONE

                    cancelButton.setOnClickListener {
                        viewModel.cancelSubscription(
                            state.subscriptionCode ?: "",
                            getEmailToken()
                        )
                    }
                } else {
                    statusText.text = "No active subscription"
                    cancelButton.visibility = View.GONE
                    subscribeButton.visibility = View.VISIBLE

                    subscribeButton.setOnClickListener {
                        startPaymentActivity(userEmail)
                    }
                }
            }
        }

        viewModel.checkStatus(userEmail)
    }
}

Learn More

Key Takeaways

  • Subscription checkout in Android is the same as one-time payment — just pass a plan code on the backend.
  • After checkout, Paystack manages renewals automatically. The Android app does not trigger them.
  • Check subscription status by calling your backend, which queries the Paystack API.
  • Display plan details, next billing date, and subscription code from your backend response.
  • Use a ViewModel and StateFlow to manage subscription state across screens.
  • Use Google Play Billing API if distributing on the Play Store — it is required for in-app subscriptions sold through the store.

Frequently Asked Questions

Can I use Paystack subscriptions instead of Google Play Billing?
For apps distributed on the Play Store selling digital content, Google requires Play Billing. Paystack subscriptions work for apps distributed outside the Play Store, B2B apps, and services sold for physical goods or external services. Check current Play Store policies before deciding.
How does my Android app know a subscription renewal succeeded?
Your backend receives a charge.success webhook from Paystack with a plan property. Your backend updates the subscription record. The Android app checks the backend status on next launch or receives an FCM push notification.
Where do I store the subscription code in Android?
Store it in your backend database, not on the device. The Android app fetches subscription details from your backend. If needed for offline display, cache it in SharedPreferences or Room, but always treat the backend as the source of truth.
How do I implement feature gating based on subscription in Android?
Observe the subscription ViewModel in your main Activity. If subscription.active is true, unlock premium features. If false, show the subscription screen. Use a repository pattern so all screens share the same subscription status. Refresh on app resume to stay current.

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