
Axiom Payments
- 443 installs
- 1.1k repo stars
- Updated August 3, 2026
- charleswiltgen/axiom
axiom-payments is an iOS monetization skill that implements StoreKit 2 in-app purchases, subscriptions, restore flows, and receipt validation for developers who ship paid or subscription iOS applications.
About
axiom-payments is a StoreKit 2 implementation skill from the charleswiltgen/axiom collection for monetized iOS apps. It guides developers through in-app purchases, auto-renewable subscriptions, purchase restore flows, and receipt validation hooks required for App Store compliance. The skill fits when SwiftUI or UIKit apps need product identifiers, transaction listeners, entitlement checks, and server-side validation without StoreKit 1 legacy patterns. Developers reach for axiom-payments while wiring paywalls, handling renewal states, and debugging sandbox purchase failures before App Store review.
- StoreKit 2 product queries
- Subscription lifecycle handling
- Transaction verification
- Restore purchases flow
- Paywall and entitlement gating
Axiom Payments by the numbers
- 443 all-time installs (skills.sh)
- Ranked #322 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-paymentsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 443 |
|---|---|
| repo stars | ★ 1.1k |
| Last updated | August 3, 2026 |
| Repository | charleswiltgen/axiom ↗ |
How do you implement StoreKit 2 subscriptions in iOS?
Implementing StoreKit 2 in-app purchases, subscriptions, restore flows, and receipt validation for monetized iOS applications.
Who is it for?
iOS developers adding StoreKit 2 monetization with subscriptions, one-time purchases, and receipt validation to Swift apps.
Skip if: Android Google Play Billing projects or web-only Stripe checkout without a native iOS StoreKit layer.
When should I use this skill?
A developer asks to add in-app purchases, subscriptions, restore purchases, or receipt validation with StoreKit 2 on iOS.
What you get
StoreKit 2 purchase handlers, subscription entitlement logic, restore flows, and receipt validation integration code.
- StoreKit 2 purchase code
- subscription handlers
- receipt validation hooks
Files
Real-World Payments (Apple Pay / Wallet / Tap to Pay)
You MUST use this skill when accepting ANY real-world payment — physical goods, services, donations, ticketing, loyalty cards, contactless card-present, or post-purchase order tracking. NOT for in-app purchase or digital content.
When to Use
Use this skill when you encounter:
- Adding Apple Pay to an iOS / iPadOS / macOS / Catalyst / visionOS / watchOS app
- Adding Apple Pay to a website (Apple Pay JS or W3C Payment Request API)
- Building Wallet passes (boarding passes, event tickets, coupons, loyalty cards, store cards)
- Accepting contactless card payments on iPhone (Tap to Pay on iPhone / ProximityReader)
- Surfacing post-purchase order tracking in Wallet (Orders in Wallet, FinanceKitUI add-order helpers)
- Issuer or bank card provisioning into Wallet (issuer extensions)
- Apple Pay merchant ID, processing certificate, or merchant identity certificate setup
- Pass Type ID or Order Type ID certificates and PKCS #7 signing chains
- Tap to Pay managed entitlement (
com.apple.developer.proximity-reader.payment.acceptance) - App Review rejections that mention payments, IAP, Apple Pay, Wallet, or Acceptable Use Guidelines
- Domain verification for Apple Pay on the web (
.well-known/apple-developer-merchantid-domain-association.txt) - Sandbox testing with Apple Pay sandbox cards or sandbox tester accounts
When NOT to Use
| Issue | Correct Skill | Why NOT axiom-payments |
|---|---|---|
| In-app purchase, subscriptions for digital content | axiom-integration | StoreKit 2 / digital goods boundary; see axiom-integration (skills/in-app-purchases.md) |
| Generic NFC tag reads (Core NFC, non-Wallet) | axiom-integration | Different framework; PassKit NFC is Wallet-specific |
| Code signing / provisioning fundamentals | axiom-security | Code signing is generic; payment certs are added to that flow |
| App Store rejection workflow & appeals | axiom-shipping | Rejection workflow lives there; payment-specific rejection patterns route from there into here |
| HIG cross-cutting design guidance | axiom-design | Apple Pay / Wallet HIG specifics live here, but design overview lives in axiom-design |
| Consumer banking surface (FinanceKit) | Out of scope | This suite uses FinanceKitUI only for Orders. Account aggregation / transaction queries are not covered. |
| PKSecureElementPass, transit cards, car keys | Out of scope | Issuer-controlled, not relevant to merchant developers |
Quick Reference
| Symptom / Task | Reference |
|---|---|
| Should this be Apple Pay or IAP? | See skills/apple-pay-vs-iap.md |
| Selling physical goods, services, or donations | See skills/apple-pay-vs-iap.md |
| App was rejected for using IAP for physical goods | See skills/apple-pay-vs-iap.md |
| Native Apple Pay (iOS / iPadOS / macOS / Catalyst / visionOS / watchOS) | See skills/apple-pay.md |
| PassKit / PKPaymentRequest API surface | See skills/apple-pay-ref.md |
| Apple Pay on the web (Apple Pay JS or Payment Request API) | See skills/apple-pay-web.md |
| Web ApplePaySession / Payment Request API surface | See skills/apple-pay-web-ref.md |
| Domain verification, merchant identity cert, third-party browser support | See skills/apple-pay-web.md |
| Tap to Pay on iPhone (ProximityReader) | See skills/tap-to-pay.md |
| ProximityReader / PaymentCardReader API surface | See skills/tap-to-pay-ref.md |
| Tap to Pay entitlement stuck in "Submitted" | See skills/tap-to-pay.md |
| Wallet passes (boarding, event ticket, coupon, loyalty, store card) | See skills/wallet-passes.md |
| pass.json schema, semantic tags, barcodes, NFC payloads | See skills/wallet-passes-ref.md |
Poster generic style, featured actions, new barcode types OS27 | See skills/wallet-passes-ref.md |
| Pass Designer app, Pass Builder server package | See skills/wallet-passes-ref.md |
Customer engagement on a paired device (CustomerEngagementSession) OS27 | See skills/tap-to-pay-ref.md |
| Pass signing, manifest hashing, PKCS #7 | See skills/wallet-passes.md |
| Pass updates not arriving (web service / APNs) | See skills/wallet-passes.md |
| Orders in Wallet, signed order packages, fulfillment status | See skills/wallet-orders.md |
| Issuer / bank card provisioning extensions | See skills/wallet-extensions-ref.md |
| "No payment sheet appears" / merchant validation 503 / pass won't import | See skills/payments-diag.md |
| Sandbox testing failures, prod-vs-sandbox cert mismatch | See skills/payments-diag.md |
| Apple Pay button vs Apple Pay Mark confusion | See skills/apple-pay.md |
| App Review rejection for Apple Pay / Wallet / Tap to Pay | See skills/payments-diag.md |
Decision Tree
digraph payments {
"Payments question?" [shape=diamond];
"Apple Pay or IAP?" [shape=diamond];
"Where is the surface?" [shape=diamond];
"What kind of pass?" [shape=diamond];
"Something not working?" [shape=diamond];
"skills/apple-pay-vs-iap.md" [shape=box];
"skills/apple-pay.md" [shape=box];
"skills/apple-pay-web.md" [shape=box];
"skills/tap-to-pay.md" [shape=box];
"skills/wallet-passes.md" [shape=box];
"skills/wallet-orders.md" [shape=box];
"skills/wallet-extensions-ref.md" [shape=box];
"skills/payments-diag.md" [shape=box];
"axiom-integration\n/in-app-purchases.md" [shape=box];
"Payments question?" -> "Apple Pay or IAP?";
"Apple Pay or IAP?" -> "skills/apple-pay-vs-iap.md" [label="boundary unclear"];
"Apple Pay or IAP?" -> "axiom-integration\n/in-app-purchases.md" [label="digital content /\nsubscription for digital"];
"Apple Pay or IAP?" -> "Where is the surface?" [label="real-world payment"];
"Where is the surface?" -> "skills/apple-pay.md" [label="native app"];
"Where is the surface?" -> "skills/apple-pay-web.md" [label="website"];
"Where is the surface?" -> "skills/tap-to-pay.md" [label="contactless on iPhone"];
"Where is the surface?" -> "What kind of pass?" [label="pass / order"];
"Where is the surface?" -> "skills/wallet-extensions-ref.md" [label="card provisioning\n(issuer / bank)"];
"What kind of pass?" -> "skills/wallet-passes.md" [label="boarding / ticket /\ncoupon / loyalty"];
"What kind of pass?" -> "skills/wallet-orders.md" [label="post-purchase\norder tracking"];
"Payments question?" -> "Something not working?";
"Something not working?" -> "skills/payments-diag.md" [label="yes"];
}Simplified routing:
1. Is this a digital good or subscription for digital content? → Use `axiom-integration/skills/in-app-purchases.md` instead. 2. Selling physical goods / services / donations, or unsure? → skills/apple-pay-vs-iap.md 3. Native Apple Pay (iOS / iPadOS / macOS / Catalyst / visionOS / watchOS)? → skills/apple-pay.md + skills/apple-pay-ref.md 4. Apple Pay on the web? → skills/apple-pay-web.md + skills/apple-pay-web-ref.md 5. Tap to Pay on iPhone (ProximityReader)? → skills/tap-to-pay.md + skills/tap-to-pay-ref.md 6. Wallet passes (ticket / coupon / loyalty / store card)? → skills/wallet-passes.md + skills/wallet-passes-ref.md 7. Orders in Wallet (post-purchase tracking)? → skills/wallet-orders.md 8. Issuer / bank card provisioning? → skills/wallet-extensions-ref.md 9. Something not working (no sheet / merchant validation fails / pass won't import / Tap to Pay never enables)? → skills/payments-diag.md
Cross-Suite Routing
Apple Pay vs IAP boundary (the most common cross-suite question):
- Selling physical goods, services, or donations → stay here (
skills/apple-pay-vs-iap.md) - Selling digital content, premium app features, subscriptions for digital content → use axiom-integration (
skills/in-app-purchases.md) - App was rejected for using the wrong one →
skills/apple-pay-vs-iap.mdthenskills/payments-diag.md
Payments + App Review rejection:
- Rejection cites Section 3.1 / 3.2 / Apple Pay AUG → stay here (
skills/payments-diag.md) for the root cause; also invoke axiom-shipping (skills/app-store-diag.md) for appeal workflow
Payments + cert management:
- Merchant Identity Certificate, Pass Type ID Certificate, Order Type ID Certificate, Payment Processing Certificate — operational discipline → stay here
- Generic Keychain export /
.p12mechanics → also invoke axiom-security (skills/keychain-ref.md) - Tap to Pay managed entitlement → stay here (
skills/tap-to-pay.md); generic managed-capability mental model → axiom-security (skills/code-signing.md)
Payments + Xcode capability / provisioning:
- Apple Pay capability checkbox in Xcode, merchant ID selection, Tap to Pay entitlement plumbing in the provisioning profile → also invoke axiom-build for capability/profile mechanics; payment-specific guidance stays here
Payments + HIG:
- Apple Pay button vs Apple Pay Mark, Wallet pass design specs, Tap to Pay button label → stay here
- Cross-cutting design context → also invoke axiom-design (
skills/hig.md)
Payments + Catalyst / macOS:
- Apple Pay on Mac and Catalyst (window requirement, web security model, static merchant validation URL) → stay here (
skills/apple-pay.mdCatalyst section) - Generic Catalyst patterns → axiom-macos
Payments + Apple Watch:
- WKInterfacePaymentButton, watchOS payment delegate flow → stay here (
skills/apple-pay-ref.mdwatchOS section) - Generic watchOS patterns → axiom-watchos
Anti-Rationalization
| Thought | Reality |
|---|---|
| "We sell physical stuff but we'll use IAP — easier integration" | Guaranteed App Review rejection (Section 3.1.1 / 3.1.3(e)). Use Apple Pay for physical goods, services, donations. |
| "Our app is digital content, we'll use Apple Pay because IAP fees are higher" | Guaranteed App Review rejection. Digital content + subscriptions for digital content must use IAP. |
| "I'll just embed our PSP's raw card form on the web — it's faster" | Acceptable Use Guidelines violate parity rule. If you accept any other payment method on the web, you must offer Apple Pay at least as prominently. |
| "Tap to Pay just needs a capability checkbox like other features" | Tap to Pay uses a managed entitlement requested via a separate form. The Xcode capability flow doesn't apply. Two-step request (dev → distribution); rejection or "Submitted" stalls add 1–4 weeks per loop. |
| "We'll roll our own pass signing — it's just zip + sign" | PKCS #7 detached signature, manifest hashing, WWDR Intermediate cert, S/MIME signing-time, PEM/DER format gotchas — most signing failures originate from rolled-from-scratch implementations. Use a server library. |
| "The Apple Pay Mark is just a button graphic" | The Mark is "Apple Pay accepted" signage — never tappable. The Button is API-provided and initiates payment. Using the Mark as a button is an HIG violation and a known conversion killer. |
| "Production cards will work the same as sandbox cards" | Sandbox transactions decline pre-fulfillment by design. Production transactions need production keys + activated certs. Test on real devices with real cards before launch. |
| "Apple Pay merchant ID expires every year" | Merchant IDs never expire. Payment Processing Certificates expire after 25 months. These are different things. |
| "I can call merchant validation from the browser" | The browser must never call paymentSession. Server-only call using two-way TLS with the merchant identity cert. Calling from the browser leaks the cert. |
| "FinanceKit will let our app see the user's bank transactions" | Out of scope for this suite. FinanceKit consumer banking surface is not covered. We use FinanceKitUI only for the order-add helpers. |
Out of Scope
This suite intentionally does not cover:
- In-App Purchase (StoreKit 2) — Use
axiom-integration/skills/in-app-purchases.mdandaxiom-integration/skills/storekit-ref.md. The boundary rule lives inskills/apple-pay-vs-iap.md. - FinanceKit consumer banking (account aggregation, transaction queries) — not covered. We only use FinanceKitUI for order-add buttons.
- PKSecureElementPass, transit cards, car keys — issuer-controlled Wallet surfaces, not merchant-developer territory.
- Tap to Present ID (`MobileDocumentReader`) — uses ProximityReader but for identity verification, not payment. Brief cross-ref appears in
skills/tap-to-pay-ref.md; identity-verification UX as a whole is axiom-integration territory if it lands anywhere. - Generic NFC reads (Core NFC) — different framework. PassKit NFC is Wallet-specific (loyalty / contactless ticket / boarding).
Example Invocations
User: "Should I use Apple Pay or IAP for my hotel-booking app?" → See skills/apple-pay-vs-iap.md
User: "How do I set up Apple Pay in my iOS app?" → See skills/apple-pay.md
User: "What's the structure of PKPaymentRequest?" → See skills/apple-pay-ref.md
User: "Apple Pay on my website doesn't show the button in Chrome" → See skills/apple-pay-web.md
User: "Domain verification keeps failing" → See skills/payments-diag.md
User: "I want to add Tap to Pay on iPhone to my point-of-sale app" → See skills/tap-to-pay.md
User: "My Tap to Pay entitlement has been Submitted for two weeks" → See skills/tap-to-pay.md and skills/payments-diag.md
User: "How do I build a Wallet pass for my event tickets?" → See skills/wallet-passes.md
User: "My .pkpass file won't import — Wallet says invalid" → See skills/payments-diag.md
User: "I want order tracking to appear in Wallet after Apple Pay checkout" → See skills/wallet-orders.md
User: "App Review rejected my app for using IAP for restaurant delivery" → See skills/apple-pay-vs-iap.md then skills/payments-diag.md
User: "How does Apple Pay differ from In-App Purchase?" → See skills/apple-pay-vs-iap.md
User: "Implementing card provisioning for our bank's iOS app" → See skills/wallet-extensions-ref.md
Resources
WWDC: 2020-10662, 2021-10092, 2022-10041, 2023-10114, 2024-10108
Tech Talks: 111381 (Apple Pay on the Web), 110336 (Implementing Apple Pay Orders)
MIG: Apple Pay Merchant Integration Guide (2026 edition) — operational spine, cited throughout this suite
Docs: /passkit, /applepayontheweb, /proximityreader, /walletpasses, /design/human-interface-guidelines/apple-pay, /design/human-interface-guidelines/wallet, /apple-pay/acceptable-use-guidelines-for-websites
App Review: Section 3.1 (In-App Purchase), Section 3.1.3(e) (Goods and Services Outside of the App), Section 3.2 (Other Business Model Issues), Section 4.9 (Apple Pay)
Skills: axiom-integration (in-app-purchases, storekit-ref), axiom-shipping (app-store-diag, app-review-guidelines), axiom-security (keychain-ref, code-signing), axiom-design (hig, hig-ref), axiom-macos
Apple Pay — PassKit API Reference
API surface for native Apple Pay across iOS, iPadOS, macOS, Catalyst, visionOS, and watchOS. For the discipline (when/how/why), see apple-pay.md. For web, see apple-pay-web-ref.md.
Core Classes
| Class | Role | Available on |
|---|---|---|
PKPaymentAuthorizationController | Headless controller; preferred entry point. Used with a delegate. | iOS 8+, iPadOS 8+, macOS 11+, Catalyst 13.1+, visionOS 1+, watchOS 3+ |
PKPaymentAuthorizationViewController | UIKit/AppKit view controller form. | iOS 8+, iPadOS 8+, macOS 11+, Catalyst 13.1+, visionOS 1+ |
PKPaymentRequest | The request object describing the purchase. | All Apple Pay platforms |
PKPayment | Payload returned in didAuthorizePayment. Contains token, billingContact, shippingContact, shippingMethod. | All |
PKPaymentToken | The encrypted blob wrapper; contains paymentMethod, transactionIdentifier, paymentData. | All |
PKContact | Address + name + phone + email container. | All |
PKPaymentMethod | Display info: displayName, network, type (credit/debit/prepaid/store). | All |
Delegate Protocols
| Protocol | Methods (selected) |
|---|---|
PKPaymentAuthorizationControllerDelegate | paymentAuthorizationController(_:didAuthorizePayment:handler:), paymentAuthorizationController(_:didChangeShippingContact:handler:), paymentAuthorizationController(_:didChangeShippingMethod:handler:), paymentAuthorizationController(_:didChangePaymentMethod:handler:), paymentAuthorizationController(_:didChangeCouponCode:handler:), paymentAuthorizationController(_:didRequestMerchantSessionUpdate:) (Mac/Catalyst), paymentAuthorizationControllerDidFinish(_:) |
PKPaymentAuthorizationViewControllerDelegate | Same callbacks, scoped to view-controller form |
All change callbacks deliver an update type (PKPaymentRequestShippingContactUpdate, PKPaymentRequestShippingMethodUpdate, PKPaymentRequestPaymentMethodUpdate, PKPaymentRequestCouponCodeUpdate) carrying refreshed summary items + errors. 30-second response window per callback.
Payment-Request Variants
Set at most one on a PKPaymentRequest:
| Property | Type | Use case |
|---|---|---|
recurringPaymentRequest | PKRecurringPaymentRequest | Subscriptions at fixed intervals (regular or trial billing cycle) |
automaticReloadPaymentRequest | PKAutomaticReloadPaymentRequest | Auto top-up at threshold (transit, store-card balance) |
deferredPaymentRequest | PKDeferredPaymentRequest | Hotel / pre-order / car rental (free-cancellation period + bill-on date) |
multiTokenContexts | [PKPaymentTokenContext] | Multi-merchant in one sheet (e.g. travel-booking) |
applePayLaterAvailability | PKPaymentRequest.ApplePayLaterAvailability | .available / .unavailable (US-only, requires entitlement) |
PKDisbursementRequest is a separate request type (not a property of PKPaymentRequest) for funds-out flows; pair with PKInstantFundsOutFeeSummaryItem for fee disclosure.
Merchant Information
| Property | Type | Purpose |
|---|---|---|
merchantIdentifier | String | merchant.com.example.foo reverse-DNS form |
merchantCapabilities | PKMerchantCapability | .threeDSecure, .credit, .debit, .emv (option-set) |
merchantCategoryCode | PKPaymentRequest.MerchantCategoryCode | ISO 18245 four-digit MCC (WWDC24); set when supported card types vary by category |
attributionIdentifier | String? | Attribution data for partner integrations |
isDelegatedRequest | Bool | True when a delegated entity is making the request on behalf of the merchant |
applicationData | Data? | Hash committed into the payment token's header.applicationData; opaque to Apple |
Networks and Capabilities
request.supportedNetworks: [PKPaymentNetwork]
request.merchantCapabilities: PKMerchantCapability // .threeDSecure, .credit, .debit, .emv
request.supportedCountries: Set<String>? // ISO 3166 2-letter
request.countryCode: String // your merchant's country
request.currencyCode: String // ISO 4217 3-letterPKPaymentNetwork | Coverage |
|---|---|
.visa, .masterCard, .amex, .discover | Global |
.chinaUnionPay | Mainland China |
.interac | Canada |
.eftpos | Australia |
.electron, .maestro, .vPay | Europe (Visa / Mastercard variants) |
.JCB | Japan |
.mada | Saudi Arabia |
.idCredit, .quicPay | Japan domestic |
Use PKPaymentRequest.availableNetworks() to query device-supported networks at runtime instead of hard-coding.
unsupportedPrimaryAccountIdentifiers: [String] OS27 (iOS/macOS/watchOS/visionOS 27) — primary account identifiers excluded from funding the payment; per the header, for merchants who are also the card issuer, to prevent self-funding scenarios.
Bancomat naming flip-flop (Italy): the 26.5 SDK deprecates .pagoBancomat in favor of .bancomat; the 27 SDK reverses this — .bancomat is deprecated in favor of .pagoBancomat. Follow the SDK you build with.
Summary Items
Order in paymentSummaryItems matters: the last item is the line displayed next to "Pay" on the sheet, with its label being the customer-facing business name.
| Type | Purpose |
|---|---|
PKPaymentSummaryItem | Generic line item (label, amount, type) |
PKRecurringPaymentSummaryItem | Carries intervalUnit, intervalCount, startDate, endDate |
PKDeferredPaymentSummaryItem | Carries deferredDate (when payment will occur) |
PKAutomaticReloadPaymentSummaryItem | Carries thresholdAmount |
PKDisbursementSummaryItem | For funds-out flows |
PKInstantFundsOutFeeSummaryItem | Fee line for instant disbursement |
Summary item type is .final (default) or .pending for unknown amounts (rideshare, post-pay). Pending items show "Pending" instead of the amount.
Contact Fields
request.requiredBillingContactFields: Set<PKContactField>
request.requiredShippingContactFields: Set<PKContactField>PKContactField cases: .postalAddress, .name, .phoneNumber, .emailAddress, .phoneticName.
Privacy discipline: request only what fulfilment needs. Apple penalizes over-collection in HIG review.
Deprecated (iOS 11+, do not use): requiredBillingAddressFields, requiredShippingAddressFields, PKAddressField enum.
Pre-populating known contacts
request.billingContact = existingBillingContact // PKContact
request.shippingContact = existingShippingContact // PKContactSkips the fields the user has already provided in your account flow.
Shipping
request.shippingMethods: [PKShippingMethod]
request.shippingType: PKShippingType
request.shippingContactEditingMode: PKShippingContactEditingModePKShippingType | Sheet language |
|---|---|
.shipping (default) | "Shipping" / "Ship to" |
.delivery | "Delivery" |
.storePickup | "Pickup" |
.servicePickup | "Service pickup" |
PKShippingContactEditingMode: .available (default — user can edit), .storePickup (read-only — for in-store pickup, see /passkit/displaying-a-read-only-pickup-address).
PKDateComponentsRange (WWDC21)
Use on PKShippingMethod.dateComponentsRange to express delivery windows. Carries startDateComponents and endDateComponents plus calendar metadata so Wallet can render localized ranges:
let arriving = PKDateComponentsRange(
start: DateComponents(year: 2026, month: 5, day: 5),
end: DateComponents(year: 2026, month: 5, day: 7)
)!
let method = PKShippingMethod(label: "Standard", amount: 5.99)
method.dateComponentsRange = arriving
method.detail = "Arrives May 5–7"Coupon Codes (WWDC21)
request.supportsCouponCode = true
request.couponCode = "" // empty = show input field; non-empty = pre-populateImplement paymentAuthorizationController(_:didChangeCouponCode:handler:) to validate and respond with an updated summary or paymentCouponCodeInvalidError(localizedDescription:) / paymentCouponCodeExpiredError(localizedDescription:).
Errors
PKPaymentError is the error type. Construct via convenience class methods on PKPaymentRequest:
| Constructor | Use |
|---|---|
paymentBillingAddressInvalidError(withKey:localizedDescription:) | Bad billing field (use CNPostalAddressKey constants for key) |
paymentShippingAddressInvalidError(withKey:localizedDescription:) | Bad shipping field |
paymentShippingAddressUnserviceableError(withLocalizedDescription:) | Address valid but you don't ship there |
paymentContactInvalidError(withContactField:localizedDescription:) | Bad name/email/phone — pass PKContactField |
paymentCouponCodeInvalidError(localizedDescription:) | Coupon malformed |
paymentCouponCodeExpiredError(localizedDescription:) | Coupon past expiry |
Errors flow back via the update.errors array on each change callback's update object, or via PKPaymentAuthorizationResult(status: .failure, errors: [...]) on the final auth.
SwiftUI Buttons (WWDC22, iOS 16+)
| View | Purpose |
|---|---|
PayWithApplePayButton(_:action:) | Initiates Apple Pay; uses system styling (_PassKit_SwiftUI) |
AddPassToWalletButton(action:) | Adds a .pkpass to Wallet (_PassKit_SwiftUI; see wallet-passes.md) |
VerifyIdentityWithWalletButton(_:action:) | Identity verification via Wallet (_PassKit_SwiftUI; axiom-integration territory) |
AddOrderToWalletButton is not a PassKit button — it lives in FinanceKitUI (iOS 17+, iOS-only) and takes no action: closure. Its only initializer is AddOrderToWalletButton(signedArchive: Data, onCompletion: @escaping (Result<FinanceStore.SaveOrderResult, Error>) -> Void). Style via .addOrderToWalletButtonStyle(_:) with AddOrderToWalletButtonStyle (.black / .blackOutline). See wallet-orders.md.
Modifiers:
PayWithApplePayButton(.buy) { ... }
.payWithApplePayButtonStyle(.automatic) // .black, .white, .whiteOutline, .automatic
.frame(height: 45)
.disabled(!canPay)PKPaymentButtonType: .plain, .buy, .setUp, .inStore, .donate, .checkout, .book, .subscribe, .reload, .addMoney, .topUp, .order, .rent, .support, .contribute, .tip, .continue (case selection drives the button label localization).
PKPaymentButtonStyle (UIKit): .white, .whiteOutline, .black, .automatic.
Payment Token Format
PKPaymentToken.paymentData is a UTF-8 JSON dictionary with this shape:
{
"version": "EC_v1", // or "RSA_v1"
"data": "<base64 encrypted payment data>",
"signature": "<base64 detached PKCS #7 signature>",
"header": {
"publicKeyHash": "<base64 SHA-256 of merchant public key>",
"transactionId": "<hex>",
// EC_v1 only:
"ephemeralPublicKey": "<base64 X.509 encoded key>",
// RSA_v1 only:
"wrappedKey": "<base64 symmetric key wrapped with merchant RSA public key>",
// Optional, both versions:
"applicationData": "<hex SHA-256 of original PKPaymentRequest.applicationData>"
}
}| Field | Notes |
|---|---|
signature | Detached PKCS #7 envelope (not a raw ECDSA / RSA signature). Algorithm and signing certificate live inside the CMS structure. |
version | EC_v1 for ECC-encrypted (most regions); RSA_v1 for RSA-encrypted (used where ECC is unavailable due to regulation, e.g. mainland China). |
applicationData | SHA-256 hash of PKPaymentRequest.applicationData. Omitted from the header if the original property was nil. Use to bind the token to a specific order ID. |
wrappedKey | RSA_v1 only. Symmetric key wrapped with your RSA public key; unwrap with your RSA private key. |
ephemeralPublicKey | EC_v1 only. ANSI X.963 / X.509 encoded ephemeral public key. |
Verification + decryption
Per /passkit/payment-token-format-reference:
1. Verify the signature. The signature is over ephemeralPublicKey || data || transactionId || applicationData (EC_v1) or wrappedKey || data || transactionId || applicationData (RSA_v1). Validate the X.509 chain to Apple Root CA — G3, check the marker OIDs (1.2.840.113635.100.6.29 leaf, 1.2.840.113635.100.6.2.14 intermediate), and verify CMS signing time is within 5 minutes of the transaction. 2. Identify the merchant key via publicKeyHash (matches the SHA-256 of your Payment Processing certificate's public key). 3. Restore the symmetric key. EC_v1: ECDH from ephemeralPublicKey + your private key, then NIST-style KDF. RSA_v1: unwrap wrappedKey with your RSA private key. Apple delegates the KDF specifics to /passkit/restoring-the-symmetric-key. 4. Decrypt `data`. EC_v1 uses AES-256-GCM (id-aes256-GCM); RSA_v1 uses AES-128-GCM (id-aes128-GCM). Both modes use a 16-null-byte IV with no associated authentication data (AAD). 5. Verify uniqueness of transactionId against your processed-payment store (5-minute window). 6. Verify business fields in the decrypted payload: currencyCode, transactionAmount, applicationData hash matches your stored request.
The decryption key never belongs on the device. Most merchants pass the encrypted blob through to the PSP; only self-decrypt if you're the merchant of record AND you generated the CSR yourself.
Decrypted payment-data shape (selected keys)
| Key | Description |
|---|---|
applicationPrimaryAccountNumber | DPAN — the device-specific tokenized PAN |
applicationExpirationDate | YYMMDD |
currencyCode | ISO 4217 numeric, as string (preserves leading zeros) |
transactionAmount | Number |
cardholderName | Optional |
paymentDataType | "3DSecure" or "EMV" |
paymentData | Nested dict — onlinePaymentCryptogram + eciIndicator (3DSecure), or emvData + encryptedPINData (EMV; RSA_v1 only) |
authenticationResponses | Multi-token requests only — list of submerchant cryptograms |
merchantTokenIdentifier / merchantTokenMetadata | Merchant-token (MPAN) requests only |
PKPaymentMethod.type: .unknown, .debit, .credit, .prepaid, .store.
In-App Sequence Diagram (MIG p.26)
[1] Customer taps Apple Pay button
[2] App constructs PKPaymentRequest
[3] App presents PKPaymentAuthorizationController
[4] System displays sheet
[5] Customer interacts (shipping / coupon / method changes)
[6] Each interaction → delegate change callback → app responds with update
[7] Customer authenticates (Face/Touch/Optic ID)
[8] System encrypts payment data with merchant's Payment Processing public key
[9] System calls didAuthorizePayment with PKPayment (encrypted token + contacts)
[10] App POSTs token to merchant server
[11] Merchant server forwards to PSP (encrypted blob OR self-decrypted card data)
[12] PSP authorizes via acquirer / network / issuer
[13] PSP returns success/failure to merchant server
[14] Merchant server returns to app
[15] App calls completion handler with PKPaymentAuthorizationResult
[16] System dismisses sheet with result animation
[17] Optional: PKPaymentOrderDetails handoff to Wallet Orders surfaceSteps 11–13 happen out-of-band over the merchant-controlled network path. Steps 7–9 are the trust boundary — Apple's public key is what protects the card data in transit between Wallet and the PSP.
Apple Pay Later API (WWDC23, US-only)
| Type | Purpose |
|---|---|
PKPayLaterValidateAmount(_:currencyCode:completion:) | Free C function in PKPayLaterValidator.h (iOS 17+, iOS-only) — completion receives a BOOL eligible telling you whether the amount qualifies for merchandising |
PKPayLaterView (UIKit) / PayLaterView (SwiftUI, _PassKit_SwiftUI) | Pre-checkout merchandising surface |
PKPaymentRequest.applePayLaterAvailability | .available / .unavailable |
PKPayLaterValidateAmount is declared NS_REFINED_FOR_SWIFT, so the importer generates the Swift-projected name; the C signature is void PKPayLaterValidateAmount(NSDecimalNumber *amount, NSString *currencyCode, void(^completion)(BOOL eligible)). It is iOS-only (API_UNAVAILABLE(macos, watchos, tvos)), so there is no Catalyst/macOS/watchOS form.
Mark .unavailable for prohibited categories (subscriptions, recurring items, gift cards). The PKPayLaterView / PayLaterView is the merchandising widget you place on product / cart pages to indicate "Pay Later available" before checkout.
watchOS
WKInterfacePaymentButton — Storyboard / WatchKit-only; no SwiftUI equivalent on watchOS yet. Configure via setLabel(_:) / setStyle(_:) and wire the action through the storyboard. Delegate flow uses PKPaymentAuthorizationController (same as iOS) with these adaptations:
- No shipping picker on the watch. Resolve shipping pre-presentation; the watch sheet doesn't show shipping options.
- Recommend short summary items. Long lists scroll uncomfortably on a 41/45/49mm display.
- Pairing model: payment apps that ship for both iPhone and Watch should treat the iPhone app as the source of truth for setup; Watch app inherits merchant ID / capability via App Group sharing if needed.
PKPaymentButtonLabel cases parallel iOS PKPaymentButtonType (.buy, .setUp, .inStore, .donate, etc.). Verify exact case set against current Apple docs (/watchkit/wkinterfacepaymentbutton) before relying on a specific label.
visionOS
API surface is identical to iOS. Auth modality is Optic ID (or device passcode). No code changes vs iOS — PKPaymentAuthorizationController and PayWithApplePayButton work as-is. SwiftUI is preferred on visionOS.
Capability Detection (Static)
PKPaymentAuthorizationController.canMakePayments() -> Bool
PKPaymentAuthorizationController.canMakePayments(usingNetworks:) -> Bool
PKPaymentAuthorizationController.canMakePayments(usingNetworks:capabilities:) -> BoolcanMakePayments() checks Secure Element presence; doesn't check card provisioning. The two-arg form checks both. The three-arg form additionally filters by capability (e.g. .threeDSecure).
Web-side equivalent (for reference): applePayCapabilities() / ApplePaySession.canMakePayments() — see apple-pay-web-ref.md.
Application-Specific Data
request.applicationData: Data? // hash signed into the token; opaque to AppleUse to bind a payment to your app's order ID without leaking it through the encrypted blob. The data is cryptographically committed in the token; tampering invalidates the signature.
Deprecations to Avoid
| Deprecated | Replacement |
|---|---|
requiredShippingAddressFields | requiredShippingContactFields |
requiredBillingAddressFields | requiredBillingContactFields |
PKAddressField enum | PKContactField |
billingAddress / shippingAddress properties | billingContact / shippingContact |
PKShippingContactEditingMode.enabled | PKShippingContactEditingMode.available |
Country-specific merchant validation URLs (apple-pay-gateway-uk.apple.com, etc.) | apple-pay-gateway.apple.com (production), apple-pay-gateway-cert.apple.com (sandbox) |
canMakePaymentsWithActiveCard() (web) | applePayCapabilities() (WWDC24) |
Resources
MIG: pp.13–17 (request + delegates), pp.18–19 (variants + merchant tokens), pp.20–21 (token format + auth), p.26 (sequence diagram)
WWDC: 2020-10662 (button types, automatic style), 2021-10092 (coupon codes, date ranges), 2022-10041 (multi-merchant, SwiftUI buttons, MCC), 2023-10114 (Apple Pay Later, deferred, disbursements), 2024-10108 (third-party browser, applePayCapabilities, MCC)
Docs: /passkit, /passkit/pkpaymentrequest, /passkit/pkpayment, /passkit/pkpaymenttoken, /passkit/pkpaymentauthorizationcontroller, /passkit/pkpaymentnetwork, /passkit/pkcontactfield, /passkit/pkshippingmethod, /passkit/payment-token-format-reference, /passkit/displaying-a-read-only-pickup-address
Skills: apple-pay (discipline), apple-pay-web-ref (web API surface), wallet-orders (PKPaymentOrderDetails handoff), payments-diag (token / merchant-validation failure modes)
Apple Pay vs In-App Purchase — The Boundary
You MUST resolve this boundary before writing any payment code. Picking the wrong one is a guaranteed App Store rejection — App Review enforces this in both directions.
The Rule (Apple-canonical wording)
From the Apple Pay HIG and the Apple Pay Merchant Integration Guide p.4 (identical wording, both Apple-controlled):
Use Apple Pay in your app to sell physical goods like groceries, clothing, and appliances; for services such as club memberships, hotel reservations, and tickets for events; and for donations. Use In-App Purchase to sell virtual goods, such as premium content for your app, and subscriptions for digital content.
That paragraph is the entire decision in one sentence. Everything below is applying it to specific cases.
The App Review Guidelines codify it in Section 3.1.1 (IAP is required to "unlock features or functionality within your app" — premium content, subscriptions, in-game currency, full-version unlocks) and Section 3.1.3(e) (goods and services that will be consumed outside of the app must use other purchase methods such as Apple Pay or traditional credit card entry, not IAP).
Treat the "consumed inside vs outside the app" framing throughout this skill as a heuristic that matches §3.1.1 + §3.1.3(e) in 95%+ of cases — but when in doubt, the canonical wording is the one cited in §3.1.1 ("unlock features or functionality within your app").
Decision Tree
digraph apple_pay_vs_iap {
"What is the customer buying?" [shape=diamond];
"Consumed inside the app?" [shape=diamond];
"Subscription for what?" [shape=diamond];
"Physical or service?" [shape=diamond];
"Reader-app exemption?" [shape=diamond];
"Use IAP" [shape=box];
"Use Apple Pay" [shape=box];
"Use IAP (3.1.2)" [shape=box];
"Use Apple Pay (services)" [shape=box];
"Use Apple Pay (physical / service)" [shape=box];
"IAP + reader exemption" [shape=box];
"What is the customer buying?" -> "Consumed inside the app?";
"Consumed inside the app?" -> "Reader-app exemption?" [label="digital content"];
"Consumed inside the app?" -> "Use Apple Pay" [label="real-world goods\nor services"];
"Consumed inside the app?" -> "Subscription for what?" [label="subscription"];
"Reader-app exemption?" -> "IAP + reader exemption" [label="yes (3.1.3(a))"];
"Reader-app exemption?" -> "Use IAP" [label="no"];
"Subscription for what?" -> "Use IAP (3.1.2)" [label="digital content\n(streaming, news, SaaS)"];
"Subscription for what?" -> "Physical or service?" [label="real-world\nrecurring"];
"Physical or service?" -> "Use Apple Pay (physical / service)" [label="recurring goods /\ngym / club / hotel"];
}Three short-circuit answers cover ~95% of cases:
1. Selling something the customer touches or experiences in the real world (groceries, hotel, gym membership, event ticket, ride, food delivery, donation) → Apple Pay. 2. Selling something that exists only inside your app or another digital experience (premium features, extra levels, in-game currency, streaming subscription, news subscription, cloud storage) → In-App Purchase. 3. Recurring revenue → use the type of subscription that matches the underlying product. Digital subscription → IAP §3.1.2. Physical/service subscription (e.g. monthly meal kit, gym auto-renew) → Apple Pay (typically PKRecurringPaymentRequest).
Concrete Category Mapping
| Category | Use | App Review reference |
|---|---|---|
| Groceries, clothing, appliances, electronics | Apple Pay | 3.1.3(e) |
| Restaurant order, food delivery, grocery delivery | Apple Pay | 3.1.3(e) |
| Hotel booking, vacation rental, flight, train | Apple Pay | 3.1.3(e) |
| Event ticket (concert, sports, theatre) | Apple Pay | 3.1.3(e) |
| Parking, transit, tolls | Apple Pay | 3.1.3(e) |
| Gym, club, professional association membership | Apple Pay | 3.1.3(e) |
| Donations to nonprofits | Apple Pay (only by approved nonprofits — see "Donations" below) | 3.2.1(vi), 3.2.2(iv), HIG |
| One-to-one real-time person-to-person services (tutoring, telemed, real estate tour, fitness training) | Apple Pay | 3.1.3(d) |
| One-to-few or one-to-many real-time services (group fitness class, group telemed, classroom tutoring) | IAP | 3.1.3(d) |
| Premium app features, ad removal, themes, additional UI | IAP | 3.1.1 |
| In-game currency, extra levels, character unlocks | IAP | 3.1.1 |
| Streaming media subscription (audio, video) | IAP | 3.1.2 |
| News / magazine subscription | IAP | 3.1.2 |
| Cloud storage, SaaS subscription consumed in app | IAP | 3.1.2 |
| Reader-app account management (existing subscriber) | Out-of-app web link allowed under 3.1.3(a) entitlement | 3.1.3(a) |
| Marketplace where you broker payment between unrelated buyer + merchant | Apple Pay, label "Pay [Merchant] (via [You])" | HIG §"Streamlining checkout" |
| Self-checkout in someone else's physical store | Apple Pay, label both businesses on Pay line | HIG §"Streamlining checkout" |
| Free app companion to paid web service (3.1.3(f)) | No payment in-app | 3.1.3(f) |
The "via" label is required by HIG when you are an intermediary — App Review reads the Pay line and rejects flows where the actual merchant is hidden.
Donations
Donations have their own narrow rule that often surprises developers:
- §3.2.1(vi) — Approved nonprofits may fundraise in their own apps or in third-party apps, and must offer Apple Pay support. Apps that broker donations between donors and nonprofits ("nonprofit platforms") must ensure every listed nonprofit has gone through the approval process.
- §3.2.2(iv) — If you are not an approved nonprofit and not otherwise covered by §3.2.1(vi), you may not collect donations in-app at all. Such apps must be free on the App Store and may only collect funds outside the app (Safari, SMS).
There is no "donation IAP" path for non-approved fundraisers. The choice is: be an approved nonprofit (then use Apple Pay), or collect outside the app entirely.
Subscriptions — Pick by Underlying Product
Subscriptions are where developers most often pick the wrong rail.
- Digital content subscription (Spotify, Netflix, news, productivity SaaS) → IAP §3.1.2.
- Physical recurring delivery (meal kit, contact lenses, coffee subscription) → Apple Pay with
PKRecurringPaymentRequest. Seeapple-pay.md. - Service subscription consumed in the real world (gym auto-renew, club dues, parking pass, transit pass) → Apple Pay
PKRecurringPaymentRequest. - Hybrid app that sells both (a fitness app with a digital coaching subscription AND optional physical equipment) → both rails, with each product on the rail that fits it. App Review rejects either side if it's on the wrong rail.
Web — Acceptable Use Guidelines
The rules above govern apps. Apple Pay on the web has a separate set of rules — the Acceptable Use Guidelines for Apple Pay on the Web (see Resources). They prohibit Apple Pay on websites that offer:
- Tobacco, marijuana, or vaping products
- Firearms, weapons, or ammunition
- Illegal drugs or non-legally-prescribed controlled substances
- Items that create consumer safety risks
- Items intended to be used to engage in illegal activities
- Pornography
- Counterfeit or stolen goods
- Personal fundraising or collections of nonprofit donations unless approved by Apple
- Sites that primarily offer or sell drug paraphernalia or sexually-oriented items or services
- Promotion of hate, violence, or intolerance based on race, age, gender, gender identity, ethnicity, religion, or sexual orientation
- Purchase or transfer of currency (including cryptocurrencies) unless approved by Apple
- Staged digital wallets (a second transaction conducted to complete the first, or a substitute merchant of record)
- Fraud, IP / publicity / privacy violations, or content showing Apple in a false or derogatory light
Cross-ref the full list in Resources. AUG enforcement is independent of App Review — Apple can disable Apple Pay on a website at any time without affecting your app status.
The AUG also enforces a parity rule: if any other payment method appears on a page, Apple Pay must appear with at-least-equal prominence on the same page. And if applePayCapabilities() indicates an active card is provisioned, Apple Pay must be the primary displayed option (not necessarily the sole one). Hiding Apple Pay below other options is the most common AUG violation.
PSP-Direct (Raw Card Entry) — When Is It Allowed?
| Surface | Selling physical / service / donation | Selling digital content |
|---|---|---|
| iOS / iPadOS / macOS / visionOS / watchOS app | Not allowed for raw-card-only. App must offer Apple Pay; may additionally offer a PSP-direct PCI form alongside, with Apple Pay shown at least as prominently per HIG ("Offering Apple Pay" — feature Apple Pay at least as prominently on every page or screen that offers payment methods). | Not allowed at all — must use IAP. |
| Mac AppKit app | Same as iOS app rules | Same as iOS app rules |
| Catalyst app | Treated as iOS app for App Review purposes | Same as iOS app rules |
| Website (consumer-facing) | Allowed alongside Apple Pay, with parity. Apple Pay required if any other payment method shown. | N/A — IAP doesn't apply to websites. Use whatever PSP supports. |
| Mac AppKit app sold outside Mac App Store | Outside App Review jurisdiction (notarization only, not App Review) | Outside App Review jurisdiction |
Rule of thumb: if your app ships through the App Store and accepts payment for physical goods, Apple Pay must be present. Adding a PSP-direct card form in addition is fine but doesn't satisfy the requirement.
Common Rejection Patterns
The patterns below are cited from real App Review rejection reasons (failure corpus tracked in payments-diag.md). They map 1:1 to the rule.
| Rejection Reason | Root Cause | Fix |
|---|---|---|
| "Your app uses IAP for selling [physical good or service]" | Wrong rail — physical/service products went through IAP | Switch checkout to Apple Pay; resubmit. See apple-pay.md. |
| "Your app sells [digital content] using a payment method other than IAP" | Wrong rail — digital content went through Apple Pay or PSP | Switch checkout to IAP. See axiom-integration/skills/in-app-purchases.md. |
| "Apple Pay must be at parity with other payment methods" | Web AUG parity violation — Apple Pay shown below or smaller than other options | Promote Apple Pay to at-least-equal prominence on every page that shows payment methods |
| "Custom button mimics or displays Apple Pay branding" | HIG violation — non-API button used "Apple Pay" text or logo | Use the system-provided Apple Pay button API. See apple-pay.md. |
| "Apple Pay marked as 'unavailable' inappropriately" | HIG — button greyed out before user interaction | Always show the button; gracefully handle missing requirements after tap |
| "Marketplace transaction obscures the end merchant" | HIG — Pay line shows only the intermediary's name | Use the "Pay [Merchant] (via [You])" format on the Pay line |
For full diagnostic flow on each rejection, see payments-diag.md — it maps these to specific code fixes.
Anti-Rationalization
| Thought | Reality |
|---|---|
| "IAP fees are higher, we'll route digital content through Apple Pay" | Guaranteed rejection. The rail is determined by the product, not by what you'd prefer to pay Apple. |
| "Apple Pay is 'just credit cards' so it's the safer choice for everything" | Apple Pay is for real-world purchases. Using it for in-app digital content is rejected the same as any non-IAP payment for digital content. |
| "Other apps in my category use IAP for delivery — App Review approved them" | App Review is not bound by past approvals. The rail is defined by 3.1.1 vs 3.1.3(e). Recent enforcement has tightened, not loosened. |
| "We'll mix the two — IAP for premium features, raw card for the physical product" | Allowed, as long as each product is on the correct rail. The bug is using the wrong rail for the product, not having both rails. |
| "Donations should obviously use IAP" | No — §3.2.1(vi) requires approved nonprofits to offer Apple Pay support when fundraising in-app, and §3.2.2(iv) prohibits non-approved apps from collecting donations in-app at all. There is no IAP donation rail to fall back on; the choice is approved-nonprofit-with-Apple-Pay or no-in-app-collection. |
| "We're a registered 501(c)(3), so we can collect donations in-app via Apple Pay" | IRS 501(c)(3) status is not Apple's nonprofit approval. Apple runs its own verification: U.S. nonprofits need a Candid Seal of Transparency; non-U.S. nonprofits apply through Benevity with their Developer Program Team ID (§3.2.1(vi)). A charity must clear that before donations can be collected in-app — even a genuine 501(c)(3) can't until Apple-approved. A commercial app brokering donations to a charity partner must verify the partner has cleared Apple's process, not just confirm their tax status. |
| "We'll just use a PSP-direct card form on web — no Apple Pay needed" | If your site shows any other payment method, Apple Pay is required at parity per AUG. Going PSP-direct without Apple Pay is what gets the site AUG-flagged. |
| "Reader app exemption covers our streaming app, so we don't need IAP" | §3.1.3(a) lets Reader apps offer account creation for free tiers and account management for existing customers, plus an informational external link via the External Link Account Entitlement. It does not authorize any in-app payment rail other than IAP. Reader apps that monetize in-app must use IAP; otherwise, redirect to web for payment. |
| "Our subscription is 'physical' because we mail a sticker once a year" | App Review evaluates the primary value of the subscription. A digital service with token physical delivery is still digital. |
| "Crypto purchases work on web with Apple Pay because it's not 'in the app'" | Web AUG explicitly prohibits currency / cryptocurrency without Apple approval. Web jurisdiction cuts both ways. |
| "We're a marketplace, the seller's business name doesn't need to appear on Pay" | HIG explicitly requires "Pay [End_Merchant_Business_Name (via Your_Business_Name)]" when you're an intermediary. Hiding the end merchant is a rejection trigger. |
Red Flags — STOP and Re-Check
- You picked IAP and the product is consumable in the real world (food, stay, ride, ticket)
- You picked Apple Pay and the product is consumed only inside your app
- You're a marketplace and the Pay line shows only your business
- You're on the web and Apple Pay sits below other payment methods, or is smaller, or is hidden under a "more options" disclosure
- You're using a PSP-direct card form in your iOS app for physical goods without Apple Pay alongside it
- You added a custom button that says "Apple Pay" or shows the Apple Pay logo
Each of these is a known rejection trigger. Resolve before submitting.
Boundary Summary
| You sell… | Surface | Use |
|---|---|---|
| Physical goods | App | Apple Pay |
| Physical goods | Web | Apple Pay (with PSP-direct allowed alongside, at parity) |
| Real-world services | App or Web | Apple Pay |
| Donations (approved nonprofits only; non-approved apps may not collect in-app) | App or Web | Apple Pay (3.2.1(vi)) |
| Premium app content / in-game items | App | IAP |
| Digital subscription consumed in app | App | IAP §3.1.2 |
| Reader-app subscription (existing customer account management) | App | IAP for in-app monetization; account-management UI + External Link Entitlement allowed under 3.1.3(a) for existing customers / free tiers |
| One-to-one real-time person-to-person service | App | Apple Pay (3.1.3(d)) |
| One-to-few or one-to-many real-time service | App | IAP (3.1.3(d) — exemption is 1:1 only) |
| Hardware-tied feature unlock | App | May skip IAP under 3.1.4 narrow exception; otherwise IAP |
Once the boundary is settled, see apple-pay.md (native), apple-pay-web.md (web), or axiom-integration/skills/in-app-purchases.md (digital).
Resources
App Review: 3.1.1 (IAP requirement), 3.1.2 (Subscriptions), 3.1.3 (Other Purchase Methods, esp. (a) Reader, (d) P2P, (e) Goods Outside the App, (f) Free Stand-alone), 3.1.4 (Hardware-Specific), 3.2 (Other Business Models), 4.9 (Apple Pay)
MIG: p.4 ("Apple Pay vs In-App Purchases" guideline box)
HIG: /design/human-interface-guidelines/apple-pay (the Tip box)
Docs: /apple-pay/acceptable-use-guidelines-for-websites
Skills: apple-pay (native discipline), apple-pay-web (web discipline), payments-diag (rejection root causes), axiom-integration/in-app-purchases (digital content), axiom-shipping/app-store-diag (rejection workflow)
Apple Pay on the Web — API Reference
API surface for both Apple Pay JS and the W3C Payment Request API form. For the discipline (when, how, why), see apple-pay-web.md. For native, see apple-pay-ref.md.
API Choice
| API | Identifier | Browsers |
|---|---|---|
| Apple Pay JS | ApplePaySession global | Safari (Mac / iOS / iPadOS / visionOS) |
| Payment Request API | PaymentRequest global; method identifier https://apple.com/apple-pay | Cross-browser (Safari + third-party on iOS 18+ via JS SDK 1.2.0+) |
Both produce the same encrypted token; differences are in event-handler shape and request structure. You can ship either, both, or pick by navigator.userAgent + capability detection.
Apple Pay JS — ApplePaySession
Constructor
const session = new ApplePaySession(version, paymentRequest);version is the Apple Pay JS API version. WWDC24 introduced version 14; later versions may exist — check /applepayontheweb/apple-pay-on-the-web-version-history for current. Pin to the lowest version you actually need; new versions add features but require capability fallbacks for older clients.
Session lifecycle methods
| Method | Purpose |
|---|---|
session.begin() | Display the payment sheet. Triggers onvalidatemerchant. |
session.abort() | Dismiss the sheet (e.g. user navigated away from checkout). |
session.completeMerchantValidation(sessionObject) | Pass the opaque session JSON returned from your server to advance past validation. |
session.completePayment(result) | Resolve the authorization with success or error status (see Status Codes). |
session.completePaymentMethodSelection(update) | Resolve onpaymentmethodselected with updated total + line items. |
session.completeShippingContactSelection(update) | Resolve onshippingcontactselected. |
session.completeShippingMethodSelection(update) | Resolve onshippingmethodselected. |
session.completeCouponCodeChange(update) | Resolve oncouponcodechanged. |
Event handlers
| Handler | Signature | Trigger |
|---|---|---|
onvalidatemerchant | (event: ApplePayValidateMerchantEvent) => void | Sheet appeared; event.validationURL ready for server-side merchant session request. |
onpaymentauthorized | (event: ApplePayPaymentAuthorizedEvent) => void | Customer authenticated; event.payment carries token + contact data. Call completePayment(). |
onpaymentmethodselected | (event) => void | Customer switched cards. Recalculate any card-specific surcharge / fee. |
onshippingcontactselected | (event) => void | Customer changed shipping address (redacted form). |
onshippingmethodselected | (event) => void | Customer chose a shipping option. |
oncouponcodechanged | (event: { couponCode }) => void | Customer entered / cleared coupon. |
oncancel | (event) => void | Sheet dismissed without authorization. |
Static methods
| Static | Returns | Purpose |
|---|---|---|
ApplePaySession.canMakePayments() | Boolean | Device hardware supports Apple Pay |
ApplePaySession.canMakePaymentsWithActiveCard(merchantId) | Promise<Boolean> | Deprecated WWDC24 — use applePayCapabilities() |
ApplePaySession.openPaymentSetup(merchantId) | Promise<Boolean> | Open Wallet's setup flow |
ApplePaySession.supportsVersion(version) | Boolean | Capability check for a specific JS API version |
Top-level applePayCapabilities() (WWDC24)
const result = await applePayCapabilities("merchant.com.example.shop");
// result.paymentCredentialStatus: one of
// "paymentCredentialsAvailable"
// "paymentCredentialsUnavailable"
// "paymentCredentialStatusUnknown"
// "applePayUnsupported"Replaces canMakePaymentsWithActiveCard. Drives the show-primary / show-secondary / hide UX decisions per HIG + AUG. Returns from a global, not a session method — call before constructing the session.
ApplePayPaymentRequest
Object passed to new ApplePaySession(version, request):
| Property | Type | Notes |
|---|---|---|
countryCode | String | ISO 3166 2-letter |
currencyCode | String | ISO 4217 3-letter |
merchantCapabilities | String[] | "supports3DS", "supportsCredit", "supportsDebit", "supportsEMV", "supportsInstantFundsOut" (WWDC24) |
supportedNetworks | String[] | "visa", "masterCard", "amex", "discover", "chinaUnionPay", "interac", "jcb", "mada", "electron", "maestro", "vPay", "eftpos" |
total | ApplePayLineItem | Final amount; label is your customer-facing business name |
lineItems | ApplePayLineItem[] | Line items shown above the total |
requiredBillingContactFields | String[] | "postalAddress", "name", "phoneNumber", "emailAddress", "phoneticName" |
requiredShippingContactFields | String[] | Same set |
billingContact, shippingContact | ApplePayPaymentContact | Pre-populate known data |
shippingMethods | ApplePayShippingMethod[] | Each carries label, detail, amount, optional dateComponentsRange |
shippingType | String | "shipping", "delivery", "storePickup", "servicePickup" |
applicationData | String | Base64-encoded; SHA-256 binds into token's header.applicationData |
supportsCouponCode | Boolean | Show coupon-code field on sheet |
couponCode | String | Pre-populate field |
recurringPaymentRequest | ApplePayRecurringPaymentRequest | Subscription variant |
automaticReloadPaymentRequest | ApplePayAutomaticReloadPaymentRequest | Stored-balance reload variant |
deferredPaymentRequest | ApplePayDeferredPaymentRequest | Pay-later-at-delivery variant |
multiTokenContexts | ApplePayPaymentTokenContext[] | Multi-merchant in one sheet |
ApplePayLineItem:
{
label: "Subtotal",
amount: "89.99", // string — never JS Number
type: "final" // or "pending" for unknown amounts
}Payment Request API form
const methodData = [{
supportedMethods: "https://apple.com/apple-pay",
data: {
version: 14,
merchantIdentifier: "merchant.com.example.shop",
merchantCapabilities: ["supports3DS"],
supportedNetworks: ["visa", "masterCard", "amex", "discover"],
countryCode: "US"
}
}];
const details = {
displayItems: [{ label: "Subtotal", amount: { currency: "USD", value: "89.99" } }],
total: { label: "Example Shop", amount: { currency: "USD", value: "94.99" } },
shippingOptions: [...],
modifiers: [...]
};
const options = {
requestPayerName: true,
requestPayerEmail: false,
requestPayerPhone: false,
requestShipping: true,
shippingType: "shipping" // or "delivery", "pickup"
};
const request = new PaymentRequest(methodData, details, options);PaymentRequest method | Purpose |
|---|---|
request.show() | Display the sheet; returns Promise<PaymentResponse> |
request.abort() | Programmatic dismissal |
request.canMakePayment() | Promise<Boolean> — capability check |
Event on PaymentRequest | Apple Pay JS analog |
|---|---|
merchantvalidation | onvalidatemerchant |
shippingaddresschange | onshippingcontactselected |
shippingoptionchange | onshippingmethodselected |
paymentmethodchange | onpaymentmethodselected |
couponcodechange | oncouponcodechanged |
PaymentResponse.complete(result) resolves the flow. result is "success", "fail", or "unknown".
Modifiers (Payment Request API)
details.modifiers carries Apple-Pay-specific overrides on a per-method basis. Use for:
| Modifier shape | Purpose |
|---|---|
recurringPaymentRequest | Subscription |
automaticReloadPaymentRequest | Auto top-up |
deferredPaymentRequest | Pay later |
multiTokenContexts | Multi-merchant |
additionalLineItems: [{ type: "disbursement", ... }] | Web disbursement (WWDC24) |
Each variant requires the matching capability declared in methodData[].data.merchantCapabilities. Disbursements specifically require "supportsInstantFundsOut".
ApplePayPayment (Authorization Result)
Delivered as event.payment on onpaymentauthorized:
{
token: ApplePayPaymentToken,
billingContact?: ApplePayPaymentContact,
shippingContact?: ApplePayPaymentContact
}ApplePayPaymentToken
{
paymentMethod: {
displayName: "Visa 1234",
network: "Visa",
type: "credit" // "credit" | "debit" | "prepaid" | "store"
},
transactionIdentifier: "<hex>",
paymentData: { ... } // The encrypted payload — same structure as native PKPaymentToken.paymentData
}paymentData shape is identical to the native form (version, data, signature, header). See apple-pay-ref.md § "Payment Token Format" for the EC_v1 / RSA_v1 details.
ApplePayPaymentContact
{
phoneNumber?: string,
emailAddress?: string,
givenName?: string,
familyName?: string,
phoneticGivenName?: string,
phoneticFamilyName?: string,
addressLines?: string[],
subLocality?: string,
locality?: string,
postalCode?: string,
subAdministrativeArea?: string,
administrativeArea?: string,
country?: string,
countryCode?: string
}Pre-auth (returned in onshippingcontactselected) is redacted — only country, countryCode, administrativeArea, locality, postalCode. Post-auth (returned in onpaymentauthorized) is the full form.
Apple Pay Errors
new ApplePayError(code, contactField?, message?)code | When |
|---|---|
"shippingContactInvalid" | Bad shipping address field; pass field name as contactField |
"billingContactInvalid" | Bad billing address field |
"addressUnserviceable" | Address valid, you don't ship there |
"couponCodeInvalid" | Coupon malformed |
"couponCodeExpired" | Coupon past expiry |
"unknown" | Generic |
Contact-field names: "postalAddress", "name", "phoneNumber", "emailAddress", "phoneticName", plus address sub-fields "locality", "postalCode", "administrativeArea", "country", "countryCode", "addressLines".
Errors flow back via the errors property on each completion update object, or via completePayment({ status, errors }) for final auth.
Status Codes
ApplePaySession.completePayment({ status, errors }) accepts:
| Code | Constant | Meaning |
|---|---|---|
0 | STATUS_SUCCESS | Authorization succeeded |
1 | STATUS_FAILURE | Authorization failed |
2 | STATUS_INVALID_BILLING_POSTAL_ADDRESS | Invalid billing postal address |
3 | STATUS_INVALID_SHIPPING_POSTAL_ADDRESS | Invalid shipping postal address |
4 | STATUS_INVALID_SHIPPING_CONTACT | Invalid shipping name / email / phone |
5 | STATUS_PIN_REQUIRED | (legacy) |
6 | STATUS_PIN_INCORRECT | (legacy) |
7 | STATUS_PIN_LOCKOUT | (legacy) |
Prefer the errors: [ApplePayError, ...] shape over numeric status codes — it surfaces field-specific feedback to the user, which the numeric codes don't.
Apple Pay Later Merchandising Widget (WWDC23)
A pre-checkout banner that informs eligible US customers Apple Pay Later is available. Renders as a custom element from the JS SDK:
<apple-pay-later-merchandising
amount="89.99"
currency="USD">
</apple-pay-later-merchandising>Place on product / cart pages — before the customer reaches checkout. The widget adapts to amount thresholds and handles localization automatically. Common attributes shown; see /applepayontheweb/adding-an-apple-pay-later-visual-merchandising-widget for full attribute list (presentation, locale handling, etc.).
Track with Apple Wallet Button (WWDC23)
For order tracking handoff to Wallet (see wallet-orders.md):
<apple-pay-wallet-button
type="track"
locale="en-US">
</apple-pay-wallet-button>Common attributes shown; see /applepayontheweb/adding-a-track-with-apple-wallet-button for the full type set and customization options. Pairs with the order package URL flow described in wallet-orders.md.
Web Sequence Diagrams
In-Safari direct flow (MIG p.27)
[1] User taps Apple Pay button
[2] JS creates ApplePaySession, calls .begin()
[3] Sheet appears
[4] Browser fires onvalidatemerchant with validationURL
[5] JS POSTs validationURL to your server
[6] Server POSTs to apple-pay-gateway.apple.com (two-way TLS, merchant identity cert)
[7] Apple Pay returns opaque session JSON
[8] Server returns session JSON to browser
[9] JS calls session.completeMerchantValidation(sessionJson)
[10] User interacts with sheet (shipping / coupon / method changes → events)
[11] User authenticates (Face/Touch/Optic ID on linked iPhone or Mac TouchID)
[12] Browser fires onpaymentauthorized with payment.token
[13] JS POSTs token to your server
[14] Server forwards token to PSP (encrypted blob OR self-decrypted)
[15] PSP authorizes via acquirer / network / issuer
[16] Server returns to browser
[17] JS calls session.completePayment({ status: STATUS_SUCCESS })
[18] Sheet dismisses with success animationPSP-hosted page flow (MIG p.28)
When checkout is hosted on the PSP's domain (Stripe Checkout, Adyen Drop-in, etc.):
[1] Your site embeds PSP iframe / redirects to PSP-hosted page
[2] PSP page renders Apple Pay button (may be CSS or JS SDK)
[3] Apple Pay flow runs against PSP's merchant ID + cert (not yours)
[4] PSP receives token, decrypts, processes
[5] PSP returns to your site with order ID / statusIn this model you don't directly handle merchant validation or token decryption — the PSP does. Your integration concern shifts to PSP-specific webhook handling and idempotency.
Maintaining Your Environment
Three things expire on the web side:
| What | Expiry | Renewal |
|---|---|---|
| Payment Processing Certificate | 25 months | Create-but-don't-activate workflow (see apple-pay.md § "Cert renewal") |
| Merchant Identity Certificate | Documented expiry | Re-create + redeploy to server |
| Domain verification | Periodic re-verification | Re-download apple-developer-merchantid-domain-association.txt and re-verify |
The Merchant ID itself never expires.
Capability Detection Decision Tree
digraph capability {
"applePayCapabilities() result?" [shape=diamond];
"Show primary / pre-selected" [shape=box];
"Show, your choice of order" [shape=box];
"Hide button" [shape=box];
"applePayCapabilities() result?" -> "Show primary / pre-selected" [label="paymentCredentialsAvailable"];
"applePayCapabilities() result?" -> "Show, your choice of order" [label="paymentCredentialStatusUnknown"];
"applePayCapabilities() result?" -> "Hide button" [label="paymentCredentialsUnavailable"];
"applePayCapabilities() result?" -> "Hide button" [label="applePayUnsupported"];
}Combine with ApplePaySession.canMakePayments() (sync, hardware-only) for fast initial gating before the async capabilities call resolves.
Resources
MIG: pp.10–14 (config + validation), p.27 (in-Safari sequence), p.28 (PSP-hosted sequence)
WWDC: 2021-10092 (JS SDK button, coupon codes, date ranges), 2022-10041 (multi-merchant, automatic-reload), 2023-10114 (Apple Pay Later merchandising widget, deferred, disbursements), 2024-10108 (third-party browser, JS SDK 1.2.0, applePayCapabilities, web disbursements, MCC)
Tech Talks: 111381 (Get started with Apple Pay on the Web)
Docs: /applepayontheweb/applepaysession, /applepayontheweb/apple-pay-js-api, /applepayontheweb/payment-request-api, /applepayontheweb/applepayvalidatemerchantevent, /applepayontheweb/applepaypaymentauthorizedevent, /applepayontheweb/checking-for-apple-pay-availability, /applepayontheweb/creating-an-apple-pay-session, /applepayontheweb/providing-merchant-validation, /applepayontheweb/requesting-an-apple-pay-payment-session, /applepayontheweb/apple-pay-status-codes, /applepayontheweb/displaying-apple-pay-buttons-using-javascript, /applepayontheweb/loading-the-latest-version-of-apple-pay-js, /applepayontheweb/adding-an-apple-pay-later-visual-merchandising-widget, /applepayontheweb/adding-a-track-with-apple-wallet-button, /applepayontheweb/apple-pay-on-the-web-version-history
Skills: apple-pay-web (discipline), apple-pay-ref (native API parallels), wallet-orders (order tracking handoff), payments-diag (cert / validation failures)
Apple Pay on the Web
You MUST use this skill for ANY Apple Pay integration on a website. The shape of the integration is materially different from native — additional certificate, domain verification, server-side merchant validation, and (since iOS 18) third-party browser support. For native apps see apple-pay.md. For the IAP/Apple Pay boundary (which doesn't apply to web — IAP doesn't exist on the web) see apple-pay-vs-iap.md.
Why Web Setup Is Different
Native and web share the same merchant ID and Payment Processing Certificate, but web adds three things that don't exist in native:
| Web-only requirement | Why |
|---|---|
| Merchant Identity Certificate (RSA 2048; separate from Payment Processing Certificate) | Authenticates server sessions with Apple Pay servers via two-way TLS. Native doesn't need it because the device handles trust. |
| Domain registration + verification | Apple ties the Apple Pay button to specific domains. Every TLD and subdomain that displays the button must be registered and verified. |
| Server-side merchant validation step | Every checkout begins with your server requesting a one-time, 5-minute, single-use opaque session object from Apple — using the Merchant Identity Certificate. |
A native app skips all three. If you've shipped native Apple Pay before, expect web to take 2–3× the setup time on the certificate / domain side.
Pre-Flight Web Checklist (MIG p.10)
Run before writing any JavaScript. Skipping any item produces silent merchant-validation failures that surface only at the first checkout.
| Step | Owner | What |
|---|---|---|
| HTTPS + TLS 1.2+ on every page that shows the button | Server admin | Apple's servers will not validate sessions for non-HTTPS or weak-TLS sites. |
| Domain registered AND verified | Account admin | Add domain in Certificates, IDs & Profiles → Merchant IDs → your ID → Merchant Domains → Add. Download apple-developer-merchantid-domain-association.txt, place at /.well-known/ on the apex of the domain. Domain cannot be behind a redirect or proxy — must be directly reachable by Apple's IPs. Re-verify when association file expires. |
| Merchant Identity Certificate created and exported | Developer | RSA 2048-bit key in Keychain. Export as .p12 with password. Split into ApplePay.crt.pem and ApplePay.key.pem via openssl pkcs12 for server use (commands below). |
Cert tested via curl to apple-pay-gateway-cert.apple.com | Developer | One-shot validation that your cert + domain + merchant ID work. Test before any frontend work. |
| Allow Apple's IPs in firewall | Server admin | Apple publishes the IP list at /applepayontheweb/setting-up-your-server. Domain verification fails silently if Apple can't reach your .well-known/ file. |
Exporting the Merchant Identity Certificate (MIG p.10–11)
# Export from Keychain as ApplePayMerchantID_and_privatekey.p12 with a password,
# then split:
openssl pkcs12 -in ApplePayMerchantID_and_privatekey.p12 \
-out ApplePay.crt.pem -nokeys
openssl pkcs12 -in ApplePayMerchantID_and_privatekey.p12 \
-out ApplePay.key.pem -nocertsTest cert + domain via curl (MIG p.11)
curl --location 'https://apple-pay-gateway-cert.apple.com/paymentservices/paymentSession' \
--header 'Content-Type: text/plain' \
--data '{
"merchantIdentifier": "merchant.com.example.shop",
"displayName": "Example Shop",
"initiative": "web",
"initiativeContext": "shop.example.com"
}' \
--cert ApplePay.crt.pem \
--key ApplePay.key.pemA successful response is an opaque session JSON blob. If you get nothing or a TLS error, the cert + domain pair is broken — don't proceed to frontend. Common failure modes are documented in payments-diag.md.
"It is important that this object is not inspected, parsed or modified in any way. Apple may update the contents of this object from time to time with changes and enhancements." — MIG p.12
Pass it through verbatim to completeMerchantValidation().
Choose the Right API
Two JavaScript APIs accept Apple Pay on the web. Both are supported; pick by browser scope:
| API | Supported browsers | When to use |
|---|---|---|
Apple Pay JS (ApplePaySession) | Safari (Mac, iOS, iPadOS, visionOS) | Established, full feature surface, Apple-controlled. Pick this for Safari-only flows or when you want maximum feature parity with new Apple Pay capabilities at launch. |
W3C Payment Request API (PaymentRequest) | Cross-browser, including third-party browsers on iOS 18+ via the Apple Pay JS SDK | Pick for cross-browser support. Required for third-party browser scan-to-pay (WWDC24). |
You can support both — many large merchants do, choosing dynamically based on the browser. Apple's recommended migration path is toward Payment Request API for portability, but Apple Pay JS remains supported.
Display the Button (WWDC24, MIG p.13)
There are two ways to render the Apple Pay button on the web. One of them works on third-party browsers; one doesn't.
| Method | Works on Safari? | Works on third-party browsers (iOS 18+)? | Recommended? |
|---|---|---|---|
JavaScript SDK button (<apple-pay-button> custom element) | Yes | Yes | Yes — required for non-Safari support |
CSS-implemented button (background-image with -webkit-appearance) | Yes | No | Legacy only |
Load the SDK in <head>:
<script crossorigin
src="https://applepay.cdn-apple.com/jsapi/v1.latest/apple-pay-sdk.js">
</script>v1.latest resolves to the current SDK. Specific-version pinning paths are documented at /applepayontheweb/loading-the-latest-version-of-apple-pay-js — verify the exact form before pinning. SDK 1.2.0 or newer is required for third-party browser scan-to-pay.
Render the button:
<apple-pay-button buttonstyle="black" type="buy" locale="en-US">
</apple-pay-button>The SDK custom element handles localization, sizing, and Light/Dark adaptation automatically. Don't try to style it with custom CSS beyond the documented attributes.
Capability Detection (WWDC24)
Three APIs, two of them deprecated. Pick correctly.
| API | Returns | Status |
|---|---|---|
ApplePaySession.canMakePayments() | Boolean — device hardware supports Apple Pay | Current |
ApplePaySession.canMakePaymentsWithActiveCard(merchantId) | Promise<Boolean> — device has a card provisioned | Deprecated (WWDC24) |
applePayCapabilities(merchantId) | Promise<{ paymentCredentialStatus }> | Current — replaces canMakePaymentsWithActiveCard |
paymentCredentialStatus values:
| Value | Meaning | UI guidance |
|---|---|---|
paymentCredentialsAvailable | Active card provisioned | Show Apple Pay first / pre-selected (HIG + AUG primacy rule) |
paymentCredentialsUnavailable | No card; Apple Pay not usable | Hide the button |
paymentCredentialStatusUnknown | Can't determine (e.g. third-party browser before scan-to-pay) | Show the button; ordering is your choice |
applePayUnsupported | Browser / device fundamentally can't | Hide the button |
const result = await applePayCapabilities("merchant.com.example.shop");
switch (result.paymentCredentialStatus) {
case "paymentCredentialsAvailable": showPrimary(); break;
case "paymentCredentialStatusUnknown": showSecondary(); break;
case "paymentCredentialsUnavailable":
case "applePayUnsupported": hide(); break;
}The HIG / AUG rule: ifapplePayCapabilities()returnspaymentCredentialsAvailable, Apple Pay must be the primary displayed payment option. Not necessarily the only one — but pre-selected, larger, or otherwise visually first.
Merchant Validation Flow (MIG p.14, providing-merchant-validation, tech talk 111381)
The single most security-critical part of web integration. Get it wrong and either you leak your merchant identity cert or your sessions don't authenticate.
Browser Your server Apple Pay servers
│ │ │
│── checkout button click ──▶│ │
│ │ │
│◀── ApplePaySession.begin() ┤ │
│ (sheet appears) │ │
│ │ │
│── onmerchantvalidation ───▶│ │
│ (validationURL) │ │
│ │ │
│ │── POST /paymentSession ────────▶│
│ │ (merchant identity cert, │
│ │ two-way TLS) │
│ │ │
│ │◀── opaque session JSON ───────┤
│ │ │
│◀── completeMerchantValidation(session) │
│ │ │Implementation contract
1. Browser registers onmerchantvalidation (Apple Pay JS) or listens for the merchantvalidation event (Payment Request API). The handler receives a validationURL — always use the URL the event provides; it can vary. 2. Browser POSTs the validationURL to your own server. 3. Server POSTs to the Apple Pay endpoint using the Merchant Identity Certificate for two-way TLS. Never call this endpoint from the browser. 4. Apple Pay returns an opaque JSON session object. 5. Server returns it verbatim to the browser. 6. Browser calls session.completeMerchantValidation(sessionObject) (Apple Pay JS) or resolves the validation event (Payment Request API).
Server-side request shape
POST https://apple-pay-gateway.apple.com/paymentservices/paymentSession
Content-Type: text/plain
{
"merchantIdentifier": "merchant.com.example.shop",
"displayName": "Example Shop",
"initiative": "web",
"initiativeContext": "shop.example.com"
}Two-way TLS using the merchant identity cert + private key. Apple's gateway accepts JSON content with either Content-Type: text/plain (per the MIG curl example, p.11) or application/json — they both work. Use `validationURL` from the event, not a hardcoded URL — Apple may route validation through different paths.
Critical rules (any one of these will break validation)
- Server-side only. Calling
paymentSessionfrom the browser leaks the certificate. App Review and Apple Pay servers both reject this design. - Allowlist the `validationURL` host before you POST to it. Your endpoint receives
validationURLfrom the browser and then POSTs your merchant identity cert to it over two-way TLS. An unvalidatedvalidationURLis an SSRF primitive — a malicious client sends its own URL and your server proxies its client cert to an attacker-chosen host. Match the parsed host for exact equality against Apple's known validation hosts —apple-pay-gateway.apple.com,apple-pay-gateway-cert.apple.com(sandbox),cn-apple-pay-gateway.apple.com(China) — and requirehttps. Do not suffix-match onapple.com:apple-pay-gateway.apple.com.evil.comends inapple.com, and a bareendsWith("apple.com")also matchesevilapple.com. Failing closed on an unknown host (you add it when Apple adds a regional pod) is safer than failing open to SSRF. - Don't inspect or modify the session object. It's opaque. Apple updates the schema without notice. Pass it through verbatim.
- Single-use. Each session object is good for one
completeMerchantValidation()call. - 5-minute expiry. Sessions older than 5 minutes are dead.
- Sandbox vs production endpoint. Sandbox uses
apple-pay-gateway-cert.apple.com; production usesapple-pay-gateway.apple.com. Don't ship sandbox URLs to production.
Payment Request Construction (MIG pp.13–17)
Apple Pay JS form:
const session = new ApplePaySession(14, {
countryCode: "US",
currencyCode: "USD",
merchantCapabilities: ["supports3DS"],
supportedNetworks: ["visa", "masterCard", "amex", "discover"],
total: {
label: "Example Shop",
type: "final",
amount: "94.99"
},
lineItems: [
{ label: "Subtotal", amount: "89.99" },
{ label: "Shipping", amount: "5.00" }
],
requiredShippingContactFields: ["postalAddress", "name"],
requiredBillingContactFields: ["postalAddress"]
});Payment Request API form:
const supportedNetworks = ["visa", "masterCard", "amex", "discover"];
const methodData = [{
supportedMethods: "https://apple.com/apple-pay",
data: {
version: 14,
merchantIdentifier: "merchant.com.example.shop",
merchantCapabilities: ["supports3DS"],
supportedNetworks,
countryCode: "US"
}
}];
const details = {
displayItems: [
{ label: "Subtotal", amount: { currency: "USD", value: "89.99" } },
{ label: "Shipping", amount: { currency: "USD", value: "5.00" } }
],
total: { label: "Example Shop", amount: { currency: "USD", value: "94.99" } }
};
const options = { requestPayerName: true, requestShipping: true };
const request = new PaymentRequest(methodData, details, options);The currency-amount form differs between the two APIs (string vs nested {currency, value} object). Decimal-precise strings throughout — never JS Number literals; floating-point math will silently corrupt totals.
The Charged Amount Must Be Server-Authoritative
The total and lineItems you pass to ApplePaySession / PaymentRequest are display only — they render the sheet and nothing more. They are fully client-controlled: a hostile user opens devtools, calls your checkout with amount: "0.01", authorizes a genuine Apple Pay payment for a penny, and the encrypted token that comes back is valid. Decimal-precise strings (above) stop floating-point corruption; they do not stop tampering. These are two different problems.
| Discipline | Why |
|---|---|
Recompute the captured amount server-side from the cart's persisted line items (product IDs + quantities in your DB), inside the same endpoint that processes the onpaymentauthorized token. Never capture the number the client sent. | The sheet proves who paid and that a card authorized — it does not prove how much you should charge. That figure is yours to compute. |
Bind the cart to the authenticated session. A cartId the client passes must belong to that user. | Otherwise one user can pay for / process another user's cart. |
Idempotency key on capture (e.g. cartId + token), enforced at your endpoint and toward the PSP. | A retried onpaymentauthorized — network blip, double-tap — must not double-charge. |
The merchant-validation discipline above protects your identity; this protects your revenue. Both are server-side concerns; neither is optional.
Event Handlers
Respond promptly — the system aborts the transaction if your handler stalls.
| Apple Pay JS event | Payment Request API equivalent | Trigger |
|---|---|---|
onshippingaddresschange | shippingaddresschange event | Shipping address picked / changed |
onshippingmethodchange | shippingoptionchange event | Shipping method picked |
onpaymentmethodselected | paymentmethodchange event | Payment card switched |
oncouponcodechanged | couponcodechange event | Coupon code entered |
onpaymentauthorized | paymentResponse from request.show() | Customer authenticated |
oncancel | promise rejects with AbortError | Sheet dismissed |
Coupon code (WWDC21)
const session = new ApplePaySession(14, {
// ...
supportsCouponCode: true,
couponCode: "SUMMER10" // optional — pre-fills the field
});
session.oncouponcodechanged = (event) => {
if (codeIsValid(event.couponCode)) {
session.completeCouponCodeChange({
newTotal: { label: "Example Shop", type: "final", amount: "84.99" },
newLineItems: [...]
});
} else {
session.completeCouponCodeChange({
errors: [new ApplePayError("couponCodeInvalid")]
});
}
};
// newLineItems: replace with your refreshed line items arrayPre-auth (redacted) vs post-auth (full) shipping contact
Same rule as native: pre-auth address has no street / phone / name. Use it for shipping option calculation. Full address arrives in onpaymentauthorized after the customer authenticates.
Variants — Recurring / Automatic Reload / Deferred / Disbursement (MIG p.18, WWDC22)
Pass as paymentRequestModifier (Payment Request API) or as a sub-object on ApplePayPaymentRequest (Apple Pay JS):
| Scenario | Modifier | Use when |
|---|---|---|
| Subscription at fixed interval | recurringPaymentRequest | Streaming, gym, club dues |
| Auto top-up at threshold | automaticReloadPaymentRequest | Stored balance / transit reload |
| Pay later at delivery | deferredPaymentRequest | Hotel, pre-order, car rental |
| Pay out to the user | Disbursement modifier (WWDC24) | Funds transfer from your platform |
Each variant carries a corresponding summary item type and a tokenNotificationURL for merchant-token lifecycle events (UNLINK / EXPIRED / etc.). Without tokenNotificationURL you can't be informed when a customer un-provisions a card from Wallet.
Web Disbursements (WWDC24)
Disbursements existed in native (iOS 17, see apple-pay.md); WWDC24 extended them to the web via Payment Request API. Pattern:
const methodData = [{
supportedMethods: "https://apple.com/apple-pay",
data: {
version: 14,
merchantIdentifier: "merchant.com.example",
countryCode: "US",
supportedNetworks,
merchantCapabilities: ["supports3DS", "supportsInstantFundsOut"]
}
}];
const details = {
total: { label: "Example Cashout", amount: { currency: "USD", value: "200.00" } },
additionalLineItems: [{
type: "disbursement",
label: "Withdrawal",
amount: { currency: "USD", value: "200.00" }
}]
};
const options = { requestShipping: false }; // disbursements don't shipDiscipline:
- `requestShipping: false` — disbursements have no shipping concept. Setting
trueconfuses the sheet. - `supportsInstantFundsOut` capability declares your processor supports the rail.
- `additionalLineItems` with `type: "disbursement"` declares the cashout amount.
- The flow ends with the funds appearing on the user's card linked in Wallet.
Acceptable Use Guidelines
The web AUG governs what you can sell with Apple Pay on the web — independent of App Review (which doesn't apply on the web). Disabling Apple Pay on a website is at Apple's discretion and is enforced separately.
The full AUG cross-reference and prohibited-categories list lives in apple-pay-vs-iap.md § "Web — Acceptable Use Guidelines". Two AUG rules that hit web integrations specifically:
Parity rule
If any other payment method appears on a page, Apple Pay must appear with at-least-equal prominence on the same page.
Most common AUG violations: Apple Pay tucked under a "more options" disclosure, sized smaller than other buttons, or shown on the cart page but not the express checkout panel.
Primary-option rule (when active card detected)
IfapplePayCapabilities()returnspaymentCredentialsAvailable(active card provisioned in Wallet), Apple Pay must be pre-selected as the primary payment option.
A neutral chooser ("which payment method?") with all options equal is a violation when an active card is detected.
Third-Party Browser Scan-to-Pay (WWDC24)
iOS 18 enabled Apple Pay on Chrome / Edge / Firefox / Brave on iOS via QR-scan handoff to the user's iPhone Wallet. Requirements:
- JavaScript SDK button (the custom element) — CSS buttons don't render the QR flow
- JS SDK 1.2.0+
- Standard Apple Pay JS or Payment Request API integration on your server side — no special server logic for scan-to-pay
If you already use the SDK button at version 1.2.0+, you get scan-to-pay for free. The user sees a QR code in the third-party browser; their iPhone Wallet authenticates and returns the encrypted token over the existing flow.
Testing (MIG p.22)
| Surface | Use |
|---|---|
| Apple Pay sandbox tester accounts | Sign-in flow same as native; sandbox FPANs work in Safari / Chrome / Edge / Firefox |
applepaydemo.apple.com | Use this. Apple's interactive demo shows correct flow patterns. Treat it as a tool, not just a doc — it's the fastest way to verify your environment works end-to-end. |
| Curl test (MIG p.11) | Validates merchant identity cert + domain before you wire frontend |
| Real cards in production | Sandbox is for flow validation; only production proves end-to-end works |
Anti-Patterns
| Anti-Pattern | Why it fails | Fix |
|---|---|---|
Calling paymentSession from the browser | Leaks merchant identity cert; rejected by Apple Pay servers and by App Review (where applicable) | Server-side only, two-way TLS |
| CSS-implemented button | Doesn't render on third-party browsers | Use SDK button (custom element) |
| Hardcoding the validation URL | Apple may route different domains differently | Always use event.validationURL |
| Inspecting / modifying the merchant session object | Opaque schema; modification breaks validation | Pass through verbatim |
| Domain behind CDN with redirect or proxy | Domain verification fails silently | Direct HTTPS access from Apple IPs |
apple-developer-merchantid-domain-association.txt not at apex /.well-known/ | File must be at top-level | Move file; re-verify |
Using canMakePaymentsWithActiveCard() | Deprecated WWDC24 | Switch to applePayCapabilities() |
| Apple Pay below other payment methods | AUG parity violation | Promote to at-least-equal prominence |
| Apple Pay not pre-selected when active card detected | AUG primary-option violation | Pre-select when paymentCredentialsAvailable |
| Floating-point amounts | JS Number precision corruption | Decimal-precise strings throughout |
Capturing the client-supplied total as the charge amount | Sheet amount is display-only and client-controlled — price tampering | Recompute the captured amount server-side from persisted cart line items |
POSTing to validationURL without checking its host | SSRF — proxies your merchant identity cert to an attacker-chosen server | Exact-host allowlist (apple-pay-gateway.apple.com, -cert sandbox, cn- China) + require https; never suffix-match apple.com |
| No idempotency key on capture | Retried onpaymentauthorized double-charges | Key capture on cartId + token at your endpoint and the PSP |
| Sandbox URL in production code | Validation succeeds in sandbox, transactions decline in production | Switch endpoints based on env |
Domain-Verification Debug Checklist
If domain verification fails (the most common single web-integration blocker):
- [ ] File at exact path
/.well-known/apple-developer-merchantid-domain-association.txt(not/apple-developer-merchantid-domain-association.txtat root) - [ ] HTTPS endpoint reachable from Apple's published IP ranges (see
/applepayontheweb/setting-up-your-server) - [ ] No redirects (HTTP→HTTPS redirect at the apex is fine; redirects on the file path are not)
- [ ] No CDN that strips path or rewrites response
- [ ] Server returns the file with
Content-Type: text/plain(or any non-HTML; some CDNs replace plaintext with HTML 404 pages) - [ ] Apex domain matches the registered merchant domain exactly (no
www.mismatch) - [ ] File contents unmodified from Apple's download (don't edit, don't add a BOM, don't trim newlines)
- [ ] Domain verification re-run from Apple Developer portal after placing file
If verification passes but onvalidatemerchant still fails: check the curl command in MIG p.11 against your server's cert + domain pair. That isolates the problem to either the cert or the domain.
Time-Pressure Triage Order (Production Down)
When merchant validation breaks in production with a deadline (launch day, active outage, CEO on the bridge), the sequence below isolates the failure surface in 30 seconds and prevents panic-driven mistakes that extend the outage. Run in this order, not in parallel:
| # | Action | Time | Why this order |
|---|---|---|---|
| 1 | MIG p.11 curl against apple-pay-gateway.apple.com (production) or apple-pay-gateway-cert.apple.com (sandbox) using prod merchant identity cert + key + the EXACT initiativeContext your frontend sends | 30s | Tests cert + key + domain registration + outbound network + Apple-side health in one shot. Almost every other check is a subset of what this proves. |
| 2 | If curl returns session JSON but live flow still fails: cert is fine, initiativeContext mismatch is the next suspect — diff the curl initiativeContext against the actual validationURL event payload. www-vs-apex, port numbers, and trailing-dot variants all silently fail. | 1 min | Curl proving the cert pair works narrows the search to context. |
| 3 | If curl fails with TLS error: openssl x509 -noout -dates on the cert file, plus modulus-match against the key | 1 min | Distinguishes expiry from cert/key mismatch. |
| 4 | If curl fails connection-reset / hang: domain-association file at /.well-known/, then Apple IP allowlist on egress firewall | 5 min | Connection failure = DNS / network / .well-known issue, not cert issue. |
Don't panic-do (named anti-actions during incidents)
| Anti-action | Why it's wrong |
|---|---|
| Re-issue the merchant identity cert before MIG p.11 curl confirms expiry | Re-issuance creates a new failure window; if cert wasn't the issue, you've made it worse |
| Bounce app servers as the first move | Server bounces don't fix upstream Apple-side or cert issues; they trade the outage for a cold-cache spike |
| Pre-emptively hide the button before running the 30s curl test | Curl test is faster than the deploy that would hide the button — diagnose first |
| Ship "Apple Pay temporarily unavailable" copy on the cart page | AUG governs prominence when present; absence is allowed during incidents. A status-message banner about a payment method down is itself an AUG signal Apple may flag — just remove the button conditionally |
AUG mitigation rule (incident-only)
The Web AUG parity rule applies to button presence, not absence. If Apple Pay is broken in production, hide the button via feature flag — that satisfies AUG. Do not add disclosure text about Apple Pay being temporarily down; do not show a greyed-out button. Either Apple Pay is functional and prominent, or it's absent. Re-enable behind a canary once the curl test passes again.
TLS / Cert Debug Checklist
- [ ]
ApplePay.crt.pemincludes only the certificate (no key bytes) - [ ]
ApplePay.key.pemis unencrypted (or your server is configured with the passphrase) - [ ] Server presents both client cert + private key on outbound TLS to
apple-pay-gateway.apple.com - [ ] Cert hasn't expired (Merchant Identity Cert and Payment Processing Cert both expire — track separately)
- [ ] Cert matches the merchant ID in the request body (
merchantIdentifier) - [ ]
initiativeContextmatches a verified domain registered under the same merchant ID
Pre-Production Checklist (MIG p.22)
- [ ] curl test against
apple-pay-gateway-cert.apple.comsucceeds (sandbox) andapple-pay-gateway.apple.comsucceeds (production) - [ ] Button renders on Safari (Mac, iOS, iPadOS, visionOS)
- [ ] Button renders on Chrome / Edge / Firefox via JS SDK 1.2.0+ (third-party browser scan-to-pay)
- [ ] Sandbox flow exercised end-to-end with sandbox tester + sandbox FPANs
- [ ] Production flow exercised with real cards on multiple devices
- [ ]
applePayCapabilities()decision tree exercised:paymentCredentialsAvailable(primary),paymentCredentialStatusUnknown(visible),paymentCredentialsUnavailable(hidden) - [ ] Error paths via
ApplePayErrorexercised (shipping, contact, coupon) - [ ] Recurring / automatic / deferred variants tested if applicable
- [ ] Disbursement flow tested if
supportsInstantFundsOutis in your capabilities - [ ] AUG parity verified across every page that shows a payment method
- [ ] Privacy statement linked from every page that initiates Apple Pay (App Store policy + AUG requirement)
- [ ] Cert renewal calendar entries set 30 days before each cert's expiry
Resources
MIG: pp.10–11 (cert + domain + curl test), pp.12–14 (sheet + validation), pp.13–17 (request + events), pp.18–19 (variants + tokens), p.22 (testing), p.27 (web sequence diagram), p.28 (PSP-hosted sequence)
WWDC: 2020-10662 (button, automatic style), 2021-10092 (redesigned sheet, JS SDK button, coupon, shipping date ranges), 2022-10041 (multi-merchant, automatic-reload, order tracking), 2023-10114 (Apple Pay Later merchandising, deferred, disbursements), 2024-10108 (third-party browser, JS SDK 1.2.0, applePayCapabilities, web disbursements, MCC)
Tech Talks: 111381 (Get started with Apple Pay on the Web — operational walkthrough)
Docs: /applepayontheweb, /applepayontheweb/configuring-your-environment, /applepayontheweb/setting-up-your-server, /applepayontheweb/maintaining-your-environment, /applepayontheweb/apple-pay-js-api, /applepayontheweb/payment-request-api, /applepayontheweb/applepaysession, /applepayontheweb/providing-merchant-validation, /applepayontheweb/checking-for-apple-pay-availability, /apple-pay/acceptable-use-guidelines-for-websites
Sample: applepaydemo.apple.com (interactive)
Skills: apple-pay-web-ref (API surface), apple-pay (native counterpart for shared concepts), apple-pay-vs-iap (boundary), payments-diag (cert + domain failure modes), axiom-design/hig (button placement)
Apple Pay — Native Apps (iOS / iPadOS / macOS / Catalyst / visionOS / watchOS)
You MUST use this skill for ANY native Apple Pay integration. The core operational document is the Apple Pay Merchant Integration Guide (MIG) — most of this skill cites it directly. For website integration use apple-pay-web.md. For the IAP boundary, see apple-pay-vs-iap.md.
The Five-Actor Mental Model (MIG p.5)
Customer → Merchant App → Merchant Server → PSP → Acquirer → Network → Issuer| Actor | What they do |
|---|---|
| Customer | Authenticates with biometric / passcode. Apple device encrypts payment data and returns it to your app. |
| Merchant App | Sends the encrypted Apple Pay payment object to your server. |
| Merchant Server | Forwards payment object to your Payment Service Provider (PSP). |
| PSP | Decrypts the Apple Pay payment object using your Payment Processing private key. Formats a 3D Secure authorization message. |
| Acquirer | Routes payment for authorization. |
| Payment Network | De-tokenizes the DPAN and forwards the PAN to the issuing bank. |
| Issuer | Authorizes (or declines). |
Apple decrypts nothing. Apple's role is to encrypt the device-specific tokenized credentials (DPAN) at authentication time. The private key for decryption belongs to you (or your PSP). This affects who controls the Payment Processing Certificate's CSR — see Pre-Flight Checklist.
Pre-Flight Checklist (MIG pp.4–9)
Run this before writing any PassKit code. Skipping any item produces silent failures that surface only at sandbox or production.
| Step | Owner | What |
|---|---|---|
| Confirm PSP supports Apple Pay | Payments team | Check Apple's supported PSPs list at /apple-pay (#payment-platforms section). If yours isn't listed, contact them directly. |
| Apple Developer Program membership | Account holder | Required, renewed yearly. Enterprise Program accounts cannot use Apple Pay. |
| Create Merchant ID | Account holder | merchant.com.example.foo reverse-DNS form. Never expires. Reusable across multiple apps + websites. |
| Create Payment Processing Certificate | Account holder + PSP | ECC 256-bit (or RSA 2048 for mainland China). Expires every 25 months. If your PSP decrypts on their side, follow their CSR procedure (typically they provide the CSR). If you self-decrypt, you generate the CSR locally. |
| Enable Apple Pay capability in Xcode | Developer | Signing & Capabilities → + → Apple Pay → click refresh → select merchant ID. |
| (For automation) Apple Pay sandbox tester account | Developer | Created in App Store Connect. Sign out of iCloud first; sign in with the sandbox tester. |
Cert renewal — the create-but-don't-activate workflow (MIG p.9)
Renewal is a two-stage operation. Most production outages around Apple Pay come from skipping the staging:
1. Generate the new Payment Processing Certificate before the old one expires (create). 2. Coordinate with your PSP. Wait until both sides are ready. 3. Click Activate in the Apple Developer portal at the agreed cutover time.
"This prepares your replacement certificate but doesn't activate it immediately. Work with your PSP to choose the best time to switch over to the renewed certificate, and when you are both ready, press the 'activate' button next to the certificate in the Apple Developer Portal." — MIG p.9
Failing to coordinate the activation toggle = a window where Apple servers encrypt with one key but your PSP holds another. Transactions fail.
Constructing the Payment Request
The shape of the request is the same regardless of platform. Three patterns:
One-time payment (the default)
let request = PKPaymentRequest()
request.merchantIdentifier = "merchant.com.example.shop"
request.supportedNetworks = [.visa, .masterCard, .amex, .discover]
request.merchantCapabilities = [.threeDSecure] // or include .credit / .debit if needed
request.countryCode = "US"
request.currencyCode = "USD"
request.paymentSummaryItems = [
PKPaymentSummaryItem(label: "Subtotal", amount: NSDecimalNumber(string: "89.99")),
PKPaymentSummaryItem(label: "Shipping", amount: NSDecimalNumber(string: "5.00")),
// Final line: label = your customer-facing business name; amount = grand total displayed next to "Pay"
PKPaymentSummaryItem(label: "Example Shop", amount: NSDecimalNumber(string: "94.99"))
]
request.requiredShippingContactFields = [.name, .postalAddress]
request.requiredBillingContactFields = [.postalAddress]`PKPaymentSummaryItem.amount` is `NSDecimalNumber`, not `Double`. Use NSDecimalNumber(string:) from a string, or NSDecimalNumber(decimal:) from a Swift Decimal. Passing a Double literal won't compile. Wrong amount precision is the single most common Apple Pay integration bug — currency math from Double is the root cause.
Privacy discipline: only request fields you actually need to fulfil the order. The HIG penalizes over-collection.
The last summary item is the line displayed next to "Pay" on the sheet, and the label is the business name customers see. If you are an intermediary, format as "Pay [End_Merchant] (via [Your_Business])" per HIG and apple-pay-vs-iap.md.
Recurring / automatic-reload / deferred / disbursement variants (MIG pp.18–19)
Apple Pay has dedicated request types for non-one-time scenarios. You cannot set more than one variant on a single request.
| Scenario | Request property | Use when |
|---|---|---|
| Subscription (digital or service) at fixed interval | recurringPaymentRequest (PKRecurringPaymentRequest) | Streaming, gym, club dues, utility-style recurring billing. The MPAN ("merchant token") lets billing continue across device upgrades and card replacements. |
| Auto top-up at threshold | automaticReloadPaymentRequest (PKAutomaticReloadPaymentRequest) | Stored-balance / transit / store-card reload when balance dips below threshold. |
| Pay later at delivery | deferredPaymentRequest (PKDeferredPaymentRequest) | Hotel booking, pre-order, car rental with incidental authorization. Free-cancellation period and bill-on date. |
| Pay out to the user | PKDisbursementRequest | Funds transfer from your platform to the user's card linked in Wallet. Web-equivalent uses Disbursement Request Modifier. |
Use these instead of rolling your own subscription billing. Without PKRecurringPaymentRequest, you lose merchant-token continuity — every device swap or card replacement breaks the subscription.
Apple Pay Later (US-only, WWDC23)
WWDC23 introduced two complementary surfaces for Apple Pay Later:
- Pre-checkout merchandising via
PKPayLaterView(UIKit) /PayLaterView(SwiftUI). Place on product / cart pages to indicate "Pay Later available" before the customer initiates checkout. Gate display with the freePKPayLaterValidateAmount(_:currencyCode:completion:)function from PassKit'sPKPayLaterValidator.h(iOS 17+, iOS-only); the completion block receives aBOOL eligible. - Per-request gating via
applePayLaterAvailability(current and supported —API_AVAILABLEmacos 14, ios 17, watchos 10; not deprecated in the iOS 26.5 SDK). When you set it,.unavailablerequires an associatedReason:
request.applePayLaterAvailability = .unavailable(.itemIneligible)
// or .unavailable(.recurringTransaction) for subscription-style requestsUse .unavailable for prohibited transaction types (subscriptions, recurring items, gift cards). Apple Pay Later is folded into the existing payment request — it's not a separate flow.
Multi-merchant in one sheet (PKPaymentTokenContext, WWDC22)
A booking flow that pays a hotel, an airline, and a car-rental company in one user gesture:
let hotelContext = PKPaymentTokenContext(
merchantIdentifier: "merchant.com.example.partners.hotelco",
externalIdentifier: "hotelco",
merchantName: "HotelCo",
merchantDomain: "hotelco.example",
amount: NSDecimalNumber(string: "320.00")
)
request.multiTokenContexts = [hotelContext, airlineContext, carRentalContext]Each context produces its own encrypted payment token with its own PSP routing. The user sees one sheet but the merchant settlement is split.
Merchant Category Code (WWDC24)
Set merchantCategoryCode (ISO 18245 four-digit code) when supported card types vary by category. Without an MCC, the sheet may show cards that the customer's bank then declines for that merchant type — bad UX, avoidable.
Presenting the Sheet
| Surface | API | Notes |
|---|---|---|
| iOS / iPadOS | PKPaymentAuthorizationController (preferred) or PKPaymentAuthorizationViewController | Controller is non-UI; ViewController hosts presentation. SwiftUI PayWithApplePayButton (iOS 16+) wraps this. |
| macOS / Catalyst | PKPaymentAuthorizationController with explicit window | Mac/Catalyst use the web security model, not native — see Catalyst section. |
| watchOS | WKInterfacePaymentButton | Different API surface; see apple-pay-ref.md watchOS section. |
| visionOS | PKPaymentAuthorizationController / SwiftUI button | Identical to iOS API; only auth modality changes (Optic ID). |
SwiftUI button (preferred, iOS 16+)
PayWithApplePayButton(.buy) {
// build PKPaymentRequest, present via PKPaymentAuthorizationController
}
.payWithApplePayButtonStyle(.automatic) // adapts to Light/Dark
.frame(height: 45)PayWithApplePayButton is the modern path. Still build the PKPaymentRequest exactly as above.
Capability detection (MIG p.13)
if PKPaymentAuthorizationController.canMakePayments() {
// device hardware supports Apple Pay (Secure Element present)
}
if PKPaymentAuthorizationController.canMakePayments(usingNetworks: [.visa, .masterCard]) {
// device has at least one card in Wallet for those networks
}canMakePayments() checks device capability only — not whether a card is provisioned. canMakePayments(usingNetworks:) checks both. Use both.
HIG rule: If canMakePayments() returns true, you must show the Apple Pay button. Don't gate it on the user already having a card; the system handles the "no card → set up Wallet" flow.
Delegate Callbacks (MIG pp.15–17)
The system calls your delegate at every customer interaction with the sheet. Respond promptly — the system aborts the transaction if your handler stalls. Don't run synchronous network calls or long fulfillment logic inside a callback; pre-compute or kick off async work and return.
| Callback | Trigger | What to do |
|---|---|---|
paymentAuthorizationController(_:didChangeShippingContact:handler:) | Customer changes shipping address | Recalculate shipping methods + costs + tax, return updated PKPaymentRequestShippingContactUpdate with new summary items |
paymentAuthorizationController(_:didChangeShippingMethod:handler:) | Customer picks a different shipping option | Recalculate total, return PKPaymentRequestShippingMethodUpdate |
paymentAuthorizationController(_:didChangePaymentMethod:handler:) | Customer switches to different card | Recalculate any card-specific fees / discounts, return PKPaymentRequestPaymentMethodUpdate |
paymentAuthorizationController(_:didChangeCouponCode:handler:) | Customer enters / changes coupon code | Validate, return PKPaymentRequestCouponCodeUpdate |
paymentAuthorizationController(_:didAuthorizePayment:handler:) | Customer confirms with Face/Touch/Optic ID | This is where you hand the encrypted token to your server. Return PKPaymentAuthorizationResult (.success or .failure(errors:)) promptly. |
paymentAuthorizationControllerDidFinish(_:) | Sheet dismissed | Clean up; may or may not have been a successful payment. |
Privacy: redacted addresses pre-authorization (MIG p.15)
Before the user confirms, you receive a redacted shipping contact — no street, name, or phone. Use this to compute shipping options and tax. After the user authorizes, the complete contact is delivered.
// Pre-auth (redacted)
{ country: "US", region: "NC", city: "Raleigh", postalCode: "27601" }
// Post-auth (full)
{ country: "US", addressLines: ["2399 Elm St"], region: "NC", city: "Raleigh",
postalCode: "27601", recipient: "Allison Cain", phone: "..." }Don't require post-auth fields to compute pre-auth state. Plenty of integrations bug out because they need a phone number to estimate shipping cost — they can't, by design.
Error Handling (MIG p.16)
Use PKPaymentError to point users at specific fields with friendly messages. The sheet highlights the bad field automatically:
let zipError = PKPaymentRequest.paymentShippingAddressInvalidError(
withKey: CNPostalAddressPostalCodeKey,
localizedDescription: "ZIP code doesn't match city"
)
completion(PKPaymentAuthorizationResult(status: .failure, errors: [zipError]))Address validation discipline:
- Tolerate ZIP+4 and various phone formats. Apple's words: "intelligent enough to ignore irrelevant data."
- Accept addresses with missing `subAdministrativeArea` / `subLocality` / `phoneticFamilyName` — they're often empty.
- Don't validate address fields against your own US-only rules when shipping internationally —
country/countryCodeis authoritative.
Authorization Handoff to PSP (MIG pp.20–21)
After didAuthorizePayment, you have a PKPaymentToken containing:
PKPaymentToken
├─ paymentMethod (network, type, displayName) — non-sensitive, OK to display
├─ transactionIdentifier — opaque
└─ paymentData — the encrypted blob
├─ data: ciphertext
├─ signature: ECDSA / RSA signature
├─ header: { publicKeyHash, ephemeralPublicKey, transactionId }
└─ version: "EC_v1" (or "RSA_v1" for mainland China)Two paths to PSP:
1. Pass the encrypted blob through. Most PSPs accept paymentData as-is; they decrypt on their side using the Payment Processing private key (which they hold because they generated the CSR). This is the default and recommended path. 2. Self-decrypt and forward. If you generated the CSR yourself, you hold the private key and can decrypt server-side. Then you forward the decrypted card data to the PSP. See apple-pay-ref.md "Payment Token Format" and Apple's /passkit/payment-token-format-reference.
Never put decryption logic in the client app. The private key never belongs on the device — server-side only. This is non-negotiable.
Completing the handler
// Inside didAuthorizePayment
let result = await myServer.authorize(token: payment.token)
if result.success {
completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
} else {
completion(PKPaymentAuthorizationResult(status: .failure, errors: result.errors))
}Keep the auth call fast — the system aborts if you stall. If your server is slow, don't return .success and reconcile asynchronously: that charges the customer for an order you can't yet fulfil. Tighten your auth path instead.
Verify the amount server-side. The PKPaymentRequest — including paymentSummaryItems and the grand total — is assembled on the device and is attacker-controllable. Recompute the order total on your server from trusted data (catalog prices, cart state) before you authorize the charge. Never charge the amount the client sends.
Order tracking handoff (WWDC22)
Set PKPaymentOrderDetails on the result to hand off post-purchase tracking to Wallet's Orders surface (see wallet-orders.md):
let orderDetails = PKPaymentOrderDetails(
orderTypeIdentifier: "order.com.example.shop",
orderIdentifier: "ORD-12345",
webServiceURL: URL(string: "https://orders.example.com")!,
authenticationToken: "shared-secret-for-this-order"
)
let result = PKPaymentAuthorizationResult(status: .success, errors: nil)
result.orderDetails = orderDetails // set as property; not an init parameter
completion(result)Apple async-pulls the signed order package from your server. The customer sees the order in Wallet automatically. See wallet-orders.md for package signing.
Apple Pay Mark vs Apple Pay Button — A Named Anti-Pattern
This is the most common HIG violation in shipped apps. They are different things:
| Element | Purpose | Tappable? | API-provided? |
|---|---|---|---|
| Apple Pay Button | Initiates the payment flow | Yes | Yes (PKPaymentButton, PayWithApplePayButton, WKInterfacePaymentButton) |
| Apple Pay Mark | Communicates "Apple Pay accepted" — signage only | No | Static graphic from Apple Pay Marketing Guidelines (/apple-pay/marketing) |
"Use the Apple Pay mark only to communicate that Apple Pay is accepted. The Apple Pay mark doesn't facilitate payment. Never use it as a payment button or position it as a button." — Apple Pay HIG
The wrong path: placing the Apple Pay Mark in your custom checkout button as both label and trigger. The right path: use the Apple Pay Mark as inline signage on your product / cart pages; use the API-provided Apple Pay Button for the action.
If you must use a custom button (e.g. a generic "Pay" button on a page where multiple methods exist), the HIG is explicit: don't display "Apple Pay" or the Apple Pay logo on a custom button. Reference Apple Pay separately on the same page using the Mark.
Sandbox Testing Discipline (MIG p.22)
| Rule | Why |
|---|---|
| Use App Store Connect sandbox tester account; sign out of iCloud first | The sandbox-vs-prod boundary is at the iCloud account level. |
| Use Apple-provided sandbox FPANs | Test cards available per region (US / UK / etc.). For OTP prompts, use 111111. |
| Sandbox transactions decline pre-fulfillment by design | This is not a bug. Sandbox is for flow validation, not order completion. |
| Test in production with real cards before launch | The PSP test key won't match the production key. End-to-end with real money on real cards is the only valid pre-launch test. |
| Test on multiple devices and browsers | Especially since iOS 18+ added third-party browser support; web flows must work outside Safari. |
| Map every PKPayment field to your order system | Phone numbers may have + prefix; ZIP may be ZIP+4; verify your fulfillment ingest. |
"Attempting to use these cards with a live production environment will result in your PSP rejecting the transaction." — MIG p.22
The sandbox is fragile by design. If your PSP returns an error against sandbox, that's not a sandbox failure — it's a working sandbox correctly refusing fake money.
Catalyst / macOS Considerations (WWDC20, MIG)
Mac and Catalyst Apple Pay use the web security model, not native. This affects three things:
1. Window requirement. PKPaymentAuthorizationController must be presented from a window — not from a controllerless context. AppKit + Catalyst always have a window; the requirement is operational, not API-shape. 2. Merchant validation is required even in-app. Implement paymentAuthorizationController(_:didRequestMerchantSessionUpdate:) — Catalyst calls this just like the web does. Native iOS/iPadOS doesn't. 3. Static merchant validation URL. Use apple-pay-gateway.apple.com/paymentservices/paymentSession (production) and apple-pay-gateway-cert.apple.com/paymentservices/paymentSession (sandbox). The legacy region-specific URLs (e.g. apple-pay-gateway-uk.apple.com) were removed — using one will fail merchant validation.
The merchant validation step on Mac/Catalyst is server-side only. Same rules as the web: never call paymentSession from the client, only from your server with the Merchant Identity Certificate via two-way TLS.
visionOS
API-shape identical to iOS. The only material difference is the auth modality: Optic ID (or device passcode) replaces Face ID / Touch ID. No code changes required — PKPaymentAuthorizationController adapts automatically. SwiftUI PayWithApplePayButton works as on iOS.
App Clips (WWDC20)
Apple Pay is the recommended payment method for App Clips:
- No account-creation friction (Apple Pay supplies the contact + payment data).
- Pair with Sign in with Apple for guest checkout if you need a customer record.
- App Clips have a 10MB binary limit; PassKit's footprint is system-supplied, no impact.
A clip's button surface and authorization flow are identical to a full app. Use the SwiftUI PayWithApplePayButton to keep the binary lean.
Anti-Patterns
| Anti-Pattern | Why it fails | Fix |
|---|---|---|
| Putting payment-token decryption in the client app | Private key on device = security violation; PSPs reject this design | Decrypt only on your server, or pass the encrypted blob through to the PSP |
Skipping merchantCategoryCode (WWDC24) when supported card types vary | Sheet shows cards the customer's bank declines for that MCC | Set MCC from ISO 18245 |
Using deprecated requiredShippingAddressFields instead of requiredShippingContactFields | Deprecated since iOS 11; doesn't surface name/email correctly | Use requiredShippingContactFields |
Rolling your own subscription billing instead of PKRecurringPaymentRequest | Loses merchant-token continuity; subscription breaks on device upgrade | Use the dedicated request type |
Calling old country-specific merchant validation URL (apple-pay-gateway-uk.apple.com etc.) | URL was removed | Use static apple-pay-gateway.apple.com |
| Treating the Apple Pay Mark as a button | HIG violation; conversion-killer; App Review rejection trigger | Use API-provided button; Mark is signage |
| Validating addresses against US-only rules in international flow | Rejects valid international addresses (UK postcodes, e.g.) | Tolerate variations; use countryCode to scope validation |
| Requesting more contact fields than you need | Privacy issue + cart abandonment | Request only what fulfillment requires |
| Trusting the client-built request total | The on-device PKPaymentRequest is attacker-controllable; a tampered total charges the wrong amount | Recompute and verify the order total server-side before authorizing |
| Skipping the cert-renewal create-but-don't-activate workflow | Production transactions decrypt with wrong key during cutover | Coordinate activation with PSP; flip toggle at agreed time |
| Using Enterprise Program account | Apple Pay is not available on Enterprise Program | Use Apple Developer Program (paid, $99/year) |
Pre-Launch Checklist (MIG p.22)
- [ ] Apple Pay button visible on every device + browser combination you support
- [ ] Sandbox flow exercised end-to-end (decline expected — that's correct)
- [ ] Production flow exercised with real cards on real devices (success expected)
- [ ] All PKPayment contact fields mapped to your order management system
- [ ] Phone number format variations handled (with/without
+, with/without country code, with/without spaces) - [ ] Address format variations handled across countries you ship to
- [ ] Coupon-code event handling tested if
supportsCouponCodeis enabled - [ ] Shipping-method change events tested
- [ ] Cancellation paths exercised — sheet dismissed pre-auth, dismissed post-auth, app suspended mid-flow
- [ ] Recurring/automatic-reload/deferred flows tested if applicable, including merchant-token notification (MIG p.19)
- [ ] Cert expiry calendar entry set 30 days before 25-month mark
Risk Management Checklist (MIG p.23)
Apple's recommendations for fraud-mitigation independent of the PSP's controls:
- [ ] Velocity — limit transactions per device / card / IP within a window
- [ ] Delivery — flag mismatched billing/shipping countries
- [ ] Bad-transaction list — maintain block list of known-fraud cards / contacts
- [ ] Email / IP signals — disposable-email, anonymized-IP heuristics
- [ ] Country / region — flag transactions from regions you don't normally serve
These are PSP-orthogonal — PSPs do their own fraud screening, but you should still implement merchant-level checks.
Resources
MIG: pp.4–9 (setup), pp.13–17 (request + delegates), pp.18–19 (variants + merchant tokens), pp.20–22 (auth + testing), p.23 (risk), p.26 (sequence diagram)
WWDC: 2020-10662 (Catalyst, App Clips, button types, static URL), 2021-10092 (redesigned sheet, coupon, shipping date ranges), 2022-10041 (multi-merchant, automatic payments, order tracking, SwiftUI buttons), 2023-10114 (Apple Pay Later, deferred, disbursements, MCC), 2024-10108 (third-party browser, JS SDK 1.2.0, applePayCapabilities, MCC)
HIG: /design/human-interface-guidelines/apple-pay (button, mark, payment-sheet UX)
Docs: /passkit, /passkit/setting-up-apple-pay, /passkit/pkpaymentrequest, /passkit/payment-token-format-reference, /help/account/capabilities/configure-apple-pay, /apple-pay (PSP list, marketing)
Skills: apple-pay-ref (API surface), apple-pay-vs-iap (boundary), apple-pay-web (web), wallet-orders (post-purchase), payments-diag (failure modes), axiom-design/hig (button placement), axiom-security/keychain-ref (cert export), axiom-shipping/app-store-diag (rejection patterns)
Wallet Extensions — Issuer Provisioning Reference
API surface for issuer-side Wallet extensions that let banks / card-issuer apps add provisionable cards to Apple Pay directly from within the Wallet app.
This is not for merchant developers. If you accept payments, see apple-pay.md. If you build a bank or card-issuer app, this is for you.
Audience Boundary
Wallet Extensions are exclusively for apps that issue payment cards — banks, credit unions, card networks. They surface "Add Card" entry points inside Wallet itself, so customers don't have to launch the issuer app first.
If your app is anything else, this skill doesn't apply. Use:
apple-pay.md— accepting payments (merchant)wallet-passes.md— issuing tickets / coupons / loyalty (non-payment Wallet artifacts)tap-to-pay.md— accepting contactless payments on iPhone
Availability
iOS 14.0+, iPadOS 14.0+, Mac Catalyst 14.0+, visionOS 1.0+.
Two Extensions Per Issuer App
| Extension | Class / Protocol | Role |
|---|---|---|
| Non-UI (NUI) | PKIssuerProvisioningExtensionHandler subclass | Reports status, lists provisionable passes, generates the PKAddPaymentPassRequest. No UI. |
| UI | PKIssuerProvisioningExtensionAuthorizationProviding (typically a UIViewController) | Re-authentication when NUI reports auth required. Uses the issuer app's credentials. |
Both ship as separate extension bundles inside the issuer app target. Wallet invokes them when the customer taps "Add a Card" in Wallet.
For full method signatures, parameter shapes, and result enums, see Apple's docs — the Resources section below points to the authoritative pages.
Required Entitlements (Apple-Managed)
Wallet Extensions require Apple-issued entitlements — request via Apple Developer Support, not Xcode capabilities. The NUI and UI extensions need separate entitlement keys.
You cannot test these extensions without the entitlement. Apple reviews each request and grants on a case-by-case basis (typically restricted to verified card-issuing institutions). If you're stuck waiting on the entitlement, see payments-diag.md for the entitlement-stuck pattern — there's nothing to debug locally until Apple grants access.
Sample Code
Apple ships an "Implementing Wallet Extensions" sample at /passkit/implementing-wallet-extensions — a four-target project (containing app, UI extension, NUI extension, tests). Use as a template.
The provisioning data model (PKAddPaymentPassRequest + nonce + certificate chain) is shared with in-app provisioning via PKAddPaymentPassViewController. If you've shipped in-app provisioning, the data model is identical; the extension just hosts the same flow inside Wallet's UI. See apple-pay-ref.md for the shared PKAddPaymentPassRequest shape.
Resources
Docs: /passkit/pkissuerprovisioningextensionhandler, /passkit/pkissuerprovisioningextensionauthorizationproviding, /passkit/implementing-wallet-extensions, /passkit/pkaddpaymentpassrequest, /passkit/pkaddpaymentpassviewcontroller
WWDC: 2020-10662
Skills: apple-pay, apple-pay-ref, payments-diag
Related skills
FAQ
What StoreKit features does axiom-payments cover?
axiom-payments covers StoreKit 2 in-app purchases, auto-renewable subscriptions, restore flows, and receipt validation for monetized iOS applications, focusing on modern transaction APIs instead of legacy StoreKit 1.
Does axiom-payments handle server-side receipt checks?
axiom-payments includes receipt validation guidance so iOS apps verify App Store transactions server-side or on-device, supporting entitlement checks required before unlocking paid features.