Developers Guides Tap to Pay on iPhone SDK

Tap to Pay on iPhone SDK

Accept contactless payments directly on iPhone. Complete integration guide for Surfboard's iOS SoftPOS SDK -- from setup to production.

iOSSwiftSoftPOSIn-StoreXCFramework

Add this to your codebase

Paste it into Claude Code, Codex, Cursor or any coding agent. It points the agent at this guide in machine-readable form, so it writes against the real API instead of a guess. Wire up the MCP server once and it can read the rest of the platform too.

Overview

The Surfboard Tap to Pay on iPhone SDK turns any compatible iPhone into a payment terminal. The SDK ships as a prebuilt tap_to_pay_apple.xcframework that you link into your iOS app.

  • SDK binary: tap_to_pay_apple.xcframework (static framework, module name tap_to_pay_apple).
  • Entry point: The Gap class is the single facade your app interacts with.
  • Configuration: GapCredentials encapsulates your Surfboard credentials and connection blob. Network details (Director URLs, certificates) are embedded in the blob — you do not configure them manually.
  • Backend responsibilities: Your server handles auth token issuance, order creation, and credential storage. The app never holds long-lived secrets.
  • App responsibilities: Initialize the SDK, register the terminal, start transactions, and handle events.

Note: The canonical reference implementation is the Gap Example App for iOS, which demonstrates the full integration flow.

Availability

Tap to Pay on iPhone with Surfboard is available in the following countries:

🇸🇪 Sweden 🇫🇮 Finland 🇩🇰 Denmark 🇳🇴 Norway 🇬🇧 United Kingdom 🇫🇷 France 🇮🇪 Ireland 🇪🇪 Estonia 🇱🇻 Latvia 🇱🇹 Lithuania 🇵🇱 Poland 🇭🇺 Hungary

Note: Availability depends on Apple enabling Tap to Pay on iPhone in each market and Surfboard’s payment processing coverage. Contact Surfboard if you need support for a country not listed here.

Supported Payment Methods

Tap to Pay on iPhone accepts contactless cards and NFC mobile wallets:

TypePayment methods
Card schemesVisa, Mastercard, American Express, Discover, Dankort
Mobile walletsApple Pay, Google Pay and other NFC wallets carrying a supported card

On-device PIN entry is supported. See Cardholder Verification & PIN.

Supported Devices

Tap to Pay on iPhone requires specific hardware and software:

  • Device: iPhone XS or later (models with NFC capability).
  • iOS version: iOS 17.4 or later. Apple also only allows Tap to Pay on recent iOS releases and deprecates older versions over time, roughly one year after a newer release. See Apple’s iOS versions and deprecation management for the current list, and advise merchants to keep their iPhone updated.
  • Beta iOS: Beta versions of iOS are not supported for Tap to Pay on iPhone. Always test on stable iOS releases.
  • Simulator: The iOS Simulator supports SDK initialization and terminal registration, but cannot perform real NFC card reads. Use a physical device for end-to-end payment testing.

Note: Minimum device, iOS and SDK versions can change when Apple or the card schemes update their compliance requirements. Follow the developer changelog for SDK releases and requirement changes.

Prerequisites

Before you start, confirm you have the following:

CategoryRequirement
Apple DeveloperActive account with the Tap to Pay on iPhone entitlement (development entitlement to build, distribution entitlement to release)
Surfboard credentialsCONNECTION_BLOB — opaque config string provided by Surfboard
Surfboard credentialsTMS_PUBLIC_KEY — retrieved from Developer Portal after registering your SDK app under Console → SDK Apps
Merchant identifiersMERCHANT_ID and STORE_ID from your Surfboard merchant onboarding
Auth providerproviderId and providerCertificate issued by Surfboard for your backend
BackendA server that calls Surfboard’s Client Auth Tokens API (/api/auth) and creates orders via the Orders API
iOS deviceiPhone running iOS 17.4+ with NFC (required for real payments)
ToolingXcode 15+
Simulator noteSimulator supports SDK init and registration flows, but not real card reads

Apple Developer Setup

Warning
Implementing Tap to Pay on iPhone is a complex process that requires submitting your app to Apple for approval. Plan extra time for the entitlement request and review process, which can take several weeks.

Five steps to configure your Apple Developer account and Xcode project for Tap to Pay on iPhone, plus a final distribution request before release.

Step 1: Request the development entitlement

Request the Tap to Pay on iPhone entitlement from your Apple Developer account. Apple first grants a development entitlement, which lets you build and test Tap to Pay in your app on real devices.

Step 2: Register your App ID

