SDK Concepts
This page explains why the Payrails Android SDK is designed the way it is. Understanding these concepts helps you integrate correctly and debug issues faster.
Mental Model
The SDK is built around three layers:
┌─────────────────────────────────────────────────────────┐
│ Your App (Compose) │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌─────────────┐ ┌────────────────┐ │
│ │ CardForm │ │ CardPayment │ │ Stored │ │
│ │ │ │ Button │ │ Instruments │ │
│ └────┬─────┘ └──────┬──────┘ └───────┬────────┘ │
│ │ │ │ │
│ └────────────────┼───────────────────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ Session │ │
│ └──────┬──────┘ │
│ │ │
├─────────────────────────┼───────────────────────────────┤
│ Payrails API │
└─────────────────────────────────────────────────────────┘
Session
A Session is the foundation. It holds your initialization data, manages communication with the Payrails API, and tracks payment state. You create one session per checkout flow.
val session = Payrails.createSession(configuration)All UI elements (CardForm, CardPaymentButton, StoredInstruments) are created after the session and operate within its context. If the session doesn't exist, element creation throws an error.
Elements Are Decoupled
CardForm and CardPaymentButton are independent composable elements. They don't need to be created together or rendered in the same container. The SDK links them automatically:
CardForm ◄─── auto-linked ───► CardPaymentButton
This design lets you:
- Place the form and button in different parts of your layout
- Create the button before the form (or vice versa)
- Reuse the same button for both card form payments and stored instrument payments
Delegates (Event Callbacks)
The SDK uses the delegate pattern for event callbacks. Each element type has a delegate interface:
| Element | Delegate | Key callbacks |
|---|---|---|
CardPaymentButton | PayrailsCardPaymentButtonDelegate | onAuthorizeSuccess, onAuthorizeFailed, onThreeDSecureChallenge, onStateChanged |
Delegates are set on the element instance, not passed as constructor parameters. This keeps the creation API clean and allows you to change handlers at runtime.
Payment Flow
Card Payment (Happy Path)
User fills card form
│
▼
User taps "Pay Now"
│
▼
SDK validates card data ──── Invalid? ──► Show field errors
│ (auto-clear on fix)
│ Valid
▼
SDK encrypts card data (via CSE)
│
▼
SDK calls authorize API
│
├──── Success ──────────────► onAuthorizeSuccess
│
├──── 3DS Required ─────────► Open browser (Custom Tab)
│ │
│ User completes 3DS
│ │
│ SDK polls for result
│ │
│ ├──── Success ──► onAuthorizeSuccess
│ └──── Failure ──► onAuthorizeFailed
│
└──── Failure ──────────────► onAuthorizeFailed
Stored Instrument Payment
When a stored instrument is selected, the button bypasses card form validation entirely:
User selects stored instrument
│
▼
CardPaymentButton enters "stored instrument mode"
│
▼
User taps "Pay Now"
│
▼
SDK authorizes with stored instrument
│
├──── Success ──► onAuthorizeSuccess
└──── Failure ──► onAuthorizeFailed
If the user starts typing in the card form, the stored instrument selection is automatically cleared and the button returns to "form mode."
3DS (3D Secure)
3DS is a security protocol that adds an extra verification step for card payments. Here's what happens under the hood:
The Flow
- SDK requests authorization — The Payrails API may respond with a 3DS challenge URL
- Browser opens — The SDK opens the URL in a Chrome Custom Tab (preferred) or the system browser
- User completes challenge — The user interacts with their bank's 3DS page
- User returns to app — The Custom Tab closes or the user navigates back
- SDK polls for result — The SDK checks the execution status until it reaches a terminal state
- Result delivered —
onAuthorizeSuccessoronAuthorizeFailedfires
Why Custom Tabs (Not WebView)?
The SDK uses Chrome Custom Tabs instead of WebView for 3DS because:
- Bank compatibility — Some issuer/ACS pages block or behave incorrectly in embedded WebViews
- Security — Custom Tabs provide better isolation than WebViews, reducing compliance risk
- User experience — Deep-link returns are more reliable from Custom Tabs; users are less likely to get stuck
Background Handling
When a user backgrounds your app during 3DS (e.g., switches to their banking app for OTP), the SDK uses a foreground service to keep polling alive. This ensures the payment result is captured even if the user takes time to complete the challenge.
Session Recovery
If the 3DS flow is abandoned (user closes the browser without completing) or remains non-terminal past the reconciliation window:
- SDK emits
onAuthorizationFailed - If
onSessionExpiredis configured, SDK calls it to get fresh init data - SDK refreshes the session state, allowing the user to retry
Configure this in your session setup:
val configuration = Configuration(
initData = initData,
option = Options(
redirectSessionLifecycle = RedirectSessionLifecycle(
onSessionExpired = {
val refreshed = fetchInitPayloadFromBackend()
InitData(version = refreshed.version, data = refreshed.data)
}
)
)
)Button Modes
CardPaymentButton operates in two mutually exclusive modes:
┌─────────────────────────────────────────────────────┐
│ CardPaymentButton │
│ │
│ ┌──────────────────┐ ┌───────────────────────┐ │
│ │ Form Mode │ │ Stored Instrument Mode│ │
│ │ │ │ │ │
│ │ Validates card │ │ Skips card form │ │
│ │ form, encrypts, │◄──►│ Uses stored data │ │
│ │ then authorizes │ │ directly │ │
│ └──────────────────┘ └───────────────────────┘ │
│ │
│ Switching: │
│ • setStoredInstrument() → enters stored mode │
│ • User types in card form → returns to form mode │
│ • clearStoredInstrument() → returns to form mode │
└─────────────────────────────────────────────────────┘
This means a single CardPaymentButton instance handles both new card payments and stored instrument payments. You don't need separate buttons.
Security Model
Card Data
Card data never leaves the device in plain text. The flow is:
- User enters card details into
CardForm(Compose text fields) - On payment, the SDK uses the CSE (Client-Side Encryption) library to encrypt card data
- Only the encrypted payload is sent to the Payrails API
- The SDK never stores raw card data beyond the current form state
Client Context
By default, the SDK collects device metadata (OS type, screen size, timezone, language) and attaches it to authorization requests. This data helps with fraud detection and 3DS risk assessment.
You can opt out:
val configuration = Configuration(
initData = initData,
option = Options(collectMetadata = false)
)Element Lifecycle
Elements are tied to the session lifecycle:
createSession() ──► Session active
│
├── createCardForm()
├── createCardPaymentButton()
├── createStoredInstruments()
│
▼
Activity destroyed (isFinishing = true)
│
▼
session.destroy() called automatically
│
▼
All element references become stale
The SDK registers an ActivityLifecycleCallbacks listener when you create a BrowserPaymentPresenter. When the bound Activity is destroyed (and isFinishing is true), the SDK automatically cleans up the session and element state.
Updated about 14 hours ago