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

RequirementDetails
Apple Developer Program membershipRequired to create a merchant identifier and sign an app with the Apple Pay capability.
Apple Pay enabled on your Frame accountEmail support@framepayments.com. The sheet renders without it, but authorization fails.
A physical device running iOS 17 or lateriOS 17 is the SDK minimum. Apple Pay never works in the Simulator.
A card in Apple WalletThe button hides itself on devices with no eligible card.
Frame publishable keyPassed at SDK init. See Install.
A Frame account or customerThe 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.

3. Add the Apple Pay capability in Xcode

  1. Open your project and select your app target - not the SDK package.
  2. Go to Signing & Capabilities -> + Capability -> Apple Pay.
  3. 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>

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.

OwnerResource createdNotes
.account(...)TransferThe SDK opens a Sonar session for the account first - the API rejects the transfer without one.
.customer(...)ChargeIntentCreated with confirm: true and automatic authorization.

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:

FieldValue
Supported networksVisa, Mastercard, American Express, Discover, JCB
Merchant capabilities3D Secure
Country codeUS
CurrencyThe currency from .charge; USD in .addToOwner mode
Summary itemTotal at the charge amount; a $0 pending Card Verification line in .addToOwner mode
Required billing contactPostal 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.

  1. Run on a physical device with a card added to Apple Wallet.
  2. Initialize with your sandbox keys and your merchant ID.
  3. Authorize the sheet. In sandbox, Frame returns synthetic card details and no real charge is made against the card.

Troubleshooting

SymptomCause
The button never appearsNo 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 runtimeThe merchant ID in the entitlements file doesn't match the one passed at init.
The sheet appears but authorization failsThe 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-sideThe 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 productionThe 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.

Frame Assistant

Ask anything about Frame's APIs and products