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

# SDK API Reference

> Reference for the Payrails iOS SDK: installation, the init payload, session creation, card form and button components, and delegates.

**Current version:** 3.0.0<br />
**Minimum deployment target:** iOS 14.0<br />
**Swift version:** 5.0+<br />
**Distribution:** Swift Package Manager (signed XCFramework)

## Installation

Swift Package Manager, using the package URL:

```
https://github.com/payrails/ios-sdk.git
```

```swift theme={null}
dependencies: [
    .package(url: "https://github.com/payrails/ios-sdk.git", from: "3.0.0")
]
```

The package resolves a prebuilt, signed `Payrails.xcframework` and verifies it against the checksum in the package manifest. `PayrailsCSE` and `PayPalCheckout` resolve as separate package dependencies.

***

## Getting started

### `Payrails.InitData`

Holds the init payload returned by your backend after calling the Payrails initialization endpoint.

```swift theme={null}
public struct Payrails.InitData: Codable {
    public init(version: String, data: String)
    public let version: String  // Version string from backend response
    public let data: String     // Base64-encoded JSON payload from backend response
}
```

### `Payrails.Options`

Runtime options passed to `Configuration`.

```swift theme={null}
public struct Payrails.Options {
    public init(env: Payrails.Env = .production)
    public let env: Payrails.Env
}

public enum Payrails.Env: String {
    case production
    case test
}
```

### `Payrails.Configuration`

Wraps `InitData` and `Options` as the input to `createSession`.

```swift theme={null}
public struct Payrails.Configuration {
    public init(initData: Payrails.InitData, option: Payrails.Options)
    public let initData: Payrails.InitData
    public let option: Payrails.Options
}
```

### `Payrails.createSession(with:onSessionExpired:onRequestStart:)`

Creates and stores a session. All factory methods use the most recently created session.

```swift theme={null}
// Async/await
public static func createSession(
    with configuration: Payrails.Configuration,
    onSessionExpired: SessionExpiredHandler? = nil,
    onRequestStart: RequestStartHandler? = nil
) async throws -> Payrails.Session

// Callback
public static func createSession(
    with configuration: Payrails.Configuration,
    onSessionExpired: SessionExpiredHandler? = nil,
    onRequestStart: RequestStartHandler? = nil,
    onInit: OnInitCallback
)

public typealias OnInitCallback = (Result<Payrails.Session, PayrailsError>) -> Void

public typealias SessionExpiredHandler = (
    @escaping (Result<Payrails.InitData, Error>) -> Void
) -> Void

public typealias RequestStartHandler = (
    Payrails.RequestStartContext,
    @escaping (Payrails.RequestStartDecision) -> Void
) -> Void
```

The `onSessionExpired` closure lets the SDK self-heal when the underlying Payrails execution becomes unreusable (typically: user abandoned a 3DS challenge and the backend execution stayed in `authorizePending`). The merchant supplies a closure that fetches fresh `InitData` from their backend; the SDK swaps its internal config in place — the merchant's `Session` reference and cached buttons / forms keep working unchanged.

```swift theme={null}
let session = try await Payrails.createSession(
    with: Payrails.Configuration(initData: initData, option: .init(env: .production)),
    onSessionExpired: { completion in
        myBackend.fetchPayrailsInit { result in
            completion(result)   // .success(Payrails.InitData) or .failure(Error)
        }
    }
)
```

If the closure is omitted, the SDK logs a warning at init time and cannot recover from a poisoned execution — the next payment attempt against that `Session` will fail naturally.

#### `onRequestStart`

Optional gate invoked once per payment attempt, before the authorization request is sent and before any provider UI (wallet sheet, PayPal sheet, redirect) is presented. Calling `completion(.proceed)` lets the attempt continue; `completion(.refuse())` stops it.

The gate fires for every payment method configured on the session. A handler that only gates one method must call `completion(.proceed)` on the other branches.

```swift theme={null}
public extension Payrails {
    enum RequestStartDecision {
        case proceed
        case refuse(message: String?)

        /// Refuse without a reason.
        public static func refuse() -> RequestStartDecision
    }

    struct RequestStartContext {
        /// The Payrails execution this attempt runs against, when one is known.
        public let executionId: String?
        /// `"card"`, `"payPal"`, `"applePay"`, or any other configured code.
        public let paymentMethodCode: String
        /// Whether the SDK is about to authorize or tokenize.
        public let action: Action

        public enum Action: String {
            case authorize = "AUTHORIZE"
            case tokenize  = "TOKENIZE"
        }
    }
}
```

