Apple Pay on the Web sheet fails with PKPaymentAuthorizationStateFatalError after successful merchant validation (PROD trust policy)

Environment

  • macOS 26.5.1 (Build 25F80)
  • Safari 26.5
  • Apple Pay JS API version 8
  • Using https://applepay.cdn-apple.com/jsapi/v1/apple-pay-sdk.js
  • Reproduces on the staging website (Does not reproduce on the production website with non sandbox account)
  • Signed into a Sandbox Tester Apple ID, with a test credit card registered exactly per Apple Pay Sandbox Testing — this is the only card in Wallet on the laptop
  • Merchant validation is performed by our staging backend against our real (non-sandbox) merchant identity/certificate — i.e. a sandbox tester card being evaluated against a staging merchant session, which per the sandbox testing documentation is the expected/supported setup for testing on a live site without a separate merchant sandbox environment

Summary

After completeMerchantValidation() succeeds and the merchant session is accepted, the payment sheet still terminates with PKPaymentAuthorizationStateFatalError a few hundred milliseconds later, right after the native side logs a second "Evaluating merchant session using PROD trust policy." for the same session. The user sees "Payment failed" and the sheet closes. This happens on the very first attempt, with no user interaction beyond tapping the Apple Pay button — no shipping address is even selected before the failure.

Since the sandbox tester card + production merchant session combination is the setup the sandbox testing guide describes, we'd expect the sheet to proceed to onpaymentauthorized (Apple's sandbox docs describe payments as being processed as $0 test transactions in this mode) rather than fail natively before that stage is ever reached.

Steps to reproduce

  1. Visit an item page with an Apple Pay button.
  2. Tap the Apple Pay button to open the payment sheet.
  3. The sheet presents normally; no interaction with shipping address or payment method is required for the failure to occur.
  4. Within ~1 second, the sheet dismisses and displays a "Payment failed" error state.

Minimal reproduction of the JS side

const session = new ApplePaySession(8, {
  countryCode: 'US',
  currencyCode: 'USD',
  supportedNetworks: ['visa', 'masterCard', 'amex', 'discover'],
  merchantCapabilities: ['supports3DS'],
  total: { label: 'Merchant', amount: '10.00' },
});

session.onvalidatemerchant = async () => {
  const merchantSession = await validateMerchantOnServer(); // succeeds
  session.completeMerchantValidation(merchantSession); // accepted — see log below
};

session.onpaymentmethodselected = () => {
  session.completePaymentMethodSelection({
    newTotal: { label: 'Merchant', amount: '10.00' },
  });
};

session.oncancel = event => console.log('cancelled', event);

session.begin();

We reproduced this with both our full checkout implementation and this stripped-down request object (no shipping contact required, no line items) — same failure either way, which rules out anything about our order/line-item data.

What we expected

The sheet to proceed to the biometric authorization step and dispatch onpaymentauthorized once the user confirms.

What actually happens

The sheet fails immediately with a PKPaymentAuthorizationStateFatalError transition, even though every JS-side completion call (completeMerchantValidation, the payment-method selection) succeeded and returned PKPaymentAuthorizationStatusSuccess.

System log (captured via log stream --info --debug --predicate 'process == "Safari" OR process == "passd" OR process == "com.apple.PassKit.PaymentAuthorizationUIExtension"')

21:50:41.544  PaymentCoordinator::beginPaymentSession() -> 1
21:50:41.570  Received prepareWithPaymentRequest: <private>
21:50:42.297  PaymentCoordinator::validateMerchant()
21:50:42.563  PaymentCoordinator::completeMerchantValidation()
21:50:42.564  Received merchant session update with status:PKPaymentAuthorizationStatusSuccess session:<private>
21:50:42.564  Evaluating merchant session using PROD trust policy.        <- 1st occurrence, passes
21:50:42.572  PaymentCoordinator::didSelectPaymentMethod()
21:50:42.572  PaymentCoordinator::completePaymentMethodSelection()
21:50:42.653  Evaluating merchant session using PROD trust policy.        <- 2nd occurrence
21:50:42.656  State machine change state from ClientCallback to PrepareTransactionDetails with param: <private>
21:50:43.391  Task Completed: <private>
21:50:43.393  State machine change state from PrepareTransactionDetails to PKPaymentAuthorizationStateFatalError with param: <private>
21:50:43.393  Error  Payment failed with fatal error <private>
21:50:45.130  PaymentCoordinator::didCancelPaymentSession()