In Apple’s Certificates, Identifiers & Profiles, create or select an App ID and note the Bundle ID (e.g. com.yourcompany.yourapp). Use the same Bundle ID for test and production — the Surfboard SDK controls environments through the connection blob and backend URLs, not the bundle identifier.

Step 3: Enable the capability

Edit the App ID, find Tap to Pay on iPhone under Capabilities, and enable it. Apple may require additional contracts before this is available.

Step 4: Update provisioning profiles

Regenerate your development and distribution profiles for the updated App ID and install them in Xcode.

Step 5: Add the entitlement in Xcode

In your app target under Signing & Capabilities, click ”+ Capability” and add Tap to Pay on iPhone. Xcode creates the entitlements file automatically.

The entitlement key that must be present in your .entitlements file:

Entitlement KeyTypeValue
com.apple.developer.proximity-reader.payment.acceptanceBooleantrue

Before release: request the distribution entitlement

The development entitlement is not enough to ship to the App Store. When internal testing is complete, request the distribution entitlement from Apple. Apple reviews your Tap to Pay implementation as part of this request, including the merchant education screen and the Human Interface Guidelines for Tap to Pay on iPhone.

Further reading

Tip: The Surfboard SDK does not use different bundle IDs for environments. Environments are controlled by the connection blob and backend URLs, not by the bundle ID.

SDK Installation

The SDK is delivered as a prebuilt tap_to_pay_apple.xcframework. Surfboard provides separate staging and production builds.

  1. Copy tap_to_pay_apple.xcframework into your repo (e.g., YourApp/SDKs/tap_to_pay_apple/).
  2. In Xcode, go to your app target > General > Frameworks, Libraries, and Embedded Content.
  3. Click + > Add Other > Add Files, then select the xcframework.
  4. Set embedding to “Do Not Embed” (the SDK is a static framework).
  5. Build to verify linking.

Import in Swift:

import tap_to_pay_apple

Note: Swap the xcframework binary to switch between staging and production. Store both variants in your repo (e.g., staging/ and release/ subdirectories) and link only one at a time.

Credentials & Initialization

Three objects to set up before processing payments: a logger, credentials, and the SDK instance.

GapLogger

Implement the GapLogger protocol to route SDK logs into your logging stack. The protocol requires three methods: addLog, addErrorLog, and addDebugLog.

import os
import tap_to_pay_apple

final class AppGapLogger: GapLogger {
    private let logger = Logger(subsystem: "YourApp", category: "TapToPay")

    func addLog(logText: String) {
        logger.log("\(logText, privacy: .public)")
    }

    func addErrorLog(logText: String) {
        logger.error("\(logText, privacy: .public)")
    }

    func addDebugLog(logText: String) {
        logger.debug("\(logText, privacy: .public)")
    }
}

GapCredentials

Build GapCredentials once, typically at app startup, using values provided by Surfboard:

let credentials = GapCredentials(
    connectionBlob: connectionBlob,        // from Surfboard
    versionNumber: appVersion,
    tmsPublicKey: tmsPublicKey,            // from Surfboard Developer Portal
    merchantId: merchantId,                // from merchant onboarding
    storeId: storeId,                      // from merchant onboarding
    applicationBundleId: Bundle.main.bundleIdentifier ?? ""
)

Gap instance

Create a single, long-lived Gap instance. Subscribe to events, initialize the SDK, and set the auth token:

let gap = try Gap(logger: logger, credentials: credentials)

gap.subscribeToPublicEvents { wrapper in
    // Handle wrapper.event and wrapper.data
}

try await gap.initializeSdk()

let authToken = try await fetchAuthTokenFromBackend()
gap.setAuthToken(authToken: authToken)

Warning: Auth tokens are short-lived. Refresh them before expiry and call setAuthToken(authToken:) again. Never generate tokens on the device — always fetch them from your backend.

Terminal Lifecycle

The terminal represents a logical payment endpoint on the Surfboard side. You register it once per device, then open it for each session.

Register (first run)

let result = await gap.registerTerminal()

switch result {
case .success:
    print("Terminal ID: \(gap.terminalId)")
case .failure(let error):
    print("Registration failed: \(error.code.details.message)")
}

Registration is a one-time operation per device or installation. After success, gap.terminalId contains the terminal identifier.

Open and initialize (each session)

On subsequent app launches, skip registration and go straight to opening:

let openResult = await gap.openTerminal()
let readerResult = await gap.initializeReader()
try await gap.getReadyForTransaction()

Call these three methods in sequence before starting any payment.

Cleanup

For logout or troubleshooting flows, use disposeTerminal() to close the session and clean() to reset SDK state. After calling clean(), you need to re-register the terminal.