`Action.tokenize` is reserved. The tokenization flow is not gated in this version, so `action` is always `.authorize` today.

`refuse(message:)`'s message, when supplied, becomes `AuthorizationFailure.message` on the delivered `.validationFailed`. It is passed through verbatim and is not displayed by the SDK.

| Handler behaviour | Result |
| - | - |
| `completion(.proceed)` | Authorization proceeds |
| `completion(.refuse(message:))` | Attempt stopped; delegate receives `.validationFailed` carrying `message` |
| `completion(.refuse())` | Attempt stopped; delegate receives `.validationFailed` with a generic description |
| `completion` not called within 10 seconds | Attempt stopped with a generic description; a warning is logged |
| `completion` called more than once | First answer decides; later calls ignored |

The timeout case does not carry the SDK's diagnostic into `failure.message`: it describes an integration fault rather than something phrased for a customer. Only a deliberate `.refuse(message:)` travels outward.

When the attempt is stopped, no authorization request is sent, `isPaymentInProgress` returns to `false`, and the initiating element's delegate receives `onAuthorizeFailed(_:failure:)` with `failure.code == .validationFailed`.

Omitting the handler leaves the payment path fully synchronous — the SDK skips the gate rather than taking an asynchronous detour.

***

## Session

`Payrails.Session` is returned from `createSession` and is the single typed API surface for headless integrations.

**When to use `query(_:)` vs session methods directly:**

* `query(_:)` — stateless reads of session metadata (holder reference, amount, execution ID, API links, payment method config, stored instruments). Single unified accessor returning a `PayrailsQueryResult` enum.
* **Session methods** — actions and mutations (`executePayment`, `tokenize`, `deleteInstrument`, `updateInstrument`, `update`), device-capability checks (`isApplePayAvailable`), or typed reads where merchants prefer concrete return types over an enum (`getPaymentMethodConfig(_:)`).

Rule of thumb: `query(_:)` reads data; session methods do things, check the device, or return typed values.

```swift theme={null}
public class Payrails.Session {
    // Availability — device capability only
    // Compose with getPaymentMethodConfig(.specific("apple_pay")) for
    // the combined "configured and device capable" signal.
    public var isApplePayAvailable: Bool { get }

    // Payment method configuration
    public func getPaymentMethodConfig(_ filter: PaymentMethodFilter = .all) -> [PayrailsPaymentOption]

    // Payment execution — callback variants
    public func executePayment(
        with type: PaymentType,
        paymentMethodCode: String?,
        saveInstrument: Bool,
        presenter: PaymentPresenter?,
        onResult: @escaping OnPayCallback
    )

    public func executePayment(
        withStoredInstrument instrument: StoredInstrument,
        presenter: PaymentPresenter?,
        onResult: @escaping OnPayCallback
    )

    // Payment execution — async variants
    @MainActor public func executePayment(
        with type: Payrails.PaymentType,
        paymentMethodCode: String?,
        saveInstrument: Bool,
        presenter: PaymentPresenter?
    ) async -> OnPayResult

    @MainActor public func executePayment(
        withStoredInstrument instrument: StoredInstrument,
        presenter: PaymentPresenter?
    ) async -> OnPayResult

    // Tokenization — save an instrument without paying (async + callback)
    public func tokenize(
        _ request: Payrails.TokenizationRequest,
        options: TokenizeOptions = TokenizeOptions()
    ) async throws -> SaveInstrumentResponse

    public func tokenize(
        _ request: Payrails.TokenizationRequest,
        options: TokenizeOptions = TokenizeOptions(),
        onSuccess: @escaping (SaveInstrumentResponse) -> Void,
        onFailed: @escaping (PayrailsError) -> Void,
        onCancelled: @escaping () -> Void
    )

    // Instrument management
    public func deleteInstrument(instrumentId: String) async throws -> DeleteInstrumentResponse
    public func updateInstrument(instrumentId: String, body: UpdateInstrumentBody) async throws -> UpdateInstrumentResponse

    // Session state
    public func query(_ key: PayrailsQueryKey) -> PayrailsQueryResult?
    public func update(_ options: UpdateOptions)
}
```

***

## Payment types

```swift theme={null}
public enum Payrails.PaymentType: String {
    case card
    case applePay
    case payPal
    case genericRedirect
}
```

***

## Factory methods

All factory methods are static methods on `Payrails` and require an active session.

### Card form

```swift theme={null}
public static func createCardForm(
    config: CardFormConfig? = nil,
    showSaveInstrument: Bool = false
) -> Payrails.CardForm
```

`Payrails.CardForm` is a `UIStackView` subclass. Add it to your view hierarchy and constrain with Auto Layout.