The first "Evaluating merchant session using PROD trust policy." line passes (the flow continues to didSelectPaymentMethod). The second occurrence is immediately followed by Task Completed and then the FatalError transition, which suggests PassKit re-validates the merchant session's trust a second time right before PrepareTransactionDetails, and that second check is what's rejecting the session — even though the identical session object was accepted moments earlier at completeMerchantValidation().

What we've tried / ruled out

  • Confirmed every JS completion method (completeMerchantValidation, completePaymentMethodSelection) is called and returns success — no 30-second timeout, no missing completion call, no thrown exception on our side.
  • Reproduced with a minimal, hardcoded ApplePayPaymentRequest (fixed $10.00 total, no shipping fields) — rules out anything about our real order/line-item/sales-tax data.
  • Reproduced on the main branch and independently on our staging deployment running unmodified code — rules out anything specific to our recent frontend changes.
  • The <private> redaction in the system log prevents us from seeing what specifically fails during the second trust evaluation.
  • Confirmed the sandbox tester card and its registration follow the sandbox testing guide exactly — it's the only card on the device, and no other Wallet cards are involved.

Question

  1. What does PassKit's second "Evaluating merchant session using PROD trust policy" check (immediately before PrepareTransactionDetails) actually validate, beyond what's already checked at completeMerchantValidation() time?
  2. Is a sandbox tester card being evaluated against a production merchant session expected to pass this second check, or does sandbox testing per the linked guide require something additional on the merchant/session side (e.g. a specific field on the merchant session, or a specific environment value) that our backend isn't setting?
  3. Is there a way to get an unredacted reason for the rejection — e.g. via a private-data-enabled log profile or another diagnostic — since the redacted system log alone doesn't surface one?
Answered by DTS Engineer in 897740022

Hi @tanakaworld-mercari,

You wrote:

  1. What does PassKit's second "Evaluating merchant session using PROD trust policy" check (immediately before PrepareTransactionDetails) actually validate, beyond what's already checked at completeMerchantValidation() time?

They are not the same check run twice for redundancy — they're two different stages of PassKit's merchant-session trust verification:

  • 1st occurrence (at completeMerchantValidation) validates the merchant session payload itself — this is a structural/signature check.
  • 2nd occurrence (right before PrepareTransactionDetails) happens once PassKit has a selected payment method (a specific card/token) and is about to actually prepare the transaction for authorization. This is where an environment mismatch (sandbox tester card vs. a payment-processing certificate that was issued for production) would surface.

So, the first check validates the envelope, the second validates whether that envelope's certificate can actually be used for this card's environment.

Next, you wrote:

  1. Is a sandbox tester card being evaluated against a production merchant session expected to pass this second check, or does sandbox testing per the linked guide require something additional on the merchant/session side (e.g. a specific field on the merchant session, or a specific environment value) that our backend isn't setting?

It is expected that the payment session, certs, and cards should all be configured for the same environments.

The sandbox testing guidance about using your real merchant identity/certificate on a live site is written primarily for the merchant-validation handshake working at all without a separate sandbox merchant enrollment — it is not a guarantee that the full authorization path (which depends on the card's environment, not just the session's signer) will complete. In practice, sandbox tester cards are generally expected to be exercised on sandbox-flagged endpoints/environments end-to-end (sandbox merchant session or a client explicitly flagged for sandbox), and mixing a sandbox DAN with a session PassKit resolves to production is a known incompatibility class, not a supported combination — even though nothing in the JS API surfaces this before the native authorization stage.