Apple Terms and Conditions

Before a merchant can accept their first payment, Apple requires them to accept the Tap to Pay on iPhone Terms and Conditions with their Apple Account. Apple presents this screen itself the first time the reader is set up for that merchant, and payments can’t be taken until the terms are accepted.

  • Run the first reader setup in a flow where the merchant is present and expecting it, such as an onboarding or “Enable Tap to Pay” step, not silently in the background.
  • Acceptance is tied to the merchant, so later sessions and app launches don’t show the screen again.
  • Follow the onboarding flow in Apple’s Human Interface Guidelines for Tap to Pay on iPhone. Apple checks this during review.

Merchant Education

Required by Apple
Apple requires your app to show merchants how to accept payments with Tap to Pay on iPhone. Integrate this before you request the distribution entitlement, or your app may not pass review.

On iOS 18 and later, use Apple’s ProximityReaderDiscovery API to show Apple’s “How to Tap” content. Apple keeps this content up to date and localized for the merchant’s region. ProximityReader is a built-in iOS framework, so no extra dependency is needed.

The API has two steps: fetch the content with content(for:), then display it with presentContent(_:from:).

import ProximityReader
import UIKit

func showHowToTap(from rootViewController: UIViewController) async throws {
    guard #available(iOS 18.0, *) else {
        // Show your own merchant education screen on iOS 17
        return
    }
    let discovery = ProximityReaderDiscovery()
    let content = try await discovery.content(for: .payment(.howToTap))

    // Present from the topmost view controller, otherwise the call fails
    var topViewController = rootViewController
    while let presented = topViewController.presentedViewController {
        topViewController = presented
    }
    discovery.presentContent(content, from: topViewController)
}

Keep two things in mind:

  • iOS 17 fallback: ProximityReaderDiscovery is only available on iOS 18+. Because the SDK supports iOS 17.4+, provide your own education screen for merchants on iOS 17.
  • Topmost view controller: Pass the view controller currently on top of the presentation stack. Walk the presentedViewController chain from your root view controller to find it.

A good moment to show this is right after the merchant accepts Apple’s terms, and from a help or settings entry the merchant can return to later. For more ideas, such as in-app promotion and email campaigns, see Apple’s Tap to Pay on iPhone marketing guidelines.

Payment Flow

Every payment follows three steps: create an order on your backend, start a transaction through the SDK, and handle the result.

Step 1: Create an order

From your iOS app, call your backend to create an order via the Surfboard Orders API. Pass the amount in minor units (e.g., 1000 for 10.00 SEK), the ISO numeric currency code (e.g., "752" for SEK), and gap.terminalId as the terminal identifier. Your backend returns an orderId.

Step 2: Start the transaction

let parameters = GapPaymentParameters(
    amount: NSNumber(value: amountInMinorUnits),
    type: "PURCHASE",
    currency: "SEK",
    orderId: orderId,
    aidPreference: nil
)

let paymentId = try await gap.startTransaction(parameters: parameters)

The SDK takes over and presents Apple’s Tap to Pay UI. The customer taps their card. The SDK emits events throughout — use them to update your UI (“Present card”, “Processing”, “Approved”, “Declined”).

Step 3: Complete or cancel

After the transaction resolves:

// Confirm completion
gap.sendCompletedEvent(paymentId: paymentId, approved: true)

// Or cancel an in-progress transaction
let cancelResult = await gap.cancelTransaction(paymentId: paymentId)

Cardholder Verification & PIN

Tap to Pay on iPhone supports on-device PIN entry. When a contactless transaction requires cardholder verification, the SDK presents a secure PIN input screen on the iPhone.

When is PIN required?

  • NFC wallet payments (Apple Pay, Google Pay) typically do not require PIN — the cardholder authenticates via Face ID or passcode on their own device.
  • Physical contactless cards may require PIN depending on the transaction amount, card issuer policy, and regional regulations. Contactless limits differ per country, for example around 400 SEK in Sweden and 100 GBP in the UK, and the issuer can also ask for PIN below the limit.

Offline PIN cards

Tap to Pay on iPhone only supports online PIN. Some cards require offline PIN, which means the card has to be inserted into a physical card reader to verify the PIN. Tap to Pay can’t do that, so the terminal rejects the transaction on the device. The transaction is never sent to the issuer for authorisation.

