onRequestStart when your backend must approve a payment before Payrails authorizes it—revalidating a voucher, confirming a wallet balance, or checking loyalty points the moment the customer commits.
For why the gate sits where it does, see SDK Concepts.
Steps
Step 1: Register the handler
Supply it onOptions, beside redirectSessionLifecycle:
suspend function, so call your backend directly—no callback plumbing.
Step 2: Gate only the methods you care about
The handler fires for every payment method on the session. ReturnProceed on the branches you are not gating—otherwise the handler blocks those payments too:
Step 3: Handle an unreachable backend
The SDK waits 10 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 explicit:withContext. Switch context only to update the UI:
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 cancellation reaches it.
Step 4: Handle the block in your delegate
A blocked payment arrives asonAuthorizeFailed with failure.code == VALIDATION_FAILED. Separate
it from a decline — nothing reached the backend, so there is no failed payment to explain:
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 stashing
it somewhere and correlating it back by executionId.
Google Pay
The button opens Google Pay’s sheet, not the SDK core, so the SDK consults the gate the moment the customer taps—before the sheet appears. A refusal means they never see it. Every other method reaches the gate throughSession.authorize, which also runs before any provider UI.
The practical consequence: for Google Pay, the SDK calls your handler before the customer chooses a
card. A basket-only check—a voucher, a wallet balance, loyalty points—works fine here. It cannot
depend on which card they picked.
Reference
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.