Skip to main content

Overview

The PayMaya SDK provides the checkPaymentStatus() method to manually verify the status of a payment transaction. This is useful for reconciliation, handling edge cases, or verifying payments after activity results.
The SDK automatically checks payment status when users close the payment activity. Manual checking is typically needed for reconciliation or error recovery.

CheckPaymentStatusResult

The status check returns one of three result types:
CheckPaymentStatusResult.kt
sealed class CheckPaymentStatusResult {

    /**
     * Checking payment status succeeded.
     *
     * @property status Payment status.
     */
    class Success internal constructor(
        val status: PaymentStatus
    ) : CheckPaymentStatusResult()

    /**
     * Checking payment status canceled.
     */
    object Cancel : CheckPaymentStatusResult()

    /**
     * Checking payment status failed.
     *
     * @property exception Exception with detailed reason of the failure.
     */
    class Failure internal constructor(
        val exception: Exception
    ) : CheckPaymentStatusResult()
}

PaymentStatus Enum

The PaymentStatus enum defines all possible payment states:
PaymentStatus.kt
enum class PaymentStatus {

    /**
     * Token is pending
     */
    PENDING_TOKEN,

    /**
     * Initial payment status of the checkout transaction
     */
    PENDING_PAYMENT,

    /**
     * When payment transaction is not executed and expiration time has been reached.
     */
    PAYMENT_EXPIRED,

    /**
     * When payment transaction is waiting for authentication.
     */
    FOR_AUTHENTICATION,

    /**
     * When payment transaction is currently authenticating.
     */
    AUTHENTICATING,

    /**
     * When authentication has been successfully executed.
     * Authentication may be in the form of 3DS authentication for card transactions.
     */
    AUTH_SUCCESS,

    /**
     * When authentication of payment has failed.
     */
    AUTH_FAILED,

    /**
     * When payment is processing.
     */
    PAYMENT_PROCESSING,

    /**
     * When payment is successfully processed.
     */
    PAYMENT_SUCCESS,

    /**
     * When payment is not successfully processed.
     */
    PAYMENT_FAILED,

    /**
     * When a successfully processed payment has been reversed
     * (usually before a settlement cut-off for card-based payments).
     */
    VOIDED,

    /**
     * When a successfully processed payment has been fully or partially reversed
     * (usually after a settlement cut-off for card-based payments).
     */
    REFUNDED
}

Payment Status Flow

Understanding the typical flow of payment statuses:

Using checkPaymentStatus()

Important: The checkPaymentStatus() method is synchronous and performs a network request. Always call it from a background thread, not the main UI thread.

Basic Usage

import kotlinx.coroutines.*

// Check Checkout payment status
viewModelScope.launch(Dispatchers.IO) {
    val result = payMayaCheckoutClient.checkPaymentStatus(checkoutId)
    
    withContext(Dispatchers.Main) {
        handleStatusResult(result)
    }
}

private fun handleStatusResult(result: CheckPaymentStatusResult) {
    when (result) {
        is CheckPaymentStatusResult.Success -> {
            Log.d(TAG, "Payment status: ${result.status}")
            updateUIBasedOnStatus(result.status)
        }
        
        is CheckPaymentStatusResult.Cancel -> {
            Log.d(TAG, "Status check canceled")
        }
        
        is CheckPaymentStatusResult.Failure -> {
            Log.e(TAG, "Status check failed: ${result.exception.message}")
            showError("Could not retrieve payment status")
        }
    }
}

With Different Payment Types

fun checkCheckoutStatus(checkoutId: String) {
    lifecycleScope.launch(Dispatchers.IO) {
        val result = payMayaCheckoutClient.checkPaymentStatus(checkoutId)
        
        withContext(Dispatchers.Main) {
            when (result) {
                is CheckPaymentStatusResult.Success -> {
                    processCheckoutStatus(checkoutId, result.status)
                }
                is CheckPaymentStatusResult.Failure -> {
                    Log.e(TAG, "Failed to check status", result.exception)
                }
                else -> { /* Handle cancel */ }
            }
        }
    }
}

Handling Payment Statuses

Complete Status Handler

private fun updateUIBasedOnStatus(status: PaymentStatus) {
    when (status) {
        PaymentStatus.PAYMENT_SUCCESS -> {
            showSuccessScreen()
            markOrderAsCompleted()
        }
        
        PaymentStatus.PAYMENT_FAILED -> {
            showFailureScreen("Payment was declined")
            allowRetry()
        }
        
        PaymentStatus.AUTH_FAILED -> {
            showFailureScreen("Authentication failed")
            allowRetry()
        }
        
        PaymentStatus.PAYMENT_EXPIRED -> {
            showExpiredScreen()
            createNewPayment()
        }
        
        PaymentStatus.PENDING_PAYMENT,
        PaymentStatus.PENDING_TOKEN -> {
            showLoadingScreen("Payment is being processed...")
            scheduleStatusRecheck()
        }
        
        PaymentStatus.FOR_AUTHENTICATION,
        PaymentStatus.AUTHENTICATING -> {
            showLoadingScreen("Authenticating payment...")
            scheduleStatusRecheck()
        }
        
        PaymentStatus.AUTH_SUCCESS,
        PaymentStatus.PAYMENT_PROCESSING -> {
            showLoadingScreen("Processing payment...")
            scheduleStatusRecheck()
        }
        
        PaymentStatus.VOIDED -> {
            showVoidedScreen()
            updateOrderStatus("VOIDED")
        }
        
        PaymentStatus.REFUNDED -> {
            showRefundedScreen()
            updateOrderStatus("REFUNDED")
        }
    }
}

