Accept payments
The SDK has these calls: isSupported, prepare, acceptPayment, presentEducation, isAccountLinked, linkAccount, readerState and readerStates. The API is @MainActor. Call it from the main actor, for example from a SwiftUI view.
Example
import MoneiTapToPay
// Once on launch, and again after each token renewal. A cached token that did not expire is fine.
func startTapToPay() async {
guard TapToPay.isSupported else { return } // Hide Tap to Pay on iPhone in your UI.
do {
let token = try await myServer.fetchPosToken() // The "token" field only.
try await TapToPay.prepare(token: token)
} catch {
// See "Handle errors and unclear outcomes".
}
}
// When the customer pays.
func pay(amountInCents: Int, orderId: String) async {
do {
let result = try await TapToPay.acceptPayment(
amount: amountInCents,
orderId: orderId,
callbackUrl: URL(string: "https://example.com/monei/webhook"))
switch result.status {
case .approved: showApproved(result)
case .declined: showDeclined(result)
@unknown default: showPending(orderId)
}
} catch TapToPayError.outcomeUnknown(let orderId) {
// Do not retry. Check this orderId in MONEI.
showPending(orderId)
} catch {
// See "Handle errors and unclear outcomes".
}
}
Check device support
TapToPay.isSupported is true when the device supports Tap to Pay on iPhone: an iPhone XS or newer with iOS 18.5 or later. When it is false, hide Tap to Pay on iPhone in your UI. On an unsupported device, all calls throw notSupported.
Prepare the reader
Call TapToPay.prepare(token:) once on launch, and again after each token renewal.
preparestores the token and prepares the reader in the background, so that the first payment starts faster.- The token stays in memory only. Call
prepareagain after each launch. A cached token that did not expire is fine: then the reader setup starts at once. preparedoes not show Apple's terms.prepareasks for location permission if the user did not answer yet, and waits until the user answers. There is no time limit for the answer. If the user does not allow location access,preparethrowslocationDenied. IfInfo.plisthas noNSLocationWhenInUseUsageDescription,preparethrowssetupIncompleteat once (locationDeniedbefore SDK 0.5.0).- On the first launch, show your location pre-alert screen before
prepare. Apple asks you to explain the permission during onboarding, well before checkout. - The SDK needs a location fix to prepare the reader. The first
prepareoracceptPaymentafter launch waits up to 15 seconds for it. If the device gets no location in this time, the call throwspaymentFailed(code: .locationTimeout).
Each time the app comes back from the background, the SDK prepares the reader again. You do not need to call prepare for this. A system alert or Control Center does not cause a new preparation, because iOS keeps the reader session.
Take a payment
Call TapToPay.acceptPayment(amount:orderId:callbackUrl:) when the customer pays.
| Parameter | Type | Description |
|---|---|---|
amount | Int | The amount in euro cents, more than 0. For example, 1050 is 10.50 EUR. |
orderId | String | Your own reference for the order. It must not be empty. Use a new orderId for each order. MONEI stores it with the payment, and you use it to reconcile. |
callbackUrl | URL? | The URL where MONEI sends the signed webhook with the payment result. Always set it. See Confirm the payment with the webhook. |
What occurs during a payment:
- If the account did not accept Apple's Tap to Pay on iPhone terms yet,
acceptPaymentshows them first. The user must accept them. Apple asks you to offer the terms before checkout too. See Accept Apple's terms before checkout. - Apple shows the tap screen. The customer taps the card.
- MONEI processes the payment.
acceptPaymentreturns aPaymentResultor throws aTapToPayError.
Only one payment can run at a time. During a payment, acceptPayment throws busy, so disable your pay button.
If acceptPayment throws outcomeUnknown, the card can be charged. Do not retry, and do not charge again with a new orderId. See Unclear outcome.
Read the result
acceptPayment returns a PaymentResult:
| Property | Type | Description |
|---|---|---|
paymentId | String | The MONEI payment ID. |
status | Status | .approved or .declined. |
cardBrand | String? | The card network name in lowercase, for example visa or amex. unknown for a network that the SDK does not know. nil if the network is not available. |
last4 | String? | The last 4 digits of the card, or nil. |
orderId | String | The orderId of the payment. |
amount | Int | The amount in cents. |
currency | String | The currency as an ISO 4217 code, for example EUR. |
statusCode | String? | The MONEI status code, for example E000, or nil. |
statusMessage | String? | The text of the MONEI status code, or nil. A declined result also has the reason, for example insufficient funds. |
authorizationCode | String? | The authorization code of an approved result, or nil. A declined result has no authorization code. |
cardType | String? | credit, debit or prepaid, or nil. |
cardCountry | String? | The country of the card as an ISO 3166-1 alpha-2 code, for example ES, or nil. |
Only status tells you if the payment is approved. Use the other fields to show the result in your UI. The signed webhook stays the only trusted payment result.
A result with the status .declined is not an error. The card was read and the issuer declined the payment. The payment is in MONEI with its paymentId.
Show the decline reason
A declined result has the reason in statusMessage. Show it to the customer. You do not need a call to MONEI for it. statusMessage can be nil. Then show a general text.
func showDeclined(_ result: PaymentResult) {
// statusMessage can be nil. Show a general text then.
let reason = result.statusMessage ?? "The card was declined."
showMessage("Payment declined: \(reason)")
}
Confirm the payment with the webhook
Always pass callbackUrl. MONEI sends a signed webhook with the payment result to this URL. The signed webhook is the only trusted payment result. Use the result in the app for the UI only.
- Verify the signature on your server before you fulfil the order. See Verify signature.
- The
callbackUrlmust usehttps, have 2048 characters or fewer, and its host must not be a private IP address. The SDK does not check the URL. MONEI checks it after the card read. If the URL is not valid, MONEI does not create the payment, andacceptPaymentcan throwoutcomeUnknown. Make sure that the URL is valid before you take payments. - For when MONEI sends this webhook, see Payment callback.
Show Apple's education screens
Call TapToPay.presentEducation(from:) to show Apple's screens that teach how to tap a card. Show them after the user accepts Apple's terms, and from your settings or help menu.
try await TapToPay.presentEducation(from: viewController)
Accept Apple's terms before checkout
From SDK 0.4.0. Apple asks that the merchant can accept the Tap to Pay on iPhone terms before the first customer, for example at the end of your onboarding and from your settings. Use isAccountLinked() to show the state and linkAccount() to show the terms.
// Settings: read the state from Apple each time. Do not store it.
func refreshTapToPayStatus() async {
enabled = (try? await TapToPay.isAccountLinked()) ?? false
}
// The button that enables Tap to Pay on iPhone. Show it only to a user who can accept the terms
// for the business. Show other users a message to contact an administrator.
func enableTapToPay(from viewController: UIViewController) async {
do {
try await TapToPay.linkAccount() // Shows Apple's terms if they were not accepted yet.
enabled = true
try await TapToPay.presentEducation(from: viewController)
} catch TapToPayError.termsDeclined {
// The user did not accept the terms. Keep the button.
} catch {
// See "Handle errors and unclear outcomes".
}
}
- Call
preparefirst. Both calls use the stored token. isAccountLinked()never shows the terms. It also returnsfalsewhen the check fails. This is safe:linkAccount()shows no terms to an account that accepted them.- After the user accepts the terms,
linkAccount()prepares the reader. This can take some seconds. Show the reader state until it returns. - While either call runs,
acceptPaymentthrowsbusy. Do not start them when the user is about to pay. If the user taps the Tap to Pay on iPhone button while a check runs, wait for the check to end, then callacceptPayment. Do not grey out the button: Apple does not allow it. - Keep the Tap to Pay on iPhone button in checkout. If the terms were not accepted,
acceptPaymentshows them. - Only a user who can accept the terms for the business must see Apple's terms.
acceptPaymentcannot check this. For a user who cannot accept them, callisAccountLinked()when the checkout screen opens. If it returnsfalse, the Tap to Pay on iPhone button shows a message to contact an administrator and does not callacceptPayment. - For the button text, use Apple's approved wording in the Tap to Pay on iPhone marketing guidelines.
Show the reader state
From SDK 0.4.0. Apple asks for a progress indicator while the device configures Tap to Pay on iPhone, and for an initializing view when the user starts a payment before the reader is ready. Use TapToPay.readerStates().
struct TapToPayStateView: View {
@State private var state = TapToPay.readerState
var body: some View {
Group {
switch state {
case .ready: Text("Ready")
case .preparing(let progress?):
ProgressView("Preparing", value: Double(progress), total: 100)
case .preparing: ProgressView("Preparing")
case .notReady: Text("Not ready")
@unknown default: EmptyView()
}
}
.task { for await next in TapToPay.readerStates() { state = next } }
}
}
| State | Meaning |
|---|---|
notReady | No setup runs, and the reader is not ready: no prepare yet, the account did not accept Apple's terms, the last setup failed, the session must be set up again (for example after the app was in the background), or MONEI blocked this SDK version. The next call that needs the reader sets it up or throws. |
preparing(progress:) | A setup of the reader runs now. progress is 0 to 100 while the device downloads its configuration. Then show a determinate indicator. When progress is nil, show an indeterminate indicator. |
ready | The reader is ready. acceptPayment opens Apple's screen at once. |
- Each call gets its own stream. A stream ends when its task is cancelled or the stream is released, as with a SwiftUI
.task. Abreakout of the loop alone does not end a stream that you still hold. - At checkout, call
acceptPaymentwhen the user taps the Tap to Pay on iPhone button. While the state is notready, show an initializing view, for example "Tap to Pay on iPhone will be ready soon". Apple's screen opens when the reader is ready. - After the card read, Apple's screen closes. Show a processing view until
acceptPaymentreturns.
Switch statements
The SDK uses library evolution, and its public enums are not frozen. A switch over PaymentResult.Status, TapToPayError or TapToPayErrorCode needs an @unknown default case. Without it, your app does not compile.
API reference
| Symbol | Description |
|---|---|
TapToPay.isSupported: Bool | true when the device supports Tap to Pay on iPhone. |
TapToPay.prepare(token: String) async throws | Stores the token and prepares the reader in the background. It does not show Apple's terms. It asks for location permission if the user did not answer yet. Call it once on launch and after each token renewal. |
TapToPay.acceptPayment(amount: Int, orderId: String, callbackUrl: URL?) async throws -> PaymentResult | Takes one card payment. amount is in euro cents and must be more than 0. orderId must not be empty. |
TapToPay.presentEducation(from: UIViewController) async throws | Shows Apple's screens that teach how to tap a card. |
TapToPay.isAccountLinked() async throws -> Bool | From 0.4.0. true when the account of the stored token accepted Apple's terms on this device. It never shows the terms. See Accept Apple's terms before checkout. |
TapToPay.linkAccount() async throws | From 0.4.0. Shows Apple's terms if the account did not accept them yet, then prepares the reader. Call it only from a user action. |
TapToPay.readerState: ReaderState | From 0.4.0. The reader state now: notReady, preparing(progress:) or ready. See Show the reader state. |
TapToPay.readerStates() -> AsyncStream<ReaderState> | From 0.4.0. The reader state now, then each change. Each call gets its own stream. |
PaymentResult | paymentId, status, cardBrand, last4, orderId, amount, currency, statusCode, statusMessage, authorizationCode, cardType and cardCountry. See Read the result. |
TapToPayError | The error that all calls throw. See Handle errors and unclear outcomes. |
Common questions
Why does the first payment take longer?
On the first launch, the SDK asks for location permission and waits for the answer of the user. Then it needs a location fix before it can prepare the reader, and it waits up to 15 seconds for it. On the first payment on a device, Apple can also show its Tap to Pay on iPhone terms. Call prepare on launch, so that the reader is ready before the customer pays, and offer the terms before checkout with linkAccount().
Must I call prepare again when the app comes back from the background?
No. The SDK prepares the reader again each time the app comes back from the background. Call prepare again only after a launch and after a token renewal.
Does each order need a new orderId?
Yes. Use a new, non-empty orderId for each order. MONEI stores it with the payment, and you use it to reconcile. After outcomeUnknown, do not charge again with a new orderId until you know the result of the first payment. See Unclear outcome.