### Card payment button

```swift theme={null}
// Card form mode — requires prior createCardForm()
public static func createCardPaymentButton(
    buttonStyle: CardButtonStyle? = nil,
    translations: CardPaymenButtonTranslations
) -> Payrails.CardPaymentButton

// Stored instrument mode
public static func createCardPaymentButton(
    storedInstrument: StoredInstrument,
    buttonStyle: StoredInstrumentButtonStyle? = nil,
    translations: CardPaymenButtonTranslations,
    storedInstrumentTranslations: StoredInstrumentButtonTranslations? = nil
) -> Payrails.CardPaymentButton
```

### Apple Pay button

```swift theme={null}
public static func createApplePayButton(
    type: PKPaymentButtonType,
    style: PKPaymentButtonStyle,
    showSaveInstrument: Bool = false
) -> ApplePayElement
```

### PayPal button

```swift theme={null}
public static func createPayPalButton(showSaveInstrument: Bool = false) -> PaypalElement
```

### Generic redirect button

```swift theme={null}
public static func createGenericRedirectButton(
    buttonStyle: CardButtonStyle? = nil,
    translations: CardPaymenButtonTranslations,
    paymentMethodCode: String
) -> Payrails.GenericRedirectButton
```

### Stored instruments

```swift theme={null}
public static func createStoredInstruments(
    style: StoredInstrumentsStyle? = nil,
    translations: StoredInstrumentsTranslations? = nil,
    showDeleteButton: Bool = false,
    showUpdateButton: Bool = false,
    showPayButton: Bool = false
) -> Payrails.StoredInstruments
```

***

## Static helpers

```swift theme={null}
// Returns all stored instruments (card + PayPal)
public static func getStoredInstruments() -> [StoredInstrument]

// Returns stored instruments for a specific payment type
public static func getStoredInstruments(for type: Payrails.PaymentType) -> [StoredInstrument]

// Runtime session state update
public static func update(_ options: UpdateOptions)

// Query session state
public static func query(_ key: PayrailsQueryKey) -> PayrailsQueryResult?
```

> Instrument management (delete/update) moved to typed Session methods in 1.28.0.
> Use `session.deleteInstrument(instrumentId:)` and `session.updateInstrument(instrumentId:body:)` directly — see the Session block above.

***

## Query API

### `PayrailsQueryKey`

```swift theme={null}
public enum PayrailsQueryKey {
    case executionId
    case holderReference
    case amount
    case binLookup
    case instrumentDelete
    case instrumentUpdate
    case paymentMethodConfig(PaymentMethodFilter)
    case paymentMethodInstruments(type: Payrails.PaymentType)
}
```

### `PaymentMethodFilter`

```swift theme={null}
public enum PaymentMethodFilter {
    case all
    case redirect
    case specific(String)  // paymentMethodCode
}
```

### `PayrailsQueryResult`

```swift theme={null}
public enum PayrailsQueryResult {
    case string(String)
    case amount(PayrailsAmount)
    case link(PayrailsLink)
    case paymentOptions([PayrailsPaymentOption])
    case storedInstruments([StoredInstrument])
}
```

### Supporting types

```swift theme={null}
public struct PayrailsAmount {
    public let value: String
    public let currency: String
}

public struct PayrailsLink {
    public let method: String?
    public let href: String?
}

public struct PayrailsPaymentOption {
    public let paymentMethodCode: String
    public let description: String?
    public let integrationType: String
    public let clientConfig: ClientConfig?

    public struct ClientConfig {
        public let displayName: String?
        public let flow: String?
        public let supportsSaveInstrument: Bool?
        public let supportsBillingInfo: Bool?
    }
}
```

### How to Query Session Data

`Payrails.query(_:)` provides read-only access to the current session's configuration and state. Use it to retrieve the execution ID, payment amount, stored instruments, API links, and more — without reaching into internal session state.

#### When to use `query(_:)` vs Session methods

* **Use `query(_:)`** for stateless reads of session metadata: `.holderReference`, `.amount`, `.executionId`, `.binLookup`, `.paymentMethodConfig(...)`, `.paymentMethodInstruments(...)`.
* **Use Session methods directly** for actions (`executePayment`, `deleteInstrument`, `updateInstrument`, `update`), device-capability checks (`isApplePayAvailable`), or typed reads where merchants prefer concrete return types over an enum (`getPaymentMethodConfig(_:)`).

Rule of thumb: `query(_:)` reads data. Session methods do things, check the device, or return typed values where an enum would add friction.

#### Prerequisites

