Tap to Pay
Tap to Pay is Payabli’s native mobile component for accepting contactless, card-present payments in your app. It reads the payor’s card, phone, or watch over NFC directly, so your app never touches raw card or bank data. See Tap to Pay overview for how the payment flow works conceptually, or Pay In if you’re looking for Payabli’s card/ACH component instead.
Right now, only iOS is supported.
Prerequisites
Before you start, confirm you have:
- A physical device. An iPhone XS or newer running iOS 16.7 or later, in a region where Apple supports Tap to Pay on iPhone. The Simulator can’t take a Tap to Pay payment.
- Payabli sandbox credentials. A Payabli sandbox
entryPointwith Tap to Pay enabled. Contact your Payabli representative if you don’t have one yet. - An OAuth2 client ID and client secret. See Permissions for the required permissions.
- Your app registered as an authorized app. Your app’s App ID (
<APP_ID_PREFIX>.<BUNDLE_ID>, where the prefix is your Team ID for most apps) must be one of the paypoint’s authorized apps before device attestation succeeds. See Integration steps. - Apple’s Tap to Pay entitlement.
com.apple.developer.proximity-reader.payment.acceptance. Request it through the Apple Developer Portal. - Xcode 15 or later and Swift 5.9 or later.
Apple’s Tap to Pay entitlement (com.apple.developer.proximity-reader.payment.acceptance) needs Apple’s approval before you can use it. Request it through the Apple Developer Portal. Approval can take 2–8 weeks. Apply for this entitlement as soon as you decide to integrate Tap to Pay. Don’t wait until the rest of your integration is ready.
Your app’s provisioning profile also needs com.apple.developer.devicecheck.appattest-environment, set to development for development builds or production for release builds. This value must match the environment you pass to PayabliConfig.
Installation
Install PayabliSDKTapToPay from the sdk-ios Swift package:
Add the package
In Xcode, choose File → Add Package Dependencies… and enter the sdk-ios repository URL:
Or add it to your Package.swift dependencies:
Link the product
Link the PayabliSDKTapToPay product to your app target. It includes PayabliSDKCore and the Tap to Pay reader engine, so you don’t need to add those separately:
To use the Tap to Pay and Pay In components in the same app, link PayabliSDKTapToPay and PayabliSDKPayIn, or link PayabliSDK alone. Don’t link PayabliSDK together with either of the others, or the build fails.
Backend token endpoint
The Tap to Pay component authenticates every request with a short-lived Payabli access token, obtained through a tokenProvider closure you implement.
Your backend holds the Payabli clientId and clientSecret, exchanges them for the access token, and never exposes them to your mobile app.
Create the project
This example server uses JavaScript with Node.js and Express.
Requires: Node.js 18 or later, and npm
Create a new Node.js project and enable ES modules:
Write the token endpoint
Create server.js with the following. It returns the token as { "accessToken": "..." }:
Create a .env file next to server.js with your credentials:
Anyone who can call this route gets a token that can charge, store payment methods, and void for your paypoint. Before you go live, replace authenticateUser and mayTakePayments with the same session and permission checks the rest of your app uses.
Keep clientId and clientSecret on your server, in environment variables or a secret store. Never commit .env to source control.
Call it from your app
Pass a tokenProvider closure that calls this endpoint and returns the token when you start the session:
The SDK calls the token provider before its first request, and again when a token is rejected. Concurrent callers share one call, so you don’t need to track expiration or schedule refreshes. Each call has 30 seconds to return. A call that takes longer, throws, or returns a token the SDK can’t use fails with PayabliErrorType.tokenProviderFailed.
Anyone who can call your token endpoint gets a token that can charge for your paypoint. Authenticate the caller and check that they’re allowed to take payments before you return a token.
Implementation walkthrough
This walkthrough builds a minimal Tap to Pay charge, from constructing PayabliTTP through handling the result:
Loading walkthrough...
/// Add the imports and token provider
Import PayabliSDKCore and PayabliSDKTapToPay, and add a function that exchanges credentials with your backend for a Payabli access token, the same way the Pay In component does. In production, send your app’s own session credential with this request so your token endpoint can authenticate the caller.
/// Start the session and create PayabliTTP
Start one PayabliSession with your entrypoint, environment, and token provider, then call PayabliTTP.create(), which runs on that session. create() throws a TapToPayError whose type is .sessionNotInitialized if no session has been started. You don’t pass your App ID: the SDK reads it from the signed app, and it must already be registered as one of the paypoint’s authorized apps.
/// Initialize and handle first-time setup
Call initialize() before the first charge. On a device’s first launch, it throws a TapToPayError whose type is .devicePendingActivation until your app calls activateDevice(activationCode:) with a code issued for the device. If the merchant hasn’t accepted Apple’s Tap to Pay terms, it throws one whose type is .termsNotAccepted. Present the terms with presentTerms(), confirm with areTermsAccepted(), then initialize again.
/// Charge a payment
Call charge(type:paymentDetails:customer:) when ttp.isReady is true. Hold the device near the customer’s card, phone, or watch when prompted. If the card is declined, ask for another card. For any other TapToPayError, its capture value says whether the card was charged. Keep error.paymentTransId when the outcome is unknown or charged, so you can look up or reconcile the payment.
This walkthrough covers the component-level constructor, initialize(), and charge(). For the full Tap to Pay setup (authorized apps, Apple entitlements, device activation, and Apple’s terms), see Accept Tap to Pay payments.
Implementation reference
PayabliTTP and charge(...) are the two types you construct and call directly. There’s no hosted form for Tap to Pay.
Session and PayabliTTP.create()
PayabliTTP runs on the app’s one PayabliSession. Start the session with PayabliSession.initialize(config:) before you call PayabliTTP.create():
create() throws a TapToPayError whose type is .sessionNotInitialized when no session has been started. Pay In runs on the same session: pass session to PayabliPayIn. You don’t pass your App ID to the SDK. It reads the App ID from the signed app and sends it when the device attests.
PayabliConfig takes these values:
The entrypoint slug provisioned by Payabli. PayabliConfig throws if it’s blank.
.sandbox or .production. Must match the app’s appattest-environment entitlement.
Returns a new access token from your token endpoint. The SDK calls it before its first request, and again when a token is rejected. Concurrent callers share one call. Each call has 30 seconds to return.
Start the session once. Calling initialize again with the same entrypoint, environment, and telemetry setting returns the same session. Different values throw a PayabliError whose type is .invalidConfiguration, and the running session stays in place.
Methods and properties
Attests the device, fetches its configuration, and prepares the reader. The first run on a device takes longer than later runs.
Activates a device that’s pending activation. Call initialize() again afterward.
Shows Apple’s Tap to Pay on iPhone Terms and Conditions sheet. Returning doesn’t mean the merchant accepted. Call areTermsAccepted() to check.
Returns false when the merchant hasn’t accepted Apple’s terms. Throws a TapToPayError when the reader can’t answer. Call it each time instead of caching the answer, because acceptance can change outside your app.
Takes a payment. See the charge(...) parameters below.
The session’s current state. PayabliTTP is an ObservableObject, so you can bind sessionState and isReady in SwiftUI.
true when the session is ready to charge.
Session states
A charge ends at .ready, at .sessionExpired when the reader session was used up, or at .failed(reason: .deviceSetupRequired) when the device’s registration is gone. The charge’s outcome is what charge(...) returns or throws.
To show progress, switch on sessionState in SwiftUI:
When the session is .failed, reason is a PayabliTTPFailureReason:
charge(...) parameters
The charge(...) method returns a TransactionResult only when the payment is approved. TransactionResult has a single field, paymentTransId. Store it with your order to track and reconcile the payment.
Must be .sale.
properties
The total amount charged.
Part of amount, not added to it. The card is charged amount.
Omit to charge in the paypoint’s currency.
The payor. Name the payor with at least one of firstName, lastName, customerNumber, or customerId, because a charge that names nobody can be refused. The other fields are optional, and blank values are ignored.
properties
Payabli’s internal customer ID, when the customer is already registered.
Invoice metadata, persisted at /initiate.
properties
Invoice reference forwarded to the backend and the processor.
Optional free-form description forwarded to the backend.
Errors
Every Tap to Pay failure is a TapToPayError, except a cancellation, which can end the call as a CancellationError. A TapToPayError carries the error catalog entry for its cause:
category: What to do, such as.credential(callinitialize()again) or.outcomeUnknown(find the transaction before you repeat the call).type: The cause, for the cases your app handles individually, such as.devicePendingActivation.code: The catalog number. Give it to Payabli support with the failure.message: Fixed text.detailholds the service’s or the reader’s own words, when there are any.retryAfter: How long the service asked you to wait before trying again, when it asked.
You don’t need to handle every type individually. Every TapToPayError also carries capture, which tells you whether the card was charged, and paymentTransId, when there is one:
Handle these types individually, then fall back to capture for everything else:
A .tokenProviderFailed type means your token provider failed. Credentials without inboundpayments_create reach .ready, and charge(...) then throws a TapToPayError whose type is .permissionDenied, before the card is read.
If you cancel the task running charge(...) after the transaction has opened, charge(...) throws a TapToPayError with paymentTransId and capture. Handle it like any other error.
Example app
The sdk-ios repository includes PayabliDemo, a SwiftUI example app that exercises PayabliPayIn and PayabliTTP (card/ACH capture and Tap to Pay), alongside a bundled local Node.js token server for testing.
Configure your secrets
Copy the secrets template, then fill in your sandbox identifiers and point the app at the bundled local token server:
Loading walkthrough...
/// Set your sandbox identifiers
In App/Configuration/Secrets.swift, set the sandbox entry in entryPoints to your sandbox entryPoint. There’s no App ID to set: the SDK reads it from the signed app, and the TapToPay tab shows the value to register as an authorized app. localNetworkHost is the fallback host a physical device uses when you don’t pass -PayabliTokenHost. Set it to the output of scutil --get LocalHostName on your Mac, followed by .local:
/// Point the app at the local token server
Both token endpoints ship pointing at a placeholder backend. Switch them to DemoConfiguration.TokenServer.accessTokenURL, which resolves to 127.0.0.1 in the Simulator, so the Save and Capture tabs work there without a real backend. The TapToPay tab needs a physical device, which also needs the server setup in the next step:
Start the local token server
In a separate terminal, start the bundled LocalTokenServer:
Edit .env and set PAYABLI_ACCESS_TOKEN to a short-lived sandbox access token, then start the server:
The server binds only to 127.0.0.1 by default and authenticates no caller, so run it only on your own machine.
To test on a physical device, which Tap to Pay requires, the phone has to reach the server over your network:
-
Start the server on all interfaces instead, only on a trusted network, and stop it when you’re done:
-
In Xcode, choose Product → Scheme → Edit Scheme → Run → Arguments and pass your Mac’s LAN address, on the same network as the phone:
-
Allow Local Network access when the app asks for it on the phone. See the
LocalTokenServerREADME for the credential-exchange mode and physical-device networking notes.
Run the app
Open PayabliDemo.xcodeproj in Xcode and set DEVELOPMENT_TEAM on the PayabliDemo target. Select the PayabliDemo sandbox scheme, which picks the sandbox entry from Secrets.entryPoints, and run it. The Save and Capture tabs work in the Simulator. The TapToPay tab requires a physical device. See Requirements.
The example app assumes a sandbox entryPoint with card, ACH, and Tap to Pay enabled. Contact your Payabli representative if your sandbox paypoint doesn’t have these enabled yet.
Related resources
See these related resources to help you get the most out of Payabli.
Prerequisites
- Mobile components overview - Learn how Payabli’s native mobile SDK components let you accept payments in your mobile app