Skip to main content

The three building blocks

1. Session

The Session (Payrails.Session) is the single source of truth for a checkout. It holds:
  • The parsed init payload (amounts, payment method configurations, vault settings)
  • The active execution ID
  • The holder reference
  • Links to backend API actions (BIN lookup, instrument management)
You create a session once per checkout by calling Payrails.createSession(with:). All elements and factory methods draw their configuration from the current session — there is no need to pass it around explicitly.
A session does not persist across app launches. When the user starts a new checkout, create a new session.

2. Elements

Elements are UIKit views that the SDK manages. You obtain them via factory methods on Payrails: All elements are UIView subclasses; add them to your view hierarchy with Auto Layout or frames.
Payrails.createCardPaymentButton requires that createCardForm has been called first. The form and button are linked automatically.

3. Delegates

Delegates are protocols your view controller (or any object) conforms to in order to receive payment lifecycle events. Each element type has a corresponding delegate: Assign the delegate before adding the element to the window.

Payment flows

Card payment (new card)

Stored instrument payment

Apple Pay

PayPal

Generic redirect


Tokenization

Tokenization saves a payment method as a reusable Payrails instrument without charging the customer. It returns a stable instrument id that identifies the saved method for later use. This exists to support a two-step model: tokenize first, run the resulting instrument through an external decision — a saved-card list, a subscription setup — and only then, if at all, charge it with executePayment. Charging is a separate, deliberate action, never a side effect of tokenizing. Tokenization is unified across payment methods. The same session.tokenize call handles Apple Pay (the SDK presents the Apple Pay sheet) and cards (the SDK encrypts the embedded card form), selected by the TokenizationRequest case, and both return the same SaveInstrumentResponse. Adding a method later does not change how the call is made. Tokenization is distinct from pay-and-save: pay-and-save (the storeInstrument toggle on a payment) charges the customer and vaults the method in one step, whereas tokenization vaults without any charge.

How to tokenize a card

Tokenization saves a card to the Payrails vault without triggering an immediate payment. Use this when you want to store a card for future purchases (subscriptions, one-click checkout, etc.).

Prerequisites

  • An active Payrails session (see Quick Start)
  • Vault configuration present in the init payload (providerConfigId)
  • A holderReference in the session config (required to associate the card with a customer)

How tokenization works

The card form collects and encrypts the card fields client-side using PayrailsCSE. The encrypted blob is sent to the Payrails vault, which returns an instrument ID. Your backend can then use that instrument ID for future payments without ever handling raw card data. There are two paths:

Path 1: Tokenize without payment

Step 1: Create the card form with save toggle
Step 2: Collect the card and tokenize
The card form collects and encrypts the data. You drive tokenization by calling tokenize on the session directly after collecting:
Step 3: Choose a FutureUsage
FutureUsage tells the vault how the instrument will be used for network-mandated storage rules:

Path 2: Pay and save simultaneously

Enable the save toggle on the card form. The user checks the toggle and taps Pay; the SDK performs the payment and vaults the card in one call.
The SDK automatically includes storeInstrument: true in the payment request when the toggle is checked. No additional code is needed.

Using the saved instrument ID

After tokenization, the SaveInstrumentResponse contains the instrument ID:

Verification checklist

  • providerConfigId is present in the init payload
  • holderReference is present in the init payload
  • TokenizeOptions.storeInstrument is true
  • FutureUsage matches the intended use case
  • Your backend associates the returned instrument ID with the customer record

Troubleshooting

“Vault configuration with providerConfigId is required” The init payload does not include vault configuration. Ensure your backend passes the correct Payrails environment and merchant configuration. “holderReference is required for tokenization” The holder reference is missing from the init payload. Contact your Payrails integration engineer to verify the checkout initialization call. Card form delegate not being called Make sure cardForm.delegate = self is set before the user triggers collection.

3D Secure

When a card payment requires a 3DS challenge, the SDK presents an SFSafariViewController. Your view controller must conform to PaymentPresenter and implement presentPayment(_:):
The SDK handles the rest: it polls the Payrails API until a final status is received, then calls the appropriate delegate callback.
Set payButton.presenter = self before the user taps the button.

CardPaymentButton modes

Payrails.CardPaymentButton operates in two modes: You can switch between modes at runtime using setStoredInstrument(_:) and clearStoredInstrument().

Stored instruments and bindCardPaymentButton

Payrails.StoredInstruments can be bound to a single CardPaymentButton:
When a user selects an instrument from the list, the button automatically switches to stored instrument mode. When deselected, it reverts to card form mode. This pattern lets you render one card form and one pay button that handles both flows without conditional logic in your view controller.

The pre-authorization gate

Every element — card form, card button, Apple Pay, PayPal, generic redirect, stored instrument — routes its payment through a single Session method. That convergence is what makes one merchant-supplied gate able to cover all of them, present and future, rather than each element carrying its own hook. The gate sits before the authorization request and before any provider UI. That position is the whole point: a merchant revalidating a voucher, wallet balance or loyalty points needs the answer to arrive while the customer is still on the checkout screen, not after they have approved a payment in PayPal. Validating when the element is first drawn would answer against a basket the customer can still change; the further the tap drifts from the check, the staler the answer. Two design consequences follow. Silence is a block, not a pass. If the handler never answers, the SDK stops the payment after ten seconds rather than proceeding. A gate whose failure mode is “authorize anyway” gives no guarantee at all, and the alternative — an element spinning indefinitely because a merchant endpoint hung — is worse than a refused payment the customer can retry. A block is not a decline. It surfaces as AuthorizationFailureReason.validationFailed, distinct from authorizationError, so a merchant’s own decision never lands in their analytics as an issuer rejection. Nothing reached the backend, so there is no payment attempt to reconcile. The refusal carries its own reason. .refuse(message:) rather than a bare false, because only the merchant knows why they refused — an expired voucher reads differently to a changed basket — and only they can phrase it for their customer. The message arrives as AuthorizationFailure.message, the same place all other failure text comes from, so it needs no separate channel and no correlation by executionId. The SDK’s own timeout diagnostic is deliberately not delivered this way: it describes an integration fault, not something a customer should read. The handler receives the payment method code and can therefore gate one method while leaving the rest untouched. It is opt-in: sessions created without it keep a fully synchronous payment path.

