How to Gate Payment Authorization

Use onRequestStart when your backend has to approve a payment before Payrails authorizes it —
revalidating a voucher, confirming wallet balance, or re-checking loyalty points at the moment the
customer commits.

For why the gate sits where it does, see SDK Concepts.

sequenceDiagram
    participant Customer
    participant App as Merchant App
    participant SDK as Payrails SDK
    participant Backend as Merchant Backend
    participant API as Payrails API

    Customer->>App: Taps pay
    App->>SDK: Element starts the payment
    SDK->>App: onRequestStart(context)
    App->>Backend: POST /pre-payment
    Backend-->>App: valid or invalid
    App-->>SDK: true or false
    alt true
        SDK->>API: POST authorize
        API-->>SDK: Authorized
    else false
        SDK->>App: onAuthorizeFailed(VALIDATION_FAILED)
    end

1. Register the handler

Supply it on Options, beside redirectSessionLifecycle:

val configuration = Configuration(
    initData = initData,
    option = Options(
        onRequestStart = { context ->
            if (myBackend.validatePrePayment(context.executionId)) {
                RequestStartDecision.Proceed
            } else {
                RequestStartDecision.Refuse("Your basket is no longer valid.")
            }
        }
    )
)

The handler is a suspend function, so call your backend directly — no callback plumbing.

2. Gate only the methods you care about

The handler fires for every payment method on the session. Return Proceed on the branches you are
not gating, or those payments are blocked too:

onRequestStart = { context ->
    if (context.paymentMethodCode != "payPal") {
        RequestStartDecision.Proceed          // don't gate anything else
    } else {
        val check = myBackend.validatePrePayment(context.executionId)
        if (check.ok) RequestStartDecision.Proceed else RequestStartDecision.Refuse(check.reason)
    }
}

3. Decide what an unreachable backend means

The SDK waits ten seconds, then blocks the payment. A handler that throws blocks it too. Both are
deliberate — a gate that fails open is not a gate — but they mean your error path is a decision, so
make it explicitly:

onRequestStart = { context ->
    try {
        val check = myBackend.validatePrePayment(context.executionId)
        if (check.ok) RequestStartDecision.Proceed else RequestStartDecision.Refuse(check.reason)
    } catch (e: IOException) {
        // Your service is unreachable. Refuse blocks the payment; Proceed accepts the risk of an
        // unvalidated basket. Letting the exception escape also refuses it, but without a message.
        RequestStartDecision.Refuse("We couldn't confirm your basket. Please try again.")
    }
}

The handler runs on a background dispatcher, so a suspending network call needs no extra
withContext. Switch context only to touch UI:

onRequestStart = { context ->
    val allowed = myBackend.validatePrePayment(context.executionId)   // already off the main thread
    if (!allowed) {
        withContext(Dispatchers.Main) { showBasketExpiredDialog() }
    }
    allowed
}

One caveat about the 10-second bound: it can only interrupt a handler that suspends. A handler
that blocks its thread — a synchronous HTTP client, Thread.sleep, a blocking database read — runs
to completion no matter how long it takes, because coroutine cancellation is cooperative. Use a
suspending client, or wrap a blocking one so it can be cancelled.

4. Handle the block in your delegate

A blocked payment arrives as onAuthorizeFailed with failure.code == VALIDATION_FAILED. Separate
it from a decline — nothing reached the backend, so there is no failed payment to explain:

override fun onAuthorizeFailed(button: PayPalButton, failure: AuthorizationFailure) {
    when (failure.code) {
        AuthorizationFailureReason.VALIDATION_FAILED ->
            // Your own check refused it. failure.message is the reason you passed to
            // Refuse(message), so you can show it directly instead of mapping a code.
            showBasketChangedMessage(failure.message)
        AuthorizationFailureReason.AUTHORIZATION_ERROR ->
            showDeclineMessage()            // the issuer refused it
        else ->
            showGenericError(failure.message)
    }
}

This is why Refuse carries a message rather than the handler returning a bare false: the reason
travels with the refusal to the one place you already read failure text from, instead of having to
be stashed somewhere and correlated back by executionId.

A note on Google Pay

Google Pay's sheet is opened by the button, not by the SDK core, so the gate is consulted the moment
the customer taps — before the sheet appears. A refusal means they never see it. Every other method
reaches the gate through Session.authorize, which also runs before any provider UI.

The practical consequence: for Google Pay your handler is called before the customer has chosen a
card. If your check depends only on the basket — a voucher, a wallet balance, loyalty points — that
is what you want. It cannot depend on which card they picked.

Reference

Context fieldTypeNotes
executionIdString?The Payrails execution, when known
paymentMethodCodeString"card", "payPal", "googlePay", …
actionActionAlways AUTHORIZE in this version
Handler behaviourResult
returns ProceedAuthorization proceeds
returns Refuse(message)Stopped as VALIDATION_FAILED, carrying your message
returns Refuse()Stopped as VALIDATION_FAILED with a generic description
no answer within 10 secondsStopped with a generic description, warning logged
throwsStopped with a generic description, warning logged

Only a deliberate Refuse(message) reaches failure.message. The timeout and throw cases log their
diagnostic instead of surfacing it — both describe an integration fault rather than anything phrased
for a customer, and an exception string can carry internals.

Blocking never sends an authorization request, never presents provider UI, and returns the element
to ButtonState.ENABLED.

onRequestStart is a Kotlin-only API, like onSessionExpired: suspend functions are not
implementable from Java.

Related


Did this page help you?