RegionConsideration
United KingdomDepending on the issuer, Strong Customer Authentication (SCA) may require some cards to be inserted for verification. These transactions are rejected by the terminal.
FinlandMany cards issued in Finland support offline PIN only. Transactions that need PIN on these cards are rejected by the terminal.
Recommendation
If a transaction is rejected or declined due to PIN or verification issues, ask the customer to try a different card or use an alternative payment method such as a Payment Link or a traditional card reader.

Tapping the card

Ask the customer to hold the card or phone against the top of the iPhone until the read is complete. The card may need to stay in place for a few seconds after the first vibration while the chip data is read. Pulling it away early is a common cause of failed reads.

Events & Monitoring

Subscribe to the SDK event stream to drive your UI and logging:

gap.subscribeToPublicEvents { wrapper in
    switch wrapper.event {
    case .INITIALIZED:           // SDK ready
    case .TERMINAL_CREATED:      // Registration succeeded
    case .TERMINAL_CREATION_ERROR: // Registration failed
    case .TRANSACTION_COMPLETED: // Payment finished
    case .TRANSACTION_FAILED:    // Payment failed
    default: break
    }
}

Key status properties

PropertyPurpose
gap.isInitializedSDK has completed initialization
gap.terminalIdTerminal ID after successful registration
gap.isReadyForTransactionsTerminal and reader are ready for payments
gap.canMakePayments()Device supports Tap to Pay and is in a usable state

Use these for health checks and to enable or disable payment UI elements.

Best Practices

Follow these recommendations to deliver a reliable and polished Tap to Pay experience.

Reader connection

  • Connect early: Initialize the SDK and connect to the reader in the background during app startup, so the terminal is ready when the merchant needs to accept a payment. The exception is the very first setup, which shows Apple’s Terms and Conditions and should happen while the merchant is present.
  • Automatic reconnection: When your app returns to the foreground, check gap.isReadyForTransactions and re-initialize the reader if needed. This ensures the terminal is always available after the app has been backgrounded.

User experience

Marketing & branding

Testing

Simulator: Supports SDK initialization and terminal registration. Card presentation is simulated — no real NFC reads. Use the simulator to validate your integration flow and UI before moving to a device.

Physical device: Required for end-to-end payment validation. Needs an iPhone running iOS 17.4+ with NFC. Test at least one full transaction on a real device before release.

Testing PIN entry

Test PIN entry with real cards. To trigger the PIN screen, use an amount above the local contactless limit, for example more than 400 SEK in Sweden or more than 100 GBP in the UK. Whether PIN is requested also depends on the issuer and the card’s own rules, so a card may ask for PIN below the limit or, occasionally, not at all. If a card doesn’t trigger PIN, try a card from a different bank.

Cards that require offline PIN are rejected by the terminal instead of showing the PIN screen. See Offline PIN cards.

Common issues and checks

  • SDK not initializing: Verify credentials (connection blob, TMS public key, merchant/store IDs). Check network connectivity. Inspect GapLogger output.
  • Registration fails: Confirm initializeSdk() completed. Verify a valid authToken is set. Check that merchant/store IDs match the target environment.
  • Payments fail: Confirm gap.terminalId is set. Verify order creation succeeds on your backend. Check that amount uses minor units and currency uses the correct ISO code. Inspect transaction events to distinguish declines from technical errors.

Release Checklist

Verify these items before shipping your app with Tap to Pay on iPhone.

Apple configuration

  • App ID has Tap to Pay on iPhone capability enabled
  • Provisioning profiles are up to date and installed
  • Distribution entitlement has been granted by Apple
  • Merchants are shown Apple’s Terms and Conditions in a deliberate onboarding step
  • “How to Tap” merchant education is integrated, with a fallback screen on iOS 17

Surfboard configuration

  • Using the correct connectionBlob and tmsPublicKey for the target environment
  • Backend calls the correct Surfboard endpoints (staging vs. production) for auth tokens and orders
  • merchantId and storeId match the target environment

SDK wiring

  • tap_to_pay_apple.xcframework is linked to the correct app target
  • Only one xcframework variant (staging or production) is linked at a time
  • Gap is instantiated once and reused across the app lifecycle
  • Auth tokens are refreshed before expiry via setAuthToken(authToken:)

Functional validation

  • gap.isInitialized returns true after startup
  • gap.terminalId is populated after registration
  • At least one full transaction completes on a real device
  • A transaction above the local contactless limit shows the PIN screen on a real card
  • Event stream fires expected events: INITIALIZED, TERMINAL_CREATED, TRANSACTION_COMPLETED

Reference

Disclaimer

While this document aims to assist users in the application process, it is ultimately the user's responsibility to meet Apple's requirements, and the final decision to approve or decline an application lies with Apple.

Ready to get started?

Create a sandbox account and start building your integration today.