An active session must exist (created via `Payrails.createSession(with:)`). All queries return `nil` when no session is active.

***

#### Calling `Payrails.query`

```swift theme={null}
let result: PayrailsQueryResult? = Payrails.query(.amount)
```

The return type is `PayrailsQueryResult?`, a Swift enum. Switch on it to extract the typed value:

```swift theme={null}
switch Payrails.query(.amount) {
case .amount(let amount):
    print("Amount:", amount.value, amount.currency)
case .none:
    print("No active session")
default:
    break
}
```

***

#### Available query keys

##### `.executionId`

The execution ID for the current checkout. Pass this to your backend for order correlation.

```swift theme={null}
if case .string(let executionId) = Payrails.query(.executionId) {
    print("Execution ID:", executionId)
    // myBackend.attachExecutionId(executionId)
}
```

##### `.holderReference`

The holder reference (customer identifier) associated with this session.

```swift theme={null}
if case .string(let ref) = Payrails.query(.holderReference) {
    print("Holder reference:", ref)
}
```

##### `.amount`

The payment amount and currency for the current execution.

```swift theme={null}
if case .amount(let payrailsAmount) = Payrails.query(.amount) {
    let display = "\(payrailsAmount.currency) \(payrailsAmount.value)"
    amountLabel.text = display
}
```

##### `.binLookup`

The API link for BIN lookup. Use this to call the lookup endpoint and determine card network, country, and 3DS requirements before payment.

```swift theme={null}
if case .link(let link) = Payrails.query(.binLookup) {
    print("BIN lookup URL:", link.href ?? "")
    print("Method:", link.method ?? "")
}
```

##### `.instrumentDelete`

The API link for deleting a stored instrument.

```swift theme={null}
if case .link(let link) = Payrails.query(.instrumentDelete) {
    // Use link.href and link.method to build the request in your networking layer
}
```

##### `.instrumentUpdate`

The API link for updating a stored instrument (e.g. setting as default).

```swift theme={null}
if case .link(let link) = Payrails.query(.instrumentUpdate) {
    // Use link.href and link.method to build the request
}
```

##### `.paymentMethodConfig(filter:)`

Configuration for available payment methods, filtered by a `PaymentMethodFilter`.

```swift theme={null}
// All payment methods
if case .paymentOptions(let options) = Payrails.query(.paymentMethodConfig(.all)) {
    for option in options {
        print(option.paymentMethodCode, option.clientConfig?.displayName ?? "")
    }
}

// Redirect-based methods only
if case .paymentOptions(let options) = Payrails.query(.paymentMethodConfig(.redirect)) {
    // Build redirect payment buttons dynamically
}

// A specific method by code
if case .paymentOptions(let options) = Payrails.query(.paymentMethodConfig(.specific("ideal"))) {
    let ideal = options.first
    print("iDEAL display name:", ideal?.clientConfig?.displayName ?? "")
}
```

##### `.paymentMethodInstruments(type:)`

The stored instruments for a given payment type.

```swift theme={null}
// Card instruments
if case .storedInstruments(let cards) = Payrails.query(.paymentMethodInstruments(type: .card)) {
    print("Saved cards:", cards.count)
    for card in cards {
        print(" -", card.id, card.type.rawValue)
    }
}

// PayPal instruments
if case .storedInstruments(let paypals) = Payrails.query(.paymentMethodInstruments(type: .payPal)) {
    print("Saved PayPal accounts:", paypals.count)
}
```

***

#### Summary table

| Key | Returns | Description |
| - | - | - |
| `.executionId` | `.string` | Current execution ID |
| `.holderReference` | `.string` | Customer holder reference |
| `.amount` | `.amount(PayrailsAmount)` | Payment amount and currency |
| `.binLookup` | `.link(PayrailsLink)` | BIN lookup API link |
| `.instrumentDelete` | `.link(PayrailsLink)` | Instrument delete API link |
| `.instrumentUpdate` | `.link(PayrailsLink)` | Instrument update API link |
| `.paymentMethodConfig(.all)` | `.paymentOptions([PayrailsPaymentOption])` | All payment method configs |
| `.paymentMethodConfig(.redirect)` | `.paymentOptions([PayrailsPaymentOption])` | Redirect-only methods |
| `.paymentMethodConfig(.specific(code))` | `.paymentOptions([PayrailsPaymentOption])` | Single method config |
| `.paymentMethodInstruments(type:)` | `.storedInstruments([StoredInstrument])` | Saved instruments by type |

***

## Updating session state

