Apple Pay
Apple Pay lets a customer pay with a card already in their Apple Wallet, authorized with Face ID, Touch ID, or a passcode. The Frame iOS SDK ships FrameApplePayButton: a SwiftUI view that presents the native Apple Pay sheet, converts the wallet token into a Frame PaymentMethod, and charges it. This guide covers the Apple-side setup, SDK configuration, and the charge flow.
Apple Pay costs no more than any other card transaction - it's a card payment with a device-specific token in place of the PAN. For Apple Pay in a browser, use the payment request button instead.
Prerequisites
| Requirement | Details |
|---|---|
| Apple Developer Program membership | Required to create a merchant identifier and sign an app with the Apple Pay capability. |
| Apple Pay enabled on your Frame account | Email support@framepayments.com. The sheet renders without it, but authorization fails. |
| A physical device running iOS 17 or later | iOS 17 is the SDK minimum. Apple Pay never works in the Simulator. |
| A card in Apple Wallet | The button hides itself on devices with no eligible card. |
| Frame publishable key | Passed at SDK init. See Install. |
| A Frame account or customer | The owner of the resulting payment method. See Accept a payment for creating one. |
1. Register an Apple merchant ID
Sign in to developer.apple.com -> Certificates, Identifiers & Profiles -> Identifiers -> Merchant IDs, click +, choose Merchant IDs, and create one in reverse-DNS form - merchant.com.yourcompany.appname. The description is for your own records; the identifier is what you configure everywhere else.
2. Create a new Apple Pay certificate
Apple encrypts every wallet payment against a certificate tied to your merchant ID. Frame needs the matching private key to decrypt it, so the signing request has to come from Frame - not one you generate yourself.
Go to iOS Certificate Settings in the Dashboard, click Add new application, and follow the guide.
Download a Certificate Signing Request (CSR) file to get a secure certificate from Apple that allows you to use Apple Pay.
One CSR issues exactly one certificate. If you change merchant IDs, return to iOS Certificate Settings for a fresh CSR and repeat the exchange - reusing the old one produces payloads Frame can't decrypt.
Certificates generated from a CSR you made yourself will not work. The sheet still authorizes on-device, then the charge fails server-side because Frame holds no matching key. Revoke any non-Frame certificates under your merchant ID.
3. Add the Apple Pay capability in Xcode
- Open your project and select your app target - not the SDK package.
- Go to Signing & Capabilities -> + Capability -> Apple Pay.
- Under the Apple Pay capability, click + and select the merchant identifier from step 1.
Xcode writes an entitlements file for the target. Confirm it carries your merchant ID:
<key>com.apple.developer.in-app-payments</key>
<array>
<string>merchant.com.yourcompany.appname</string>
</array>
The merchant ID in the entitlements file must match the one you pass at SDK init exactly. A mismatch fails at runtime with a "Missing entitlement" error.
4. Install the SDK
In Xcode, select File -> Add Package Dependencies…, enter https://github.com/Frame-Payments/frame-ios as the repository URL, and add the Frame-iOS product to your app target.
The Swift module is named Frame, not Frame-iOS:
import Frame
Frame-Onboarding is a separate product - add it only if you also run hosted onboarding in-app.
5. Initialize the SDK with your merchant ID
Initialize once at app launch. FrameNetworking is the single source of truth for the merchant ID: pass it here and every Frame surface - FrameApplePayButton, FrameCheckoutView, FrameCartView, OnboardingContainerView - picks it up.
import SwiftUI
import Frame
@main
struct MyApp: App {
init() {
FrameNetworking.shared.initialize(
publishableKey: "pk_sandbox_your_publishable_key",
applePayMerchantId: "merchant.com.yourcompany.appname"
)
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
Initialization also starts device attestation, which the Apple Pay flow depends on - the button stays hidden until attestation succeeds.
6. Add the Apple Pay button
Drop FrameApplePayButton into your view hierarchy. It renders nothing unless the merchant ID is configured, the device supports Apple Pay, and attestation passed, so there's no availability check to write yourself.
The button takes a mode - .charge(amount:currency:) to charge, .addToOwner to save the card without charging - and an owner, either .account(...) or .customer(...).
import SwiftUI
import Frame
struct CheckoutView: View {
var body: some View {
FrameApplePayButton(
mode: .charge(amount: 4999, currency: "usd"),
owner: .account("c1a4a27f-d6e4-46e2-9557-fd5faaa31e7d")
) { result in
switch result {
case .success(.charge(let id)):
print("Charge created: \(id)")
case .success(.paymentMethod):
break // only produced in .addToOwner mode
case .failure(let error):
print("Payment failed: \(error.localizedDescription)")
}
}
.frame(height: 50)
}
}
amount is in the currency's smallest unit - 4999 is $49.99.
Where the charge lands
The owner you pass decides which resource the charge creates. Both arrive as .success(.charge(id:)); the caller knows which to expect from the owner it supplied.
| Owner | Resource created | Notes |
|---|---|---|
.account(...) | Transfer | The SDK opens a Sonar session for the account first - the API rejects the transfer without one. |
.customer(...) | ChargeIntent | Created with confirm: true and automatic authorization. |
.charge mode creates the charge from the device, authenticated with your secret key. That means shipping an sk_ in your app binary - fine against sk_sandbox_... while you test, not something to ship to the App Store. For production, use .addToOwner and create the transfer on your server with the returned payment method ID.
What the sheet requests
The SDK builds the PKPaymentRequest for you. It isn't configurable from the button, so the values below are what your customers see and what Frame receives:
| Field | Value |
|---|---|
| Supported networks | Visa, Mastercard, American Express, Discover, JCB |
| Merchant capabilities | 3D Secure |
| Country code | US |
| Currency | The currency from .charge; USD in .addToOwner mode |
| Summary item | Total at the charge amount; a $0 pending Card Verification line in .addToOwner mode |
| Required billing contact | Postal address, name, email address |
Save a wallet card without charging
Pass mode: .addToOwner to tokenize the wallet card and attach it to the owner without moving money. The sheet still appears - Apple requires an authorization gesture - but shows a $0 verification line instead of a total.
FrameApplePayButton(
mode: .addToOwner,
owner: .account("c1a4a27f-d6e4-46e2-9557-fd5faaa31e7d")
) { result in
switch result {
case .success(.paymentMethod(let paymentMethod)):
// Send the id to your server and charge it there.
Task { try await MyAPI.charge(paymentMethodId: paymentMethod.id, amount: 4999) }
case .success(.charge):
break // only produced in .charge mode
case .failure(let error):
print("Could not save card: \(error.localizedDescription)")
}
}
This path authenticates with the publishable key alone, so no secret key ships in the app. Your server then charges the saved payment method the same way as any other card - see step 5 of Accept a payment.
Use Apple Pay inside the prebuilt checkout
With a merchant ID configured at init, FrameCheckoutView shows an Apple Pay button above its card form automatically. The bundled checkout is account-scoped, so the id it returns is always a Transfer id.
FrameCheckoutView(
accountId: "c1a4a27f-d6e4-46e2-9557-fd5faaa31e7d",
paymentAmount: 4999
) { result in
switch result {
case .completed(let transferId):
print("Transfer created: \(transferId)")
case .cancelled:
print("User dismissed checkout")
case .failed(let error):
print("Checkout failed: \(error.localizedDescription)")
}
}
Without a merchant ID, the Apple Pay row is hidden and only the card form renders. FrameCartView behaves the same way for its nested checkout step.
Customize the button
buttonType and buttonStyle map straight to PassKit's PKPaymentButtonType and PKPaymentButtonStyle. Set addCheckoutDivider to render an "Or" divider beneath the button when your own payment form follows it.
FrameApplePayButton(
mode: .charge(amount: 4999, currency: "usd"),
owner: .account("c1a4a27f-d6e4-46e2-9557-fd5faaa31e7d"),
addCheckoutDivider: true,
buttonType: .pay, // .buy is the default
buttonStyle: .white // .automatic is the default
) { result in ... }
Test Apple Pay
Apple Pay requires a real device with a real card in Wallet - neither the Simulator nor Apple's sandbox test cards work with the Frame SDK.
- Run on a physical device with a card added to Apple Wallet.
- Initialize with your sandbox keys and your merchant ID.
- Authorize the sheet. In sandbox, Frame returns synthetic card details and no real charge is made against the card.
Troubleshooting
| Symptom | Cause |
|---|---|
| The button never appears | No eligible card in Wallet, the Apple Pay capability is missing from the app target, applePayMerchantId wasn't passed at init, or device attestation failed. |
| "Missing entitlement" at runtime | The merchant ID in the entitlements file doesn't match the one passed at init. |
| The sheet appears but authorization fails | The SDK wasn't initialized, or Apple Pay isn't enabled on your Frame account yet - contact support@framepayments.com. |
| The wallet token is rejected server-side | The Apple Pay certificate is missing, expired, or was issued from a CSR Frame didn't generate. Redo step 2 from a fresh CSR. Xcode caches old certificates aggressively, so if a reissue doesn't take, re-create the merchant ID at Apple as well. |
| Charges succeed in sandbox but 401 in production | The charge step authenticates with the secret key. Move the charge to your server using .addToOwner. |
Reference
The SDK calls POST/v1/payment_methods to tokenize the wallet payload, then POST/v1/transfers or POST/v1/charge_intents depending on the owner. For the SDK surface itself, see the Frame iOS SDK and the mobile SDKs overview.