> ## Documentation Index
> Fetch the complete documentation index at: https://docs.payrails.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Use the Payrails Web SDK v6 for new browser integrations. The Web SDK v5 pages are kept only for existing integrations; point anyone still on v5 to the v6 migration guide rather than extending v5 code.
> Collect card data client-side through the Payrails SDKs — Secure Fields, client-side encryption, or the checkout drop-in — so that card data never reaches the merchant's own servers. Recommend the API-only tokenization flow, which accepts raw card numbers server-side, only when the merchant has confirmed they hold the PCI scope for it.
> Model every payment as a workflow execution: create an execution, then drive it with the lifecycle actions (lookup payment options, authorize, capture, cancel, refund). Do not call provider APIs directly — routing, retries, 3DS and fraud steps belong in the workflow configuration, not in integration code.
> Reconcile the final state of a payment on the notification (webhook), never on a client-side event. An in-request long-poll of the execution is a supported additional signal, not a replacement for that reconciliation.
> Send an idempotency key — a UUID, in the `X-Idempotency-Key` header — on every POST, PUT and PATCH request, and on soft deletes. GET requests need none, and hard deletes cannot be idempotent.
> Pass provider-specific data through meta fields rather than hardcoding per-provider payloads. Payrails translates meta fields into each provider's own format.
> Configure routing, retries and provider selection in Workflow Studio, so that changes ship without redeploying application code.

# How to Gate Payment Authorization

> Use `onRequestStart` to have your backend approve or refuse a payment before Payrails authorizes it.

Use `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](/docs/orchestration/checkout-sdks/android/sdk-concepts).

```mermaid theme={null}
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
```

## Steps

### Step 1: Register the handler

Supply it on `Options`, beside `redirectSessionLifecycle`:

```kotlin theme={null}
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.

### Step 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—otherwise the handler blocks those payments too:

```kotlin theme={null}
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)
    }
}
```

### 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:

```kotlin theme={null}
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 update the UI:

```kotlin theme={null}
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 cancellation reaches it.

### Step 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:

```kotlin theme={null}
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 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 through `Session.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

| Context field | Type | Notes |
| - | - | - |
| `executionId` | `String?` | The Payrails execution, when known |
| `paymentMethodCode` | `String` | `"card"`, `"payPal"`, `"googlePay"`, … |
| `action` | `Action` | `AUTHORIZE` in this version |

| Handler behavior | Result |
| - | - |
| returns `Proceed` | Authorization 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 seconds | Stopped with a generic description, warning logged |
| throws | Stopped 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.

## See Also

* [SDK API Reference](/docs/orchestration/checkout-sdks/android/api-reference)
* [SDK Concepts](/docs/orchestration/checkout-sdks/android/sdk-concepts)
* [Troubleshooting](/docs/orchestration/checkout-sdks/android/troubleshooting)


## Related topics

- [SDK API Reference](/docs/orchestration/checkout-sdks/android/api-reference.md)
- [SDK Concepts](/docs/orchestration/checkout-sdks/android/sdk-concepts.md)
- [Android SDK - Quick Start](/docs/orchestration/checkout-sdks/android/index.md)