```swift theme={null}
public struct UpdateOptions {
    public var amount: PayrailsAmount?
    public init(amount: PayrailsAmount? = nil)
}

// Usage
Payrails.update(UpdateOptions(amount: PayrailsAmount(value: "57.49", currency: "USD")))
```

### How to Update the Checkout Amount

The checkout amount is set when the session is initialized from the init payload. If the amount changes after initialization — for example, the user adds a tip, chooses express shipping, or applies a discount code — you must update both your backend and the SDK in lockstep.

<Warning>
  **Important**

  The SDK amount and the amount recorded in the Payrails execution must match. A mismatch causes the payment to be rejected with a 401 error.
</Warning>

***

#### How amount updates work

Updating the amount is a two-step process:

```
1. Recalculate amount in your UI (e.g. user selects a tip)
2. Call your backend to update the execution amount in Payrails
3. Call Payrails.update(options:) on the client to sync the SDK
4. User taps the pay button — amount is now consistent
```

Both steps must complete before the user initiates payment.

***

#### Step 1: Recalculate the amount

```swift theme={null}
let subtotal = 49.99
let tipPercentage = 0.15
let total = subtotal + (subtotal * tipPercentage)
let formattedTotal = String(format: "%.2f", total)  // "57.49"
let currency = "USD"
```

***

#### Step 2: Update the amount on your backend

Call your backend, which calls the Payrails API to update the execution amount. The exact endpoint and request shape are defined by your backend implementation.

```swift theme={null}
func updateExecutionAmount(value: String, currency: String) async throws {
    // Your backend call — POST /executions/{id}/update or similar
    // This MUST complete before calling Payrails.update()
    try await myBackendClient.updateCheckoutAmount(value: value, currency: currency)
}
```

***

#### Step 3: Update the SDK amount

After the backend confirms the update, sync the SDK:

```swift theme={null}
let newAmount = PayrailsAmount(value: formattedTotal, currency: currency)
Payrails.update(UpdateOptions(amount: newAmount))
```

***

#### Complete example: tip selection

```swift theme={null}
class CheckoutViewController: UIViewController {

    private var selectedTipRate: Double = 0.0

    @IBAction func tipButtonTapped(_ sender: UIButton) {
        let tipRate = tipRate(for: sender.tag)
        selectTip(rate: tipRate)
    }

    private func selectTip(rate: Double) {
        selectedTipRate = rate
        updateAmountDisplay()

        Task {
            await applyTipToPayment(rate: rate)
        }
    }

    private func updateAmountDisplay() {
        let total = calculateTotal(tipRate: selectedTipRate)
        amountLabel.text = formatAmount(total)
    }

    private func applyTipToPayment(rate: Double) async {
        let total = calculateTotal(tipRate: rate)
        let formatted = String(format: "%.2f", total)

        do {
            // Step 1: Update the backend execution
            try await myBackend.updateExecutionAmount(value: formatted, currency: "USD")

            // Step 2: Sync the SDK
            let newAmount = PayrailsAmount(value: formatted, currency: "USD")
            Payrails.update(UpdateOptions(amount: newAmount))

        } catch {
            showError("Failed to apply tip: \(error.localizedDescription)")
        }
    }

    private func calculateTotal(tipRate: Double) -> Double {
        let subtotal = 49.99
        return subtotal + (subtotal * tipRate)
    }
}
```

***

#### After a redirect session recovery

If the user's payment involved a redirect (e.g. PayPal, generic redirect) and the app returned from the background, the session may be restored from the original init payload. In this case:

* Any in-memory amount updates made via `Payrails.update()` are **reset** to the original init payload amount.
* If you need to preserve the updated amount after a redirect, you must re-apply `Payrails.update()` once the session is restored.

***

#### Troubleshooting

**Payment rejected with 401 / authorization error**
The SDK amount does not match the Payrails execution amount. Verify that your backend update completed successfully before calling `Payrails.update()`.

**`Payrails.update()` appears to have no effect**
If there is no active session, the call is silently dropped. Ensure `Payrails.createSession()` has completed successfully before calling `update()`. Check for any `No active Payrails session` log messages.

**Amount label does not update**
`Payrails.update()` updates the internal SDK state; it does not automatically refresh any UI element. Update your amount label independently after recalculating.

***

## Callbacks and results

Payment outcomes are delivered to your `CardPaymentButton` / `CardPaymentForm` / `StoredInstrumentPaymentButton` / `GenericRedirectButton` delegate. Failures arrive as an `AuthorizationFailure` struct — a flat `{ code, message, rawError }` shape that mirrors the Web SDK's `onFailed` payload:

