Skip to main content

Payment outcome

Two different signals can tell you how a payment went, and they answer different questions.

Two signals​

PaymentStatus (on-device)Transaction lifecycle event (backend)
Reaches you viaPaymentListener.onPaymentStatus in your appYour notification channel, server-to-server
What it isWhat the merchant's own checkout page displayed after the card was submittedWhat actually happened to the money on the card rails
TimingSeconds, while the customer is still looking at your appAsynchronous; usually seconds, but not guaranteed
AuthorityNone. It is an observation of a page Open Fabric does not controlAuthoritative
Use it forWhat the customer sees nextBalances, order status, ledgers, fulfilment, reconciliation

The short version: PaymentStatus drives your UI. The settlement event drives your books.

What to do for each status​

StatusSuggested customer-facing behaviourLedger action
SUCCEEDEDShow your success state. Say the order is confirmed by the merchant.None yet; wait for the settlement event.
PENDINGShow that the order is placed and awaiting payment completion; keep any instructions the merchant displayed visible.None yet.
REQUIRES_ACTIONDo nothing and keep your surface open. The customer is on a 3DS or OTP screen. Wait for the next callback.None.
FAILEDTell the customer the merchant declined the payment and let them retry with the merchant's own methods.None. Cancel the approved transaction on your backend if it should not stand.
UNCONFIRMEDShow a neutral "we're confirming your order" state, not a success and not a failure.None. Resolve from the settlement event.

UNCONFIRMED is the status integrations most often get wrong. It does not mean the payment failed and it does not mean it worked. It means the SDK could not tell. A neutral state that resolves when your backend hears from Open Fabric is both the honest and the safest treatment.

Do not tear down on a non-terminal status​

override fun onPaymentStatus(status: PaymentStatus, detail: String?) {
if (!status.isTerminal) {
// REQUIRES_ACTION: a challenge is on screen. Keep the WebView and your
// payment surface exactly as they are. The SDK is still watching and
// will call again with the terminal status.
showChallengeInProgress()
return
}
when (status) {
PaymentStatus.SUCCEEDED -> showSuccess()
PaymentStatus.PENDING -> showAwaitingPayment()
PaymentStatus.FAILED -> showDeclined()
PaymentStatus.UNCONFIRMED -> showConfirming()
// REQUIRES_ACTION returned above, so the five values are covered.
PaymentStatus.REQUIRES_ACTION -> Unit
}
}

onPaymentStatus can fire more than once for a single pay call. Only REQUIRES_ACTION is non-terminal, but check isTerminal rather than comparing against it by name, because the SDK reports statuses it does not recognise as UNCONFIRMED and the set can grow.

Reconciling the two​

The settlement event carries your tenant_reference_id and the transaction id you received when you created the transaction.

A useful pairing in your own logs is the on-device status alongside the eventual settlement outcome. It is how a merchant configuration problem becomes visible: a steady stream of UNCONFIRMED against transactions that settle fine means Open Fabric's detection configuration for that merchant has drifted, and is worth raising with us.

Next: Testing.