iOS SDK
The Paypercut iOS SDK is a drop-in native payment sheet — card, Apple Pay, and saved cards — that you present from your app with a single call. The customer pays inside a native SwiftUI sheet, and the SDK hands you back a payment method for your backend to charge.
Card details are captured directly into Paypercut's PCI-compliant vault on-device — they never pass through your app or your servers.
Why the native SDK?
- Native UX — a SwiftUI sheet that renders card, Apple Pay, and saved cards, themed to your brand.
- Minimal PCI scope — the PAN is captured straight into Paypercut's vault; it never touches your app or servers.
- You stay in control of the charge — the SDK produces a payment method; your backend confirms the payment with your secret key.
- Localized — the sheet ships in 18 languages and follows the device locale (or a locale you pass).
How It Works
The SDK is checkout-less — you supply the transaction context directly, with no checkout to create first.
- Your app presents the payment sheet with your publishable key and the amount / currency.
- The customer enters card details (or taps Apple Pay / picks a saved card) and confirms. 3-D Secure runs automatically when required.
- The SDK returns a payment method (
pm_…) viaonResult. - Your backend confirms the payment with that payment method id using your secret key.
The SDK does not move money. It collects payment details and returns a payment method; charging it is a server-to-server call from your backend.
Prerequisites
- A Paypercut merchant account — Sign up and get your publishable key (
pk_test_…/pk_live_…) from API Keys. - The transaction amount and currency — you pass these to the SDK directly. Optionally pass saved cards you've fetched server-to-server and a known customer email.
- (For Apple Pay) an Apple Pay merchant identifier registered with Paypercut — see Apple Pay setup.
Installation
Add the package in Xcode (File ▸ Add Package Dependencies…) or in your Package.swift:
dependencies: [
.package(url: "https://github.com/paypercut/paypercut-ios-sdk.git", from: "2.0.0"),
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "Paypercut", package: "paypercut-ios-sdk"),
]
),
]
The on-device tokenization dependency (Paypercut's PCI-compliant vault) is resolved transitively — you don't add it yourself.
Requirements: iOS 15+, SwiftUI.
Basic Integration
import SwiftUI
import Paypercut
struct CheckoutScreen: View {
@State private var paypercut = Paypercut(
publishableKey: "pk_test_…", // your publishable key
amount: 2500, // total in minor units (2500 = €25.00)
currency: "EUR",
applePayMerchantIdentifier: "merchant.com.yourcompany.yourapp" // omit to hide Apple Pay
)
var body: some View {
paypercut.paymentSheet { result in
switch result {
case let .created(pm): confirmOnYourBackend(pm.paymentMethodId)
case let .blik(code): confirmBlikOnYourBackend(code)
case .cancelled: dismiss()
case let .failed(message): showError(message)
}
}
}
}
The sheet renders the enabled methods, collects the details, and returns a payment method. Charging it is a call from your backend.
Configuration
Paypercut — only publishableKey, amount, and currency are required:
| Parameter | Type | Default | Description |
|---|---|---|---|
publishableKey |
String |
required | Your Paypercut publishable key (pk_test_… / pk_live_…) — resolves your account |
amount |
Int |
required | Transaction total in minor units (e.g. 2500 = €25.00; 0 for a setup-mode card save) |
currency |
String |
required | ISO 4217 code (e.g. "EUR", "PLN") |
currencyScale |
Int |
2 |
Minor-unit exponent for the currency (2 for EUR/USD, 0 for JPY) |
mode |
PaymentMode |
.payment |
.payment to charge now, .setup to save a card for later |
paymentMethodTypes |
[String] |
["card"] |
Which methods to render (e.g. ["card"], ["card", "blik"]). Wallets are gated by device + account, not this list |
savedPaymentMethods |
[SavedPaymentMethodInput] |
[] |
Saved cards to offer for one-tap reuse (fetch them server-to-server). Rendered only when non-empty |
allowSaveCard |
Bool |
true |
Show the "save card" checkbox on the card form. In setup mode it is always shown as required consent |
collectBillingAddress |
Bool |
true |
Collect a billing address in the card form |
billingAddress |
BillingAddress? |
nil |
Pre-fill the billing address. When set, the section renders collapsed to a summary (expandable); when absent, expanded |
customerEmail |
String? |
nil |
A known buyer email. Hides the sheet's email field and feeds billing + 3-D Secure |
customer |
String? |
nil |
A known customer id (01…), when you've selected one. Attributes the payment method to that customer |
environment |
PaypercutEnvironment |
.prod |
.dev / .stage / .prod |
locale |
String? |
device locale | BCP-47 tag controlling the sheet's language (e.g. "pl", "de") |
appearance |
Appearance |
default | Branding overrides — see Appearance |
applePayMerchantIdentifier |
String? |
nil |
Your Apple Pay merchant id (merchant.…). Omit to hide Apple Pay |
enableLogging |
Bool |
false |
Verbose logging for development only — never enable in production |
proceedOnThreeDSUnavailable |
Bool |
false |
When 3-D Secure comes back unavailable, proceed with the payment (no liability shift) instead of failing. Defaults to fail-closed |
You pass the amount and currency directly; saved cards are the ones you supply. Which methods render is driven by paymentMethodTypes plus device / account support for wallets.
Appearance
Match the sheet to your brand:
let paypercut = Paypercut(
publishableKey: "pk_test_…",
amount: 2500,
currency: "EUR",
appearance: Appearance(
theme: .system, // .system | .light | .dark
brandColor: 0xFF6366F1, // ARGB — CTA / brand colour (Pay button, save checkbox)
brandColorContrast: 0xFF021A1B, // ARGB — content on the brand (e.g. Pay button label)
accentColor: 0xFF10B981, // ARGB — interactive accent (selected radio, link, cursor)
accentColorContrast: 0xFF021A1B, // ARGB — content on the accent
borderRadius: 12, // points — inputs & buttons
fontFamily: "Inter" // any font bundled in your app; nil = system font
)
)
Re-running 3-D Secure
Confirmation is server-to-server: your backend confirms the payment against the Paypercut API with your secret key. Sometimes that confirm needs a fresh 3-D Secure authentication for the card — the API responds with payment_status: requires_action and a use_sdk next action (use_sdk.sdk == "three_d_secure"). Because the challenge runs on-device, your backend relays that action's metadata to the app, the app runs the challenge, and the resulting proof goes back for your backend to re-confirm with.
Plan for this from your first confirm — any card confirm can come back requires_action.
onEvent and the paymentSheet result belong to the sheet and end once you have the pm_…. The retrigger is separate: you call handleNextAction(...), an async function that returns a ThreeDSProof (or throws) — nothing new arrives on onEvent.1. Your backend confirms and inspects the response. On requires_action, read the use_sdk next action and forward its metadata to the app:
{
"payment_status": "requires_action",
"next_action": {
"type": "use_sdk",
"use_sdk": {
"sdk": "three_d_secure",
"metadata": { "vault_token_ref": "tok_…" } // or "vault_token_intent_ref"
}
}
}
Trigger the app step only when next_action.type == "use_sdk" and use_sdk.sdk == "three_d_secure". (A top-level three_d_secure next action is the web browser flow — it does not apply to the native SDK.)
2. The app runs the challenge and returns a proof. Build the action straight from the forwarded metadata — the SDK resolves the vault key and card brand itself:
let action = PaypercutThreeDSAction(metadata: serverResponse.useSdkMetadata)
do {
let proof = try await paypercut.handleNextAction(action)
try await reConfirmOnYourBackend(proof) // step 3
} catch {
showError((error as? PaypercutError)?.displayMessage ?? "Authentication failed.")
}
3. Your backend re-confirms, server-to-server, passing the proof as payment_method_options.card.three_d_secure. That call returns the terminal outcome (succeeded / failed).
handleNextAction presents the issuer's challenge if required and returns a ThreeDSProof (cryptogram, electronicCommerceIndicator, transactionId, version). It uses the amount, currency, and account the instance was created with, and throws if authentication fails, is cancelled, or yields no proof. The SDK only produces the proof — the confirm and re-confirm are both server-to-server calls your backend owns.
Paypercut instance for the sheet and handleNextAction — the retrigger builds on context the SDK retained while the sheet tokenized the card, so call it on the instance that presented the sheet. And don't swap screens while you wait: confirming, the challenge, and re-confirming are one "we're working" window — keep a single loading spinner mounted and change its label (e.g. Authenticating → Confirming payment), then move to your success / failure screen only on the terminal result.Payment Methods
You control which methods appear through the config you pass:
- Card — an inline card form, shown when
"card"is inpaymentMethodTypes(the default). WhenallowSaveCardis on, a "save card" checkbox lets the customer opt in to reuse the card later. A billing-address section appears whencollectBillingAddressis on. - Apple Pay — the native Apple Pay button appears when the device supports it, your account has it enabled, and you pass
applePayMerchantIdentifier. Apple Pay also returns the buyer's billing address. - Saved cards — pass one or more
savedPaymentMethods(fetched server-to-server for your customer) and they're offered for one-tap selection; selecting one returns itspm_…as-is. - BLIK (Poland / PLN) — add
"blik"topaymentMethodTypes. Returns the entered code (.blik), not a payment method. Your backend confirms the payment with the code and polls the result.
Handling the Result
onResult delivers one terminal outcome:
paypercut.paymentSheet { result in
switch result {
case let .created(pm):
// pm.paymentMethodId — pass this to your backend to confirm the payment
// pm.type — "card"
// pm.wallet — "apple_pay", or nil for a manual / saved card
// pm.billingEmail — buyer email captured on the sheet, if any
// pm.saveForFutureUse — customer opted to save the card; pass as save_payment_method on confirm
// pm.billingDetails — name / email / billing address collected on the sheet (or from Apple Pay)
confirmOnYourBackend(pm.paymentMethodId)
case let .blik(code):
confirmBlikOnYourBackend(code) // single-use, expires ~2 min — confirm promptly
case .cancelled:
break // buyer dismissed the sheet
case let .failed(message):
showError(message) // safe to show to the buyer
}
}
When the customer ticks the save-card checkbox, pm.saveForFutureUse is true — forward it as save_payment_method when you confirm so the card is attached to the customer for future payments.
Lifecycle events (optional)
Pass onEvent for analytics / logging. It's observational — the sheet drives its own UI:
paypercut.paymentSheet(
onEvent: { event in
switch event {
case .loaded: break
case .processing: break
case let .paymentMethodCreated(id, type, wallet, saveForFutureUse): break
case let .blikCodeEntered(code): break
case let .error(code, message): break
case .expired: break
}
}
) { result in /* … */ }
These events cover the sheet only. The 3-D Secure retrigger is a separate, direct call that does not flow through onEvent — see Re-running 3-D Secure.
Error codes
The .error event (and .failed result) carries a code and a buyer-safe message. Show the message; the code tells you which stage failed, so you can quote it when reporting an issue:
code |
Stage | Meaning |
|---|---|---|
tokenization_failed |
On-device tokenization | The card or wallet token couldn't be secured into the vault. |
threeds_authentication_failed |
3-D Secure | The issuer declined authentication, or the customer failed / cancelled the challenge. |
card_declined, session_expired, payment_method_unavailable, … |
Payment-method create | The Paypercut API rejected the create call; the code is the API's own error code. |
nil |
Network / parsing | A transport or decode failure — the message is generic on purpose. |
The message is deliberately generic for tokenization and network failures — the SDK never surfaces vault or transport internals to the customer.
Apple Pay setup
Apple Pay needs a one-time merchant setup. Nothing certificate-related lives in your app — your app carries only your Apple Pay merchant identifier in its entitlement.
- Create an Apple Pay Merchant ID in the Apple Developer portal — must start with
merchant.(e.g.merchant.com.yourcompany.yourapp). - Enable Apple Pay on your app target (Signing & Capabilities → + Capability → Apple Pay) and tick that Merchant ID, then pass the same string to the SDK as
applePayMerchantIdentifier. - Register the Merchant ID with Paypercut — contact Paypercut to register your
merchant.…. Paypercut generates a signing request; you upload it to your Merchant ID in Apple and return the certificate Apple issues. Registration is what lets Paypercut decrypt your Apple Pay tokens — required for live payments. (This is a native, in-app flow — there is no domain verification; the.well-knowndomain-association step applies only to web Apple Pay.)
In sandbox / test mode Apple Pay works before registration is finished. Registration and the certificate are required for live. A working sandbox flow does not prove live is set up.
See the Apple Pay setup guide for the full walkthrough.
Testing
Use sandbox keys (pk_test_…) and .dev / .stage environments during development. Test Apple Pay on a real device — Apple Pay does not run in the iOS Simulator.
For test cards and sandbox scenarios, see the Testing Guide.
Support & Resources
Ready to add native checkout to your iOS app? Create your free Paypercut account →

