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 via | PaymentListener.onPaymentStatus in your app | Your notification channel, server-to-server |
| What it is | What the merchant's own checkout page displayed after the card was submitted | What actually happened to the money on the card rails |
| Timing | Seconds, while the customer is still looking at your app | Asynchronous; usually seconds, but not guaranteed |
| Authority | None. It is an observation of a page Open Fabric does not control | Authoritative |
| Use it for | What the customer sees next | Balances, order status, ledgers, fulfilment, reconciliation |
The short version: PaymentStatus drives your UI. The settlement event drives your books.
What to do for each status
| Status | Suggested customer-facing behaviour | Ledger action |
|---|---|---|
SUCCEEDED | Show your success state. Say the order is confirmed by the merchant. | None yet; wait for the settlement event. |
PENDING | Show that the order is placed and awaiting payment completion; keep any instructions the merchant displayed visible. | None yet. |
REQUIRES_ACTION | Do nothing and keep your surface open. The customer is on a 3DS or OTP screen. Wait for the next callback. | None. |
FAILED | Tell 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. |
UNCONFIRMED | Show 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.