```swift theme={null}
/// Payload passed to `onAuthorizeFailed(_:failure:)` on every delegate protocol.
public struct AuthorizationFailure {
    /// Discriminating code — switch on this to give each outcome the right UX.
    public let code: AuthorizationFailureReason
    /// Human-readable detail. Backend's `errors[0].reason.result` for `.authorizationError`,
    /// generic fallback for the other cases. Never nil.
    public let message: String
    /// Underlying error when one exists (typically populated for `.unknownError`).
    public let rawError: Error?
}

/// String-raw-valued discriminator. Raw values match the Web SDK's
/// `AuthorizationFailureReasons` 1:1.
public enum AuthorizationFailureReason: String {
    /// Authorization rejected — issuer declined, 3DS rejected, fraud blocked, etc.
    case authorizationError  = "AUTHORIZATION_ERROR"
    /// Session token was rejected (HTTP 401 / 403). The merchant must
    /// re-initialise the session; the SDK also fires its `onSessionExpired`
    /// refresh in the background.
    case authenticationError = "AUTHENTICATION_ERROR"
    /// The user intentionally abandoned the flow (e.g. swiped the 3DS sheet away).
    case userCancelled       = "USER_CANCELLED"
    /// Network failure, decode error, encryption failure, polling timeout, or
    /// any other unexpected error. `rawError` carries the underlying error.
    case unknownError        = "UNKNOWN_ERROR"
    /// The merchant's `onRequestStart` handler stopped the payment before it
    /// started — it answered `false`, or did not answer within the timeout.
    /// No authorization request was sent, so this is not a decline.
    case validationFailed    = "VALIDATION_FAILED"
}
```

> Client-side *input* validation never reaches this path: an element early-returns on an invalid form rather than emitting a failure. `.validationFailed` is reserved for an `onRequestStart` handler blocking the attempt.

### Delegate callbacks fired

| Outcome | Delegate method fired |
| - | - |
| Authorization succeeded | `onAuthorizeSuccess(self)` |
| Backend declined / rejected authorization | `onAuthorizeFailed(self, failure: .authorizationError(message:))` |
| Session token expired / rejected | `onAuthorizeFailed(self, failure: .authenticationError)` |
| User dismissed 3DS sheet (no backend terminal confirmed) | `onAuthorizeFailed(self, failure: .userCancelled)` |
| Network / SDK error | `onAuthorizeFailed(self, failure: .unknownError(_))` |
| `onRequestStart` refused the attempt | `onAuthorizeFailed(self, failure: .validationFailed(message:))` |
| Backend execution pending with no action required | `onAuthorizePending(self)` |

When the user dismisses the 3DS sheet (or any other path that leaves the Payrails execution in `authorizePending`), the SDK additionally invokes the merchant's `onSessionExpired` closure (supplied at `createSession`) in the background to rebuild its internal config in place — the merchant's `Session` reference keeps working. If the closure was not supplied, the SDK logs a warning at `createSession` time and the next payment attempt against the `Session` will fail naturally against the dead execution.

***

## Payment presenter protocol

Required to present view controllers during payment (e.g. 3DS challenges):

```swift theme={null}
public protocol PaymentPresenter: AnyObject {
    func presentPayment(_ viewController: UIViewController)
    var encryptedCardData: String? { get set }
}
```

Typically conformed to by a `UIViewController`:

```swift theme={null}
extension MyCheckoutViewController: PaymentPresenter {
    func presentPayment(_ viewController: UIViewController) {
        present(viewController, animated: true)
    }
}
```

***

## Delegate protocols