To test your staging site end-to-end, do either of the following:

  1. Use a real card in production. This is the recommended approach. Test your full authorization flow with a real card against your production merchant identity/certificate on your production endpoint before deploying.
  2. Create a separate sandbox merchant enrollment. If you require a completely isolated test environment, configure a separate sandbox merchant ID and certificate specifically for staging testing.

For more information, see the Sandbox Testing section below:

Apple Pay Merchant Integration Guide

https://developer.apple.com/apple-pay/Apple-Pay-Merchant-Integration-Guide.pdf

Then, you wrote:

  1. Is there a way to get an unredacted reason for the rejection — e.g. via a private-data-enabled log profile or another diagnostic — since the redacted system log alone doesn't surface one?

Yes, you can use the Apple Pay logging profile, as mentioned in the forums post below, then reproduce the issue and finally gather sysdiagnose logs:

Gathering Required Information for Troubleshooting Apple Pay on the Web Merchant Issues

https://developer.apple.com/forums/thread/762994

Cheers,

Paris X Pinkney |  WWDR | DTS Engineer

Hi @tanakaworld-mercari,

You wrote:

  1. What does PassKit's second "Evaluating merchant session using PROD trust policy" check (immediately before PrepareTransactionDetails) actually validate, beyond what's already checked at completeMerchantValidation() time?

They are not the same check run twice for redundancy — they're two different stages of PassKit's merchant-session trust verification:

  • 1st occurrence (at completeMerchantValidation) validates the merchant session payload itself — this is a structural/signature check.
  • 2nd occurrence (right before PrepareTransactionDetails) happens once PassKit has a selected payment method (a specific card/token) and is about to actually prepare the transaction for authorization. This is where an environment mismatch (sandbox tester card vs. a payment-processing certificate that was issued for production) would surface.

So, the first check validates the envelope, the second validates whether that envelope's certificate can actually be used for this card's environment.

Next, you wrote:

  1. Is a sandbox tester card being evaluated against a production merchant session expected to pass this second check, or does sandbox testing per the linked guide require something additional on the merchant/session side (e.g. a specific field on the merchant session, or a specific environment value) that our backend isn't setting?

It is expected that the payment session, certs, and cards should all be configured for the same environments.

The sandbox testing guidance about using your real merchant identity/certificate on a live site is written primarily for the merchant-validation handshake working at all without a separate sandbox merchant enrollment — it is not a guarantee that the full authorization path (which depends on the card's environment, not just the session's signer) will complete. In practice, sandbox tester cards are generally expected to be exercised on sandbox-flagged endpoints/environments end-to-end (sandbox merchant session or a client explicitly flagged for sandbox), and mixing a sandbox DAN with a session PassKit resolves to production is a known incompatibility class, not a supported combination — even though nothing in the JS API surfaces this before the native authorization stage.

To test your staging site end-to-end, do either of the following:

  1. Use a real card in production. This is the recommended approach. Test your full authorization flow with a real card against your production merchant identity/certificate on your production endpoint before deploying.
  2. Create a separate sandbox merchant enrollment. If you require a completely isolated test environment, configure a separate sandbox merchant ID and certificate specifically for staging testing.

For more information, see the Sandbox Testing section below:

Apple Pay Merchant Integration Guide

https://developer.apple.com/apple-pay/Apple-Pay-Merchant-Integration-Guide.pdf

Then, you wrote:

  1. Is there a way to get an unredacted reason for the rejection — e.g. via a private-data-enabled log profile or another diagnostic — since the redacted system log alone doesn't surface one?

Yes, you can use the Apple Pay logging profile, as mentioned in the forums post below, then reproduce the issue and finally gather sysdiagnose logs:

Gathering Required Information for Troubleshooting Apple Pay on the Web Merchant Issues

https://developer.apple.com/forums/thread/762994

Cheers,

Paris X Pinkney |  WWDR | DTS Engineer

Apple Pay on the Web sheet fails with PKPaymentAuthorizationStateFatalError after successful merchant validation (PROD trust policy)
 
 
Q