SDK API Reference

SDK API Reference

Current version: 3.0.0
Minimum deployment target: iOS 14.0
Swift version: 5.0+
Distribution: Swift Package Manager (signed XCFramework)


Installation

Swift Package Manager, using the package URL:

https://github.com/payrails/ios-sdk.git
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.

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.

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.

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.

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

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.

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 behaviourResult
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 secondsAttempt stopped with a generic description; a warning is logged
completion called more than onceFirst 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.

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

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

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

// 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

public static func createApplePayButton(
    type: PKPaymentButtonType,
    style: PKPaymentButtonStyle,
    showSaveInstrument: Bool = false
) -> ApplePayElement

PayPal button

public static func createPayPalButton(showSaveInstrument: Bool = false) -> PaypalElement

Generic redirect button

public static func createGenericRedirectButton(
    buttonStyle: CardButtonStyle? = nil,
    translations: CardPaymenButtonTranslations,
    paymentMethodCode: String
) -> Payrails.GenericRedirectButton

Stored instruments

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

Static helpers

// 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

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

PaymentMethodFilter

public enum PaymentMethodFilter {
    case all
    case redirect
    case specific(String)  // paymentMethodCode
}

PayrailsQueryResult

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

Supporting types

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

let result: PayrailsQueryResult? = Payrails.query(.amount)

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

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.

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.

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

The payment amount and currency for the current execution.

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.

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.

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

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.

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

// 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

KeyReturnsDescription
.executionId.stringCurrent execution ID
.holderReference.stringCustomer 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

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.

Important: The SDK amount and the amount recorded in the Payrails execution must always match. A mismatch causes the payment to be rejected with a 401 error.


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

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.

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:

let newAmount = PayrailsAmount(value: formattedTotal, currency: currency)
Payrails.update(UpdateOptions(amount: newAmount))

Complete example: tip selection

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:

/// 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

OutcomeDelegate method fired
Authorization succeededonAuthorizeSuccess(self)
Backend declined / rejected authorizationonAuthorizeFailed(self, failure: .authorizationError(message:))
Session token expired / rejectedonAuthorizeFailed(self, failure: .authenticationError)
User dismissed 3DS sheet (no backend terminal confirmed)onAuthorizeFailed(self, failure: .userCancelled)
Network / SDK erroronAuthorizeFailed(self, failure: .unknownError(_))
onRequestStart refused the attemptonAuthorizeFailed(self, failure: .validationFailed(message:))
Backend execution pending with no action requiredonAuthorizePending(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):

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

Typically conformed to by a UIViewController:

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 — the SDK awaits that one and honours its answer.

PayrailsCardPaymentButtonDelegate

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?)
}

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.

PayrailsCardFormDelegate

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

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.

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
}
FieldTypeNotes
preferredSchemeString?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
codeString"visa", "cartesBancaires", "mada", …
nameStringDisplay name
logoUrlURL?Brand logo, when the scheme has one
selectedBoolWhether 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

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)
}

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.

PayrailsPayPalButtonDelegate

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)
}

Deprecated: onAuthorizeFailed(_ button: Payrails.PayPalButton) with no payload. Same
forwarding default and same reasoning as Apple Pay above.

PayrailsStoredInstrumentsDelegate

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

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), alongside executePayment.

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?
}
// 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

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:

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 for full details.

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

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)

What’s Next

Did this page help you?