> `onPaymentButtonClicked` is a notification, not a gate. It tells you the customer tapped, for
> analytics, observability or showing a spinner. It returns `Void` and the SDK does not wait for it,
> so it cannot stop or defer a payment. To make the payment conditional on your own check, use
> [`onRequestStart`](#onrequeststart) — the SDK awaits that one and honours its answer.

### `PayrailsCardPaymentButtonDelegate`

```swift theme={null}
public protocol PayrailsCardPaymentButtonDelegate: AnyObject {
    func onPaymentButtonClicked(_ button: Payrails.CardPaymentButton)
    func onAuthorizeSuccess(_ button: Payrails.CardPaymentButton)
    func onAuthorizePending(_ button: Payrails.CardPaymentButton)
    func onThreeDSecureChallenge(_ button: Payrails.CardPaymentButton)

    /// Fires for every terminal failure of an authorization attempt. The `failure`
    /// payload's `.code` discriminates between issuer decline, authentication
    /// failure, user cancellation, and other errors — see `AuthorizationFailure`.
    func onAuthorizeFailed(_ button: Payrails.CardPaymentButton, failure: AuthorizationFailure)

    /// Optional — default no-op.
    func onStoredInstrumentChanged(_ button: Payrails.CardPaymentButton, instrument: StoredInstrument?)
}
```

<Warning>
  **Breaking change in ONB-739**

  The pre-ONB-739 signature was
  `onAuthorizeFailed(_ button: Payrails.CardPaymentButton)` with no payload.
  Merchants migrating from earlier versions must update their delegate
  conformance to take `failure: AuthorizationFailure` and (if they relied on
  session-expiry signaling) supply the `onSessionExpired` closure to
  `createSession`. See the card-payment-flow documentation for a migration example.
</Warning>

### `PayrailsCardFormDelegate`

```swift theme={null}
public protocol PayrailsCardFormDelegate: AnyObject {
    func cardForm(_ view: Payrails.CardForm, didCollectCardData data: String)
    func cardForm(_ view: Payrails.CardForm, didFailWithError error: Error)
    func cardForm(_ view: Payrails.CardForm, didChangePreferredScheme change: PreferredSchemeChange)
}
```

`didChangePreferredScheme` is optional — a protocol-extension default provides a no-op, so implementations that predate co-branded support continue to compile. See [Co-branded cards](#co-branded-cards).

### Co-branded cards

Emitted by the card form when a card carries two payment networks. Requires `featureConfig.coBrandedCardsRollout` and a `links.binLookup` entry in the session init response; absent either, none of this is produced.

```swift theme={null}
public struct PreferredSchemeChange: Equatable {
    public let preferredScheme: String?
    public let cardSchemes: [CardScheme]
}

public struct CardScheme: Equatable {
    public let code: String
    public let name: String
    public let logoUrl: URL?
    public let selected: Bool
}
```

| Field | Type | Notes |
| - | - | - |
| `preferredScheme` | `String?` | Scheme code sent with the authorization. `nil` when the card is not co-branded |
| `cardSchemes` | `[CardScheme]` | Both available schemes. Empty when the card is not co-branded |
| `code` | `String` | `"visa"`, `"cartesBancaires"`, `"mada"`, … |
| `name` | `String` | Display name |
| `logoUrl` | `URL?` | Brand logo, when the scheme has one |
| `selected` | `Bool` | Whether this scheme is the current choice |

`cardForm(_:didChangePreferredScheme:)` fires when a co-branded BIN resolves, when the shopper selects a different brand, and when the card number changes such that co-branded state clears. The clearing case delivers an empty `cardSchemes` and a `nil` `preferredScheme`.

The selected scheme is carried into the authorization request by the SDK. Callers do not pass it to any payment method.

Styling is `CardFormStyle.cardBrandSelector: CardBrandSelectorStyle?`; `nil` keeps the SDK default. The selector's heading and subheading are `CardTranslations.Labels.cardBrandSelectorTitle` and `.cardBrandSelectorSubtitle`; each falls back to an SDK default when `nil`.

### `PayrailsApplePayButtonDelegate`

```swift theme={null}
public protocol PayrailsApplePayButtonDelegate: AnyObject {
    func onPaymentButtonClicked(_ button: Payrails.ApplePayButton)
    func onAuthorizeSuccess(_ button: Payrails.ApplePayButton)
    func onAuthorizeFailed(_ button: Payrails.ApplePayButton, failure: AuthorizationFailure)
    func onPaymentSessionExpired(_ button: Payrails.ApplePayButton)
}
```

<Warning>
  **Deprecated**

  `onAuthorizeFailed(_ button: Payrails.ApplePayButton)` with no payload. A default
  implementation forwards to it, so existing integrations keep receiving failures, but only the
  `failure:` variant carries the discriminating `code` — the sole way to distinguish an
  `onRequestStart` block from an issuer decline.
</Warning>

### `PayrailsPayPalButtonDelegate`

```swift theme={null}
public protocol PayrailsPayPalButtonDelegate: AnyObject {
    func onPaymentButtonClicked(_ button: Payrails.PayPalButton)
    func onAuthorizeSuccess(_ button: Payrails.PayPalButton)
    func onAuthorizeFailed(_ button: Payrails.PayPalButton, failure: AuthorizationFailure)
    func onPaymentSessionExpired(_ button: Payrails.PayPalButton)
}
```

<Warning>
  **Deprecated**

  `onAuthorizeFailed(_ button: Payrails.PayPalButton)` with no payload. Same
  forwarding default and same reasoning as Apple Pay above.
</Warning>

### `PayrailsStoredInstrumentsDelegate`

```swift theme={null}
public protocol PayrailsStoredInstrumentsDelegate: AnyObject {
    func storedInstruments(_ view: Payrails.StoredInstruments, didSelectInstrument instrument: StoredInstrument)
    func storedInstruments(_ view: Payrails.StoredInstruments, didCompletePaymentForInstrument instrument: StoredInstrument)
    func storedInstruments(_ view: Payrails.StoredInstruments, didFailPaymentForInstrument instrument: StoredInstrument, error: PayrailsError)
    func storedInstruments(_ view: Payrails.StoredInstruments, didRequestDeleteInstrument instrument: StoredInstrument)
    func storedInstruments(_ view: Payrails.StoredInstruments, didRequestUpdateInstrument instrument: StoredInstrument)
}
```

***

## Error handling

### `PayrailsError`

```swift theme={null}
public enum PayrailsError: Error, LocalizedError {
    case authenticationError
    case sdkNotInitialized
    case missingData(String?)
    case invalidDataFormat
    case invalidCardData
    case unknown(error: Error?)
    case unsupportedPayment(type: Payrails.PaymentType)
    case incorrectPaymentSetup(type: Payrails.PaymentType)
    case pollingFailed(String)
    case failedToDerivePaymentStatus(String)
    case finalStatusNotFoundAfterLongPoll(String)
    case longPollingFailed(underlyingError: Error?)
}
```

`PayrailsError` conforms to `LocalizedError`; use `error.errorDescription` for a human-readable message.

***

## Tokenization

Saves a payment method as a reusable instrument without charging the customer. `tokenize` is unified across methods: the `TokenizationRequest` case selects which instrument to tokenize and carries what that method needs. Both overloads live on `Payrails.Session` (see [Session](#session)), alongside `executePayment`.

```swift theme={null}
public enum Payrails.TokenizationRequest {
    case applePay(presenter: PaymentPresenter)  // SDK presents the Apple Pay sheet
    case card(CardForm)                          // SDK reads + encrypts the embedded card form
}

public struct TokenizeOptions {
    public let storeInstrument: Bool    // default false
    public let futureUsage: FutureUsage // default .cardOnFile
}

public enum FutureUsage: String {
    case cardOnFile
    case subscription
    case unscheduledCardOnFile
}

public struct SaveInstrumentResponse: Decodable {
    public let id: String             // stable Payrails instrument id
    public let createdAt: String
    public let holderId: String
    public let paymentMethod: String  // e.g. "applePay", "card"
    public let status: String
    public let data: InstrumentData
    public let fingerprint: String?
    public let futureUsage: String?
}
```

```swift theme={null}
// Apple Pay — async; `self` conforms to PaymentPresenter
let response = try await session.tokenize(
    .applePay(presenter: self),
    options: TokenizeOptions(storeInstrument: true)
)
let instrumentId = response.id
```

***

## Instrument management

```swift theme={null}
public struct UpdateInstrumentBody: Codable {
    // Fields depend on the update operation (e.g. isDefault)
}

public struct DeleteInstrumentResponse: Codable {
    public let success: Bool
}

public struct UpdateInstrumentResponse: Codable {
    // Returned data depends on the operation
}
```

Call the typed Session methods to manage instruments:

```swift theme={null}
let delete = try await session.deleteInstrument(instrumentId: id)
let update = try await session.updateInstrument(
    instrumentId: id,
    body: UpdateInstrumentBody(default: true)
)
```

***

## Card form configuration

See [Styling Guide](/docs/orchestration/checkout-sdks/ios/styling-guide) for full details.

```swift theme={null}
public struct CardFormConfig {
    public init(
        showNameField: Bool = false,
        showSaveInstrument: Bool = false,
        showCardIcon: Bool = false,
        showRequiredAsterisk: Bool = true,
        cardIconAlignment: CardIconAlignment = .left,
        fieldVariant: FieldVariant = .outlined,
        layout: CardLayoutConfig? = nil,
        styles: CardFormStylesConfig? = nil,
        translations: CardTranslations? = nil
    )
}

public enum FieldVariant {
    case outlined
    case filled
}

public enum CardIconAlignment {
    case left
    case right
}
```

***

## Debug

```swift theme={null}
public extension Payrails {
    struct Debug {
        // Returns a SwiftUI view displaying the parsed SDK config and logs
        public static func configViewer() -> some View
    }
}

// Logs a message to both the Xcode console and the on-screen LogStore
public static func log(_ items: Any..., separator: String, terminator: String)
```


## Related topics

- [SDK API Reference](/docs/orchestration/checkout-sdks/android/api-reference.md)
- [API References](/docs/orchestration/checkout-sdks/react-native/api-references.md)
