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.
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 nametap_to_pay_apple). - Entry point: The
Gapclass is the single facade your app interacts with. - Configuration:
GapCredentialsencapsulates 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:
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:
| Type | Payment methods |
|---|---|
| Card schemes | Visa, Mastercard, American Express, Discover, Dankort |
| Mobile wallets | Apple 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:
| Category | Requirement |
|---|---|
| Apple Developer | Active account with the Tap to Pay on iPhone entitlement (development entitlement to build, distribution entitlement to release) |
| Surfboard credentials | CONNECTION_BLOB — opaque config string provided by Surfboard |
| Surfboard credentials | TMS_PUBLIC_KEY — retrieved from Developer Portal after registering your SDK app under Console → SDK Apps |
| Merchant identifiers | MERCHANT_ID and STORE_ID from your Surfboard merchant onboarding |
| Auth provider | providerId and providerCertificate issued by Surfboard for your backend |
| Backend | A server that calls Surfboard’s Client Auth Tokens API (/api/auth) and creates orders via the Orders API |
| iOS device | iPhone running iOS 17.4+ with NFC (required for real payments) |
| Tooling | Xcode 15+ |
| Simulator note | Simulator supports SDK init and registration flows, but not real card reads |
Apple Developer Setup
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 Key | Type | Value |
|---|---|---|
com.apple.developer.proximity-reader.payment.acceptance | Boolean | true |
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
- Apple: Setting up the entitlement for Tap to Pay on iPhone
- Apple: Tap to Pay on iPhone for Developers
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.
- Copy
tap_to_pay_apple.xcframeworkinto your repo (e.g.,YourApp/SDKs/tap_to_pay_apple/). - In Xcode, go to your app target > General > Frameworks, Libraries, and Embedded Content.
- Click + > Add Other > Add Files, then select the xcframework.
- Set embedding to “Do Not Embed” (the SDK is a static framework).
- 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/andrelease/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
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:
ProximityReaderDiscoveryis 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
presentedViewControllerchain 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.
| Region | Consideration |
|---|---|
| United Kingdom | Depending on the issuer, Strong Customer Authentication (SCA) may require some cards to be inserted for verification. These transactions are rejected by the terminal. |
| Finland | Many cards issued in Finland support offline PIN only. Transactions that need PIN on these cards are rejected by the terminal. |
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
| Property | Purpose |
|---|---|
gap.isInitialized | SDK has completed initialization |
gap.terminalId | Terminal ID after successful registration |
gap.isReadyForTransactions | Terminal 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.isReadyForTransactionsand re-initialize the reader if needed. This ensures the terminal is always available after the app has been backgrounded.
User experience
- Follow Apple’s Human Interface Guidelines for Tap to Pay on iPhone to provide a consistent and intuitive payment experience.
- Show Apple’s “How to Tap” content as described in Merchant Education. Apple requires it for review.
Marketing & branding
- When promoting Tap to Pay on iPhone in your app or marketing materials, follow Apple’s Tap to Pay on iPhone Marketing Guidelines for correct branding, terminology, and asset usage.
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
GapLoggeroutput. - Registration fails: Confirm
initializeSdk()completed. Verify a validauthTokenis set. Check that merchant/store IDs match the target environment. - Payments fail: Confirm
gap.terminalIdis 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
connectionBlobandtmsPublicKeyfor the target environment - Backend calls the correct Surfboard endpoints (staging vs. production) for auth tokens and orders
merchantIdandstoreIdmatch the target environment
SDK wiring
tap_to_pay_apple.xcframeworkis linked to the correct app target- Only one xcframework variant (staging or production) is linked at a time
Gapis instantiated once and reused across the app lifecycle- Auth tokens are refreshed before expiry via
setAuthToken(authToken:)
Functional validation
gap.isInitializedreturnstrueafter startupgap.terminalIdis 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
Ready to get started?
Create a sandbox account and start building your integration today.