CHURN METRICSAPP STORE

Tracking churn with App Store Server Notifications V2

How to measure iOS subscription churn with App Store Server Notifications V2: setup, what each notification means, billing vs voluntary churn, and gaps.

Published 5 min read

App Store Connect gives you sales and subscription reports, but if you want churn numbers you can trust and act on (who left, why, and who is about to), you need your own event log. On iOS, that log comes from App Store Server Notifications V2: Apple’s server calls yours every time a subscription changes state.

This guide shows how to set them up reliably, how each notification maps to a churn metric, and how to fill the gaps. It is based on Apple’s documentation: Enabling App Store Server Notifications, Responding to App Store Server Notifications, the notificationType reference and Get Notification History (all accessed November 2026).

Setting up the endpoint

Apple’s requirements are short:

  1. An HTTPS URL on your server, with TLS 1.2 or later. If you specify a port, it must be 443 or 1024 and above.
  2. Choose version 2 for the URL in App Store Connect. Apple recommends V2 for all new implementations. You can set a separate sandbox URL, or reuse the same one.
  3. If your firewall uses an allow list, add 17.0.0.0/8, which covers both sandbox and production.
  4. Return HTTP 200 to 206 when you have stored the notification. Any other code counts as a failure.
  5. Test with the Request a Test Notification endpoint, which sends a TEST notification, then check the result with Get Test Notification Status.

Each notification arrives as a signedPayload in JWS format. Verify the signature before trusting anything in it, then decode the payload.

Delivery, retries and duplicates

Things go wrong: deploys, outages, timeouts. Apple’s documentation describes what happens then.

  • Retries. For V2, if your server does not return a success code, Apple retries five times, at 1, 12, 24, 48 and 72 hours after the previous attempt. Retries happen in production only; in sandbox Apple sends each notification once.
  • Duplicates. Each payload has a notificationUUID, which Apple says to use to identify duplicates. Store it with a unique constraint and ignore repeats.
  • History. Get Notification History returns the notifications Apple attempted to send over the past 180 days in production (30 days in sandbox), 20 records per page, optionally filtered by type, subtype or transaction. Run it after any outage.
  • Current truth. History records reflect the state at the time of sending. For the current state of a subscription, Apple points to Get All Subscription Statuses.

A good pattern: store the raw notification first, return 200, then process it from a queue. Never do slow work before answering Apple.

What is in a notification

The decoded payload has a notificationType, an optional subtype, a signedDate and a data object. For subscriptions, data contains:

  • signedTransactionInfo: the transaction (product, dates, offer used, revocation if any).
  • signedRenewalInfo: what will happen at the next renewal, including autoRenewStatus, expirationIntent, isInBillingRetryPeriod, gracePeriodExpiresDate and eligibleWinBackOfferIds.
  • status: the subscription status as of signedDate: 1 active, 2 expired, 3 in billing retry, 4 in Billing Grace Period, 5 revoked.

To connect a subscription to one of your users, set an appAccountToken at purchase. Apple returns it in the transaction and renewal info. Without it, you only have Apple’s transaction IDs.

Mapping notifications to churn metrics

Here is how Apple’s notification types translate into the numbers most teams track. Descriptions follow Apple’s notificationType table.

Metric Notification Subtype
New subscriber SUBSCRIBED INITIAL_BUY
Returning subscriber (including win-back offers) SUBSCRIBED RESUBSCRIBE
Renewal DID_RENEW none
Cancellation intent (renewal turned off) DID_CHANGE_RENEWAL_STATUS AUTO_RENEW_DISABLED
Cancellation reversed DID_CHANGE_RENEWAL_STATUS AUTO_RENEW_ENABLED
Billing failure DID_FAIL_TO_RENEW GRACE_PERIOD or none
Billing recovered DID_RENEW BILLING_RECOVERY
Voluntary churn EXPIRED VOLUNTARY
Involuntary (billing) churn EXPIRED BILLING_RETRY
Price-increase churn EXPIRED PRICE_INCREASE
Product removed from sale EXPIRED PRODUCT_NOT_FOR_SALE
Refund REFUND none, or SEATS for multiseat
Downgrade scheduled DID_CHANGE_RENEWAL_PREF DOWNGRADE

The expirationIntent in the renewal info gives the same split as a number: 1 customer canceled, 2 billing error, 3 no consent to a price increase or offer conversion, 4 product not available, 5 other (reference).

Count churn at expiry, not at cancellation

On the App Store, a cancellation only stops renewal. The customer keeps access until the period ends, and can turn renewal back on. So:

  • Use AUTO_RENEW_DISABLED as a leading indicator: subscriptions at risk this period.
  • Use EXPIRED as the churn event: subscriptions that actually ended.

If you count churn on AUTO_RENEW_DISABLED, you will overstate it and miss every customer who changed their mind.

Billing churn has a long tail

Apple’s reference says that after a failed renewal, the App Store keeps retrying billing for 60 days, or until the customer fixes their billing or cancels. With Billing Grace Period enabled, the customer keeps access during the grace period; GRACE_PERIOD_EXPIRED marks its end while retries continue. A subscription in billing retry is neither active nor churned yet. Report it as its own bucket, and only count it as churn when EXPIRED with BILLING_RETRY arrives.

Note also recentSubscriptionStartDate in the renewal info: Apple computes it ignoring lapses of 60 days or fewer, which is handy for tenure and cohorts.

A simple churn report from notifications

With every notification stored, a monthly report is a handful of queries:

  1. Active at start of month: subscriptions whose latest status before the month was active, in grace period or in billing retry (decide once whether retry counts as active and stick to it).
  2. Churned: EXPIRED notifications in the month, split by subtype.
  3. Voluntary churn rate: EXPIRED/VOLUNTARY divided by active at start.
  4. Involuntary churn rate: EXPIRED/BILLING_RETRY divided by active at start.
  5. Billing recovery: DID_RENEW/BILLING_RECOVERY divided by DID_FAIL_TO_RENEW over the same cohort.
  6. At risk now: subscriptions whose latest renewal info has auto-renew off and a future expiry.
  7. Reactivations: SUBSCRIBED/RESUBSCRIBE.

For the general formulas and the traps (mixing monthly and annual plans, counting trials), see how to calculate churn rate.

Common gaps

  • Only listening to V1, or only production. Test V2 in sandbox before switching production; Apple recommends exactly that order.
  • No reconciliation job. Even with retries, run Get Notification History periodically and after incidents.
  • Ignoring Family Sharing and groups. SUBSCRIBED and REVOKE also fire for family members and group seats. Decide whether those count as subscribers in your metrics.
  • Treating refunds as cancellations. A REFUND ends access and returns money; report it separately.
  • No appAccountToken. You get clean App Store numbers but cannot join them with usage, plan or acquisition data, which is where churn explanations come from.

Doing this with NoChurn

NoChurn gives you a URL to paste as your App Store Server Notifications endpoint. It verifies and deduplicates each notification, keeps the timeline per customer (matched by your appAccountToken), separates voluntary, billing and price-increase churn, and flags auto-renew turned off as an early risk signal in the churn score from 0 to 100. You can add your own attributes by API to see what drives churn. Apple keeps billing, retries and the cancel button. See NoChurn + App Store.

PROMPT

Read https://www.nochurn.sh/llms.txt,
then follow https://www.nochurn.sh/docs/quickstart/
to add NoChurn to this app. Use a test key
(nc_test_…) until I say we go live.

Stop leaking revenue.

  • Free until $300 MRR
  • No card needed
  • Live in 10 minutes
  • Never a cut of what we recover