The problem
Customers bought through four different paths: card payments on the web, Apple in-app purchases on two generations of StoreKit, Apple Pay, and an iOS webview. Failures on those paths were not loud. A purchase that silently failed looked like a customer who changed their mind, and only the noisiest cases ever reached support.
What we built
One error model across four rails
Typed exception classification for every payment failure path, so a declined card, an expired receipt and a webview that lost its session stop looking like the same unknown error.
StoreKit 1 and 2 handled honestly
JWS detection to tell the two generations apart, and purchase restore on StoreKit 2 with retry and de-duplication, so restoring does not grant or charge twice.
Apple Pay with a fallback
Apple Pay version 14 with an explicit fallback to version 2, plus resilience work on the iOS webview and PWA paths that customers actually buy through.
Failures that show up on a dashboard
Sentry context attached across the failure paths, so a broken rail appears in monitoring instead of arriving as a support ticket days later.
Screens



How we worked
Our founder proposed this hardening work rather than being assigned it: the failure classification came first, then the fixes the classification made visible. In-app purchases are about 8% of company orders, which is the scale the rails run at.
Results
directly attributable in-app upsell purchases
Measured after the hardening rolled out.
payment rails brought under one error model
Braintree, StoreKit 1 and 2, Apple Pay, iOS webview.
Payment failure noise in monitoring measurably dropped after rollout
Qualitative: the drop was observed, the exact percentage was not captured.
What's next
The classification is the base the platform now builds new purchase paths on.