Polling for Status Updates

For pending payments, you may need to poll for status updates:
private var statusCheckJob: Job? = null

fun startPollingStatus(paymentId: String) {
    statusCheckJob?.cancel()
    
    statusCheckJob = lifecycleScope.launch(Dispatchers.IO) {
        var attempts = 0
        val maxAttempts = 10
        val delayMs = 3000L
        
        while (attempts < maxAttempts && isActive) {
            val result = payMayaCheckoutClient.checkPaymentStatus(paymentId)
            
            when (result) {
                is CheckPaymentStatusResult.Success -> {
                    val status = result.status
                    
                    withContext(Dispatchers.Main) {
                        updateUIBasedOnStatus(status)
                    }
                    
                    // Stop polling if terminal state reached
                    if (isTerminalStatus(status)) {
                        break
                    }
                }
                
                is CheckPaymentStatusResult.Failure -> {
                    Log.w(TAG, "Status check attempt $attempts failed")
                }
                
                else -> { /* Continue polling */ }
            }
            
            attempts++
            delay(delayMs)
        }
        
        if (attempts >= maxAttempts) {
            withContext(Dispatchers.Main) {
                showError("Unable to verify payment status. Please contact support.")
            }
        }
    }
}

private fun isTerminalStatus(status: PaymentStatus): Boolean {
    return status in listOf(
        PaymentStatus.PAYMENT_SUCCESS,
        PaymentStatus.PAYMENT_FAILED,
        PaymentStatus.AUTH_FAILED,
        PaymentStatus.PAYMENT_EXPIRED,
        PaymentStatus.VOIDED,
        PaymentStatus.REFUNDED
    )
}

override fun onDestroy() {
    super.onDestroy()
    statusCheckJob?.cancel()
}

When to Check Payment Status

Verify the status if you receive a Cancel result but suspect the payment may have succeeded:
is PayMayaCheckoutResult.Cancel -> {
    result.checkoutId?.let { checkoutId ->
        // User closed activity, verify actual status
        verifyPaymentStatus(checkoutId)
    }
}
If a user returns to your app after a payment, verify the status:
override fun onResume() {
    super.onResume()
    pendingCheckoutId?.let { checkoutId ->
        verifyPaymentStatus(checkoutId)
    }
}
Periodically check status of pending payments for order reconciliation:
fun reconcilePendingPayments() {
    val pendingPayments = database.getPendingPayments()
    pendingPayments.forEach { payment ->
        checkAndUpdatePaymentStatus(payment.id)
    }
}
When users want to check their payment status:
btnCheckStatus.setOnClickListener {
    showLoading()
    checkPaymentStatus(currentCheckoutId)
}

Threading Best Practices

Never call checkPaymentStatus() on the main thread. It performs network I/O and will cause ANR (Application Not Responding) errors.
lifecycleScope.launch(Dispatchers.IO) {
    val result = client.checkPaymentStatus(id)
    withContext(Dispatchers.Main) {
        updateUI(result)
    }
}

Best Practices

Background Thread

Always call checkPaymentStatus() from a background thread to avoid blocking the UI.

Error Handling

Handle network failures gracefully with retry logic and user feedback.

Avoid Excessive Polling

Limit polling attempts and use exponential backoff to avoid overwhelming the server.

Store Transaction IDs

Always save checkoutId or paymentId for status checking and reconciliation.

Backend Verification

For critical transactions, verify status on your backend using PayMaya’s APIs.

Terminal States

Stop polling when reaching terminal states like SUCCESS, FAILED, or EXPIRED.

Example: Complete Status Flow

class PaymentStatusManager(
    private val client: PayMayaCheckout,
    private val scope: CoroutineScope
) {
    
    fun checkStatus(checkoutId: String, callback: (PaymentStatus?) -> Unit) {
        scope.launch(Dispatchers.IO) {
            try {
                val result = client.checkPaymentStatus(checkoutId)
                
                when (result) {
                    is CheckPaymentStatusResult.Success -> {
                        withContext(Dispatchers.Main) {
                            callback(result.status)
                        }
                    }
                    
                    is CheckPaymentStatusResult.Failure -> {
                        Log.e(TAG, "Status check failed", result.exception)
                        withContext(Dispatchers.Main) {
                            callback(null)
                        }
                    }
                    
                    else -> {
                        withContext(Dispatchers.Main) {
                            callback(null)
                        }
                    }
                }
            } catch (e: Exception) {
                Log.e(TAG, "Unexpected error checking status", e)
                withContext(Dispatchers.Main) {
                    callback(null)
                }
            }
        }
    }
    
    fun isSuccessful(status: PaymentStatus): Boolean {
        return status == PaymentStatus.PAYMENT_SUCCESS
    }
    
    fun isFailed(status: PaymentStatus): Boolean {
        return status in listOf(
            PaymentStatus.PAYMENT_FAILED,
            PaymentStatus.AUTH_FAILED,
            PaymentStatus.PAYMENT_EXPIRED
        )
    }
    
    fun isPending(status: PaymentStatus): Boolean {
        return status in listOf(
            PaymentStatus.PENDING_TOKEN,
            PaymentStatus.PENDING_PAYMENT,
            PaymentStatus.FOR_AUTHENTICATION,
            PaymentStatus.AUTHENTICATING,
            PaymentStatus.AUTH_SUCCESS,
            PaymentStatus.PAYMENT_PROCESSING
        )
    }
}

Next Steps

Result Handling

Learn how to handle payment activity results

Testing

Test payment status flows in sandbox environment

Build docs developers (and LLMs) love