Tap to Share on iPhone
Prerequisites
Tap to Share requires a newer merchant device than Tap to Pay on iPhone. What the customer can do depends on their device:
| Merchant device | Customer on iPhone with iOS 27+ | Any other customer device | |
|---|---|---|---|
| Requirements | iPhone 12 or newer, iOS 27+, Wi-Fi turned on (even when using cellular) | None | None |
| Connection | – | Direct, local wireless | Apple's web app fallback, through a server relay |
| View cart & share details | – | ✓ | ✓ |
| Apple Pay on their device | – | ✓ | – |
| App needed | Your app with MoovKit | None | None |
You'll also need:
- An existing Tap to Pay on iPhone integration, including the Tap to Pay entitlement, a registered and linked terminal application, and a
TerminalConfigurationProvider - Xcode 27 or newer, with the iOS 27 SDK
- The latest version of MoovKit
- The Tap to Share capability in Xcode, plus the Apple Pay capability with a merchant identifier if you'll accept Apple Pay on the customer's device (step 1 below)
@available(iOS 27.0, *), except isCustomerSessionSupported(). That method works on every iOS version MoovKit supports, and returns false on anything older than iOS 27.
What Tap to Share does
With Tap to Pay on iPhone, the customer taps their card or phone on the merchant's iPhone. With Tap to Share, the merchant and customer bring the tops of their iPhones together. That opens a secure session between the two devices. From there, the merchant's app can:
- Show the cart – The customer sees each item, any discounts, and the total on their own screen, and it updates live as the merchant adds items.
- Collect customer information – Ask for an email address or phone number to find a customer account or send a receipt, collect a shipping or billing address, or sign the customer up for a membership with optional email and SMS marketing opt-ins.
- Accept Apple Pay on the customer's device – The customer approves the payment with Face ID or Touch ID on their own iPhone.
- Show status screens – Show the customer screens like Payment completed or Checkout completed once the transaction finishes.
The customer doesn't need to install your app. On an iPhone running iOS 27 or newer, iOS provides the whole customer experience. Other devices get Apple's web app fallback from the same tap, where customers can review their cart and share their details. Either way, the customer decides what to share for every request.
When to use Tap to Share
Tap to Share is useful anywhere the checkout conversation happens face to face and the merchant is using an iPhone instead of a fixed point-of-sale screen:
- No more typing on the merchant's phone – Customers fill in emails and shipping addresses on their own device using AutoFill. This is faster and leads to fewer typos than reading details out loud to staff.
- Customers can see their cart – In line-busting, pop-up, or tableside settings, customers can check items and totals without leaning over the merchant's phone.
- Easier sign-ups – Capture loyalty or membership sign-ups, with email and SMS consent, as part of checkout.
- More privacy – The customer chooses exactly which fields to share, and their payment details stay on their own device.
- Easy fallback – If the customer's device can't use Tap to Share, or they'd rather not, the merchant can fall back to a regular Tap to Pay card tap in the same app.
Add the Tap to Share capability
Tap to Share builds on your existing Tap to Pay on iPhone entitlement. In Xcode, select your app target, then in Signing & Capabilities, add the Tap to Share capability.
Adding the capability to your entitlements file isn't enough on its own. Xcode also needs a provisioning profile that grants the capability to your app's bundle ID.
If you plan to accept Apple Pay on the customer's device, also add the Apple Pay capability, with a merchant identifier.
Before you submit your app, review Apple's Adding support for Tap to Share documentation and human interface guidelines.
Check device support
Before you show a Tap to Share option to the merchant, check whether their device can support it:
guard moov.isCustomerSessionSupported() else {
// Hide Tap to Share and use a Tap to Pay card tap instead.
return
}
isCustomerSessionSupported() only checks the OS and reader. There's no API to detect the iPhone model, and the simulator always returns true. That means createCustomerSession(configuration:) can still throw CustomerSessionError.notSupported, so always handle that error and keep a fallback payment path.
Open a session
First, initialize the reader with initReader(), as you do for Tap to Pay. Then, register a customer event handler before opening the session. That way, you'll catch the connected and ready events from the start.
await moov.setCustomerEventHandler { event in
switch event {
case .connected: // The customer's iPhone connected.
case .ready: // Safe to share a cart or request information.
case .disconnected: // The customer's iPhone went out of range.
case .closed: // The session ended.
case .unsupported: // An event added after this SDK release.
}
}
let configuration = CustomerSessionConfiguration(
currency: .USD,
region: .unitedStates,
privacyPolicyURL: URL(string: "https://example.com/privacy")!,
storeName: "Oakbrook Mall",
deviceName: "Emily"
)
try await moov.createCustomerSession(configuration: configuration)
The customer sees the store name, device name, and privacy policy link when they connect. Prompt the merchant to bring the tops of the two iPhones together. createCustomerSession(configuration:) returns once the devices connect. Wait for the .ready event before you send any requests. Any requests you send before that will throw CustomerSessionError.notReady.
MoovKit handles authentication for you. If you've already called createSession(invalidateTerminalConfiguration:) for Tap to Pay, MoovKit reuses that session. Otherwise, it fetches a card reader token from your TerminalConfigurationProvider.
Share the cart
Send the cart to the customer's iPhone with updateShoppingCart(_:). The customer's iPhone doesn't remember previous carts, so every call replaces the whole cart. Always send the complete cart, not just what changed.
- Enter amounts in whole currency units, like
15.00for $15.00. They're formatted using the currency and region from yourCustomerSessionConfigurationand the customer's locale. - Give every item a unique
id, and reuse it for the same product in later updates so the customer's screen can animate changes to quantity and price. - To show a placeholder on the customer's screen, share an empty cart.
let cart = ShoppingCart(
items: [
ShoppingCart.Item(
id: "HRSP-M-UN-11-001",
name: "Hair Spray",
quantity: 4,
totalPrice: Decimal(string: "20.00")!,
totalAdjustedPrice: Decimal(string: "15.00")!,
descriptions: ["Unscented, 11 oz", "Buy 3 Get 1 Free Offer"],
details: ["This item cannot be shipped."]
),
],
summary: ShoppingCart.Summary(
summaryLines: [
ShoppingCart.Summary.LineItem(label: "Subtotal", value: Decimal(string: "15.00")!),
ShoppingCart.Summary.LineItem(label: "Coupon Code: NY2OFF", value: Decimal(string: "-2.00")!),
// Values that aren't amounts are displayed verbatim.
ShoppingCart.Summary.LineItem(label: "Shipping", value: "Free"),
ShoppingCart.Summary.LineItem(label: "Tax", value: "10%"),
],
footer: "Thank you for shopping with us.",
total: Decimal(string: "14.30")!
)
)
let cartToken = try await moov.updateShoppingCart(cart)
Save the ShoppingCartToken from your most recent update. You'll need it to request Apple Pay.
Request customer information
Each request opens a form on the customer's iPhone. The customer chooses what to share, so any field you ask for can come back as nil. If the customer declines the whole request, it throws CustomerSessionError.userDeclined. You can only show one form at a time. If you send a second request while a form is open, it throws CustomerSessionError.sessionBusy.
Ask for contact details to find an existing customer account or send a receipt.
let info = try await moov.requestCustomerInfo(
purpose: .checkIn,
fields: [.emailAddress],
message: "Look up your account"
)
// info.emailAddress, info.phoneNumber
Collect a shipping or billing address. The response always includes the customer's name and postal address. You can also request contact details.
let address = try await moov.requestAddress(
purpose: .shipping,
fields: [.emailAddress, .phoneNumber]
)
// address.name, address.street, address.city, address.state, address.postalCode, ...
Sign a customer up for a membership or loyalty program, with optional email and SMS marketing opt-ins and Markdown-formatted terms.
let signUp = try await moov.requestSignup(
fields: [.emailAddress, .phoneNumber],
emailConsent: .visible,
smsConsent: .visible,
termsAndConditions: "**Terms and Conditions**\n\nYour terms, in Markdown."
)
// signUp.emailOptInSelection, signUp.smsOptInSelection
The purpose explains to the customer why you're asking. Available purposes are accountCreation, accountUpdate, billing, checkIn, membership, receipt, and shipping. Consent options can be hidden, visible, or preSelected.
Accept Apple Pay on the customer's device
Apple only shows the Apple Pay option for a cart you've already shared, so pass the ShoppingCartToken from your latest updateShoppingCart(_:) call. First, check customerClientType:
.localWireless– The customer is on an iPhone with iOS 27 or newer, and you can offer Apple Pay..web– The customer connected through Apple's web app fallback. Apple Pay isn't available, so take payment with a Tap to Pay card tap.
guard await moov.customerClientType == .localWireless else {
// Fall back to a Tap to Pay card tap on the merchant's iPhone.
return
}
let paymentRequest = PKPaymentRequest()
paymentRequest.merchantIdentifier = "merchant.com.example.posapp"
paymentRequest.currencyCode = "USD"
paymentRequest.countryCode = "US"
paymentRequest.supportedNetworks = [.amex, .discover, .masterCard, .visa]
paymentRequest.merchantCapabilities = .threeDSecure
paymentRequest.paymentSummaryItems = paymentSummaryItems
let presented = try await moov.requestApplePayPayment(
for: cartToken,
using: paymentRequest,
delegate: paymentAuthorizationControllerDelegate
)
if !presented {
// The customer chose another option or hasn't set up Apple Pay.
// Fall back to another payment method.
}
MoovKit shows the payment sheet and passes the authorization to your PKPaymentAuthorizationControllerDelegate. It doesn't process the resulting PKPayment. Your delegate sends the payment to your backend, and your backend completes it the same way as Apple Pay on the web:
-
Link the token to the payer's account. Call the link Apple Pay token endpoint with the
accountIDof the customer paying, and pass in the token from thePKPayment. Your access token needs the/accounts/{accountID}/cards.writescope for that account.curl -X POST "https://api.moov.io/accounts/{payerAccountID}/apple-pay/tokens" \ -H "Authorization: Bearer {token}" \ -H "x-moov-version: v2026.07.00" \ --data '{ "token": { "paymentData": { ... }, "paymentMethod": { ... }, "transactionIdentifier": "string" } }'On API
v2026.04.00and newer, the response is an array of payment methods. Use thepaymentMethodIDwith apaymentMethodTypeofapple-pay. -
Create a transfer. Call the create transfer endpoint with the
apple-paypayment method as thesourceand the merchant's Moov wallet as thedestination. Add theX-Wait-Forheader to get a synchronous response. -
Finish the payment sheet. Call the delegate's completion handler with
.successor.failure, based on the transfer result.
func paymentAuthorizationController(
_ controller: PKPaymentAuthorizationController,
didAuthorizePayment payment: PKPayment,
handler completion: @escaping (PKPaymentAuthorizationResult) -> Void
) {
Task {
do {
// Your backend links payment.token and creates the transfer.
try await partnerClient.payWithApplePay(token: payment.token, amount: total)
completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
try await moov.updateCustomerStatus(.paymentCompleted)
} catch {
completion(PKPaymentAuthorizationResult(status: .failure, errors: [error]))
}
}
}
If Apple Pay isn't available, the merchant can still take payment with a regular Tap to Pay card tap on their own iPhone. Customer details collected through Tap to Share still work with that flow.
Update status & close the session
After the payment goes through, show a status screen on the customer's iPhone. You can do this anytime a request form isn't open.
try await moov.updateCustomerStatus(.paymentCompleted)
Available statuses are ready, paymentCompleted, paymentFailed, and checkoutCompleted.
Close the session as soon as the checkout is done, so the customer's iPhone doesn't stay connected:
await moov.closeCustomerSession()
MoovKit returns customer details directly to your app. Your app is responsible for storing, using, and deleting that data in line with the privacy policy you linked in CustomerSessionConfiguration.
Test mode
Tap to Share follows the same mode as your terminal configuration. In test mode, the session doesn't show anything on a customer device. Instead, it records what you shared and returns sample customer responses. That means you can test the whole flow in the iOS 27 Simulator without a second device. Requests sent before the session opens still throw CustomerSessionError.notReady, just like they would in production.
Moov doesn't support Apple Pay in test mode. In test mode, requestApplePayPayment(for:using:delegate:) always returns false, so your integration follows its normal fallback path.
| Simulator | Physical device | |
|---|---|---|
| Test mode session | ✓ | ✓ |
| Cart, status, and request flow | ✓ | ✓ |
| Real customer connection | – | Requires a second iPhone |
| Apple Pay on the customer's device | – | Production only |
You can't open a production session in the Simulator. There, createCustomerSession(configuration:) throws CustomerSessionError.notSupported. To test end to end, you need two physical iPhones, both on iOS 27 with Wi-Fi turned on. The merchant's iPhone needs to be an iPhone 12 or newer, with the entitlement and capability.
Error handling
Session errors are reported as CustomerSessionError. Make sure to handle these errors:
| Error | Meaning | Suggested handling |
|---|---|---|
notSupported |
The device or OS can't run Tap to Share | Hide Tap to Share and use a Tap to Pay card tap instead |
wifiDisabled |
Wi-Fi is off | Ask the merchant to turn on Wi-Fi, even if they're using cellular |
notReady |
A request was sent before the .ready event |
Wait for .ready before sending requests |
sessionBusy |
A form is already showing on the customer's device | Wait for the current request to finish |
userDeclined |
The customer declined to share | Continue without the information, or ask the merchant to collect it another way |
incompatibleRequest |
The customer's device or connection type can't handle the request | Check customerClientType and use a fallback |
connectionFailed, pairingFailed |
The devices couldn't connect | Ask the merchant to bring the iPhones together and try again |
paymentRequestCancelled |
The customer dismissed the Apple Pay sheet | Offer another payment method |
If initReader() hasn't been called, createCustomerSession(configuration:) throws SDKError.notInitialized.
View the full Tap to Share SDK documentation for more information. If you run into issues, create a support ticket and attach the SDK logs, as described in the Tap to Pay on iPhone guide.
Tap to Share requires iPhone 12 or later and iOS 27 or later. Tap to Pay on iPhone requires a supported payment app and the latest version of iOS. Some contactless cards may not be accepted by your payment app. Transaction limits may apply. Tap to Pay on iPhone isn't available in all markets. For Tap to Pay on iPhone countries and regions, see https://developer.apple.com/tap-to-pay/regions/.