Why not onPaymentButtonClicked?

The two hooks look adjacent but answer different questions, and conflating them is the mistake worth avoiding: onPaymentButtonClicked is deliberately a notification. It cannot gate anything, because the SDK never looks at it and does not wait — work started inside it races the authorization rather than preceding it. The Web SDK draws the same line between its buttonClicked and requestStart events.

How to run a merchant check before 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 The pre-authorization gate.

1. Register the handler

Supply it at createSession time, alongside onSessionExpired:

2. Gate only the methods you care about

The handler fires for every payment method on the session. Call completion(.proceed) on the branches you are not gating, or those payments will be blocked too:

3. Answer explicitly, even on failure

The SDK waits ten seconds, then blocks the payment and logs a warning. Answer explicitly on your error paths so the outcome is your decision rather than a timeout:

4. Handle the block in your delegate

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

Reference

Only a deliberate .refuse(message:) reaches failure.message. The timeout logs its diagnostic instead of surfacing it — it describes an integration fault rather than anything phrased for a customer. Refusing never sends an authorization request and returns the element to its idle state.

Co-branded cards

Some cards carry two payment networks at once — a domestic scheme such as cartesBancaires, mada or dankort alongside visa or mastercard. The same card can be routed either way, and the two routes differ in cost and in which rules apply. EU regulation puts that choice with the shopper rather than the merchant, which is why the SDK surfaces it rather than resolving it silently. The lookup runs on the BIN — the first eight digits — not the full number, so it happens while the shopper is still typing and before anything sensitive is complete. Three consequences are worth understanding. The shopper chooses, and the SDK carries it. When two schemes resolve, the form shows a selector with one preselected. Whatever is selected travels with the authorization request automatically. Your integration never sets the scheme on a payment; didChangePreferredScheme exists so your own UI and analytics can follow along, not so you can forward the value. Clearing is a state change too. If the shopper edits the number so it no longer resolves to a co-branded BIN, the callback fires again with an empty cardSchemes and a nil preferredScheme. That is the signal to undo whatever the earlier call made you draw. An integration that reads preferredScheme without checking cardSchemes leaves stale UI behind — the most common mistake with this callback. It is off unless the backend turns it on. Two prerequisites, both in the session init response: a featureConfig.coBrandedCardsRollout percentage, and a links.binLookup entry for the form to call. Absent either, the card form behaves exactly as it did before — no selector, no callback, no BIN lookup. Nothing in your app switches this on. The delegate method is optional, with a protocol-extension default, so adding it was source-compatible for integrations that implement only the original two PayrailsCardFormDelegate callbacks.

How to support co-branded cards

A co-branded card carries two payment networks — a domestic scheme (cartesBancaires, mada, dankort) alongside an international one (visa, mastercard). EU regulation requires the shopper to choose which one the payment runs on. The SDK handles the choice; this guide covers what your integration has to do around it. For why the SDK works this way, see Co-branded cards above.

1. Confirm the feature is switched on

Co-branded support is gated on two things, both outside your app:
  • featureConfig.coBrandedCardsRollout in the session init response
  • a links.binLookup entry in the same response
Without both, the card form behaves exactly as before — no selector, no callback. Ask your Payrails contact to enable the rollout for your workspace.

2. Handle the scheme change

The callback is optional: a protocol-extension default means existing PayrailsCardFormDelegate implementations keep compiling. Implement it when you want to react to the choice.
It fires in three situations, and the third is the one integrations forget: Treat the third as “this is no longer a co-branded card” and reset any UI you drew from an earlier call. Reading change.preferredScheme without checking cardSchemes leaves stale hints on screen.

3. Style the selector

The selector renders with the SDK’s defaults, so existing styling is unaffected. Override it through CardFormStyle:
A nil value leaves the default in place — see the styling guide for the full token set.

4. Localize the title

The selector’s heading and subheading come from CardTranslations.Labels. Supply localized strings rather than relying on the built-in English defaults ("Card Brand" and its subtitle):
Either may be left nil to keep the SDK default for that line.

What you do not have to do

You do not send the scheme yourself. The form carries the shopper’s choice into the authorization request. didChangePreferredScheme is for your UI and analytics — the payment already carries it. You do not build the selector. It is part of the card form.

Reference


Security model

  • Card data is never exposed in plaintext. The SDK encrypts card fields using PayrailsCSE (a Skyflow vault client) before they leave the device.
  • The Session token is short-lived. Tokens are fetched by your backend and passed to the SDK; they are not stored persistently.
  • Logging is off by default. The debug overlay and Payrails.log output are only visible when explicitly enabled. See Troubleshooting for details.

Element lifecycle

Elements hold a weak reference to the session. They are safe to create in viewDidLoad and will be deallocated with the view controller. You do not need to manually tear them down. If the user navigates away during a payment, the in-flight Task is cancelled in deinit of CardPaymentButton, preventing dangling callbacks.

Next steps

Last modified on September 30, 2026