App Store Server Notifications v2: tracking your iOS subscription lifecycle in real time
A practical guide to Apple's App Store Server Notifications v2 — what each notification type means, how to verify the JWT payload, and which events you must handle to avoid silent churn in your subscription app.
When a subscriber lets their payment lapse, upgrades to annual, or triggers a refund, your server needs to know — instantly. Without App Store Server Notifications, your backend discovers these changes only when the user next opens the app, which can be days or weeks later. That lag silently inflates your "active subscriber" count, breaks entitlement logic, and makes win-back campaigns nearly impossible to time correctly.
This guide covers Apple's current notification system (version 2), the events you must handle on day one, how to verify the cryptographic payload, and how to build a handler resilient enough for production traffic.
What App Store Server Notifications are — and what version 2 changed
App Store Server Notifications are HTTPS POST requests that Apple sends to a URL you register in App Store Connect. Each POST carries a signed JWT containing a signedPayload, which decodes into data about the transaction that just occurred: the notification type, a subtype, and a signed transaction representation.
Version 1 (now deprecated but still active for apps that haven't migrated) sent a flat JSON object. Version 2, introduced alongside StoreKit 2, restructured everything into a nested JWS (JSON Web Signature) chain: the outer signedPayload decodes to a data object containing a signedTransactionInfo and a signedRenewalInfo — each itself a signed JWT. This layered structure makes it possible to verify every field independently against Apple's certificate chain, which was impractical with v1.
Registering your endpoint: In App Store Connect, navigate to your app → App Information → App Store Server Notifications. You can register one Production URL and one Sandbox URL. The URL must be publicly reachable over HTTPS; Apple does not follow redirects and does not retry indefinitely if your server is down.
Apple's own documentation recommends targeting version 2 for all new apps and treating version 1 as a compatibility shim only. If you're starting fresh or rebuilding your subscription stack, implement version 2 exclusively — the richer payload structure is worth the additional JWT parsing step.
The notification types you must handle
Version 2 defines a notificationType field and an optional subtype field. The combination of both describes the precise lifecycle event. The table below covers the events with the most direct impact on subscription revenue and entitlement state.
| notificationType | Key subtypes | What it means for your backend |
|---|---|---|
SUBSCRIBED |
INITIAL_BUY, RESUBSCRIBE |
Grant full entitlement immediately. RESUBSCRIBE means a lapsed user returned — trigger win-back analytics and suppress any active churn-recovery campaigns for this user. |
DID_RENEW |
BILLING_RECOVERY |
Extend the subscription expiry date in your database. BILLING_RECOVERY means renewal succeeded after a previous billing failure — update any "payment issue" flags you set on the account. |
DID_FAIL_TO_RENEW |
GRACE_PERIOD |
Payment failed. With subtype GRACE_PERIOD, the user still has access during Apple's billing retry window — do not revoke entitlement yet. Without the subtype, revoke immediately and begin your recovery flow. |
GRACE_PERIOD_EXPIRED |
— | Grace period ended without a successful payment. Revoke entitlement now and trigger your win-back email or push sequence. This is the definitive "involuntary churn" signal. |
EXPIRED |
VOLUNTARY, BILLING_RETRY, PRICE_INCREASE, PRODUCT_NOT_FOR_SALE |
Subscription is definitively over. The subtype is the most granular churn attribution signal Apple provides — route each reason to a different analytics bucket. |
DID_CHANGE_RENEWAL_STATUS |
AUTO_RENEW_DISABLED, AUTO_RENEW_ENABLED |
User toggled auto-renew in App Store settings. AUTO_RENEW_DISABLED is a leading churn indicator — the subscription is still active, but consider surfacing a retention prompt before the billing date. |
DID_CHANGE_RENEWAL_PREF |
UPGRADE, DOWNGRADE, CROSSGRADE |
User switched products within your subscription group. Update their entitlement tier accordingly. Downgrades take effect at the next renewal period, not immediately. |
PRICE_INCREASE |
PENDING, ACCEPTED |
Apple notified the user of a price increase. ACCEPTED means the user consented (or Apple auto-consented under qualifying conditions). Log this for pricing analytics and churn forecasting. |
REFUND |
— | Apple granted a refund. Revoke entitlement for the refunded period. The originalTransactionId in signedTransactionInfo identifies exactly which purchase was reversed. |
REVOKED |
— | A family member's access to a Family Sharing subscription was revoked. Revoke entitlement for that specific user account. |
OFFER_REDEEMED |
INITIAL_BUY, RESUBSCRIBE, UPGRADE |
User redeemed a promotional offer or offer code. Useful for measuring campaign conversion without relying on third-party attribution. Pairs with a subsequent SUBSCRIBED or DID_RENEW for the revenue event. |
A common implementation mistake is handling only SUBSCRIBED and EXPIRED and ignoring the grace period pair. That creates a race condition: your backend believes a user is active during the grace period, but if billing ultimately fails and you never handle GRACE_PERIOD_EXPIRED, the account stays "active" indefinitely in your records. RevenueCat's engineering documentation has flagged this as one of the more frequent causes of entitlement drift in production subscription apps — it's easily the highest-impact single fix for most teams building their first notification handler.
Verifying the signed payload
Every notification body is a JWS compact serialization — a dot-separated string of base64url-encoded header, payload, and signature. You must not trust the decoded payload until you have verified the signature against Apple's certificate chain. Skipping verification opens your endpoint to replay attacks or forged entitlement grants from anyone who knows your callback URL.
The verification steps for the outer signedPayload:
- Base64url-decode the header section and parse the
x5carray — an ordered list of X.509 certificates representing the signing chain, leaf first. - Verify that the leaf certificate chains up to Apple's root certificate authority (
Apple Root CA – G3, available from Apple's PKI page). - Confirm the leaf certificate's extensions contain Apple's App Store signing OID (
1.2.840.113635.100.6.11.1). - Verify the JWS signature using the public key extracted from the leaf certificate.
- Decode the payload and confirm the
bundleIdfield matches your app's bundle identifier.
The nested signedTransactionInfo and signedRenewalInfo use the same verification process applied independently. Open-source libraries exist for most server languages — Apple provides sample verification code in its StoreKit 2 documentation, and RevenueCat has published JWT verification helpers for Node.js and Python that implement the full chain-of-trust check.
Sandbox vs. production routing: The environment field in the decoded payload will be Sandbox or Production. Route sandbox notifications to your test database and production notifications to your live database. Mixing the two is a classic integration bug that populates your production subscriber table with phantom test records during development.
One subtlety worth flagging: the certificates embedded in the x5c array inside incoming JWTs are untrusted by definition — anyone can embed any certificate in a JWT header. The trust derives entirely from verifying the chain against a root certificate you obtained independently from Apple's PKI page, ideally pinned in your deployment rather than fetched at request time.
Building a reliable notification handler
Apple's delivery guarantee for server notifications is best-effort, not guaranteed. Notifications can arrive out of order, be duplicated, or in rare cases arrive with significant delay. Your handler must be designed for exactly-once processing semantics even when Apple delivers the same event more than once.
Respond fast, process async. Apple expects your endpoint to return an HTTP 200 within a few seconds. If your endpoint times out or returns a non-2xx status, Apple will retry — but retry frequency and duration are not guaranteed. The reliable pattern is to write the raw signed payload to a queue or append-only database table, return 200 immediately, and process the payload asynchronously in a background worker. Never do database lookups, external API calls, or heavy computation in the synchronous request path.
Use notificationUUID for idempotency. Every v2 notification includes a notificationUUID field — a stable unique identifier for that notification event. Store processed UUIDs in a database table with a unique constraint. Before processing any notification, check whether the UUID already exists. If it does, return 200 and skip processing entirely. This single guard eliminates duplicate-delivery bugs with minimal overhead.
Reconcile with the App Store Server API. Because delivery is best-effort, a periodic reconciliation job is good practice for any subscription app above a few hundred active subscribers. Apple's Get Transaction History and Get All Subscription Statuses endpoints let you pull the authoritative state for any originalTransactionId at any time. If your local subscription state diverges from Apple's API response, the API is always the ground truth.
The App Store Server API also provides a Get Notification History endpoint that returns up to six months of past notifications for your app. This is invaluable for debugging: if a user reports an entitlement issue, you can replay the full notification history for their originalTransactionId rather than relying solely on your own logs.
On the infrastructure side, plan for renewal-time bursts. Annual subscription apps can generate concentrated spikes of DID_RENEW notifications at the anniversary dates of acquisition cohorts. Your endpoint should be stateless, horizontally scalable, and capable of absorbing these bursts without queuing delays that exceed Apple's timeout window.
Connecting the notification stream to pricing and localization decisions
Server notifications are not just infrastructure plumbing — they are the highest-resolution subscription data source available to your backend. The subtype data inside EXPIRED notifications — VOLUNTARY versus BILLING_RETRY versus PRICE_INCREASE — is the most granular churn attribution Apple exposes. No third-party analytics SDK sees this data before you do.
If you're running localized pricing across markets — a practice discussed in detail in our post on why iOS subscription churn is higher in low-PPP markets — server notifications let you segment churn reasons by storefront. A spike in EXPIRED with subtype VOLUNTARY in a specific country storefront after a price adjustment is a far cleaner signal than any dashboard metric: it tells you not just that subscribers left, but that they chose to leave rather than being bounced by payment failure. The storefront field inside signedTransactionInfo carries the two-letter ISO country code for every transaction, giving you the geographic dimension without any additional instrumentation.
PRICE_INCREASE notifications with subtype PENDING give you a lead indicator for upcoming churn from price increase cohorts, before the billing date arrives. If consent rates in a particular market are low (you can infer this from a low ratio of ACCEPTED to PENDING events in the days following a price change), that is advance warning to revisit your pricing for that storefront. This is precisely the kind of market-level signal that informs whether PPP-adjusted price points are set appropriately — a topic covered in our pricing manager and in the broader context of Apple's grandfathering rules.
For teams automating price updates via the App Store Connect API, pairing that automation with a notification handler that captures PRICE_INCREASE → ACCEPTED events closes the feedback loop: you can confirm in near-real time that users in specific storefronts have consented, without scheduling polling jobs against the API. The notification stream is always faster than any pull-based reconciliation approach.
Sources and further reading
- Apple Developer Documentation — App Store Server Notifications overview
- Apple Developer Documentation — notificationType values reference
- Apple Developer Documentation — App Store Server API (Get Transaction History, Get All Subscription Statuses)
- RevenueCat Engineering Blog — handling App Store Server Notifications
- Apple Developer Documentation — StoreKit 2 transaction verification
- Apple PKI — Root Certificate Authority certificates
Share this post
Ready to put this into practice?
AppsOps is the first App Store ops dashboard — PPP-fair pricing for 175 App Store territories, AI metadata localization in 39 languages, AI screenshot localization for 14 Apple device classes, and one-click App Store Connect API push — all from one dashboard, all for $19/month.
Try AppsOps free — no card →