What does the App Store notification EXPIRED with subtype BILLING_RETRY mean?

The App Store subtype BILLING_RETRY on EXPIRED means the App Store gave up charging the customer and the subscription ended.

Quick facts#

Notification notificationType EXPIRED, subtype BILLING_RETRY
Where App Store Server Notifications v2 (responseBodyV2DecodedPayload)
Sent by The App Store server, to your notification URL
What Apple says The subscription expired because it failed to renew before the billing retry period ended.

What triggers it#

  • A renewal failed, and Apple retried billing for up to 60 days without success.
  • Any grace period had already ended (you saw GRACE_PERIOD_EXPIRED) or the app had no grace period.
  • The customer did not fix their payment method, and did not cancel.

What your server should do#

  1. Verify the signedPayload with Apple's App Store Server Library (SignedDataVerifier.verifyAndDecodeNotification) before you act on anything in it.
  2. Remove access and mark the churn as involuntary.
  3. Send a win-back message that asks for a new payment method and links to the customer's payment settings.
  4. Report it separately from voluntary churn in your metrics.
  5. Answer HTTP 200 (any code from 200 to 206) once you have stored the notification. Apple retries other answers five times, at 1, 12, 24, 48 and 72 hours after the previous attempt, so make your handler safe to run twice (the payload carries a notificationUUID).

Example#

JavaScript
// Run on the decoded payload, after you verified Apple's signature.
const notification = {"notificationType":"EXPIRED","subtype":"BILLING_RETRY","data":{"environment":"Sandbox","bundleId":"com.example.app"}};

function decide(n) {
  const gaveUp = n.notificationType === "EXPIRED" && n.subtype === "BILLING_RETRY";
  return gaveUp ? "remove access, mark involuntary churn, ask for a new payment method" : "ignore";
}

console.log(decide(notification));

Run-checked: npm run check:snippets runs this snippet with Node and compares its output with involuntary churn (checked 2026-10-03).

How RevenueDot handles it#

RevenueDot verifies Apple's signature, checks the bundle ID, stores the notification and answers 200. A purchase RevenueDot has not seen is applied only when the app's Track new purchases from server-to-server notifications setting is on. The type override marks a billing issue on the stored purchase. The scheduler records EXPIRATION with expiration_reason: BILLING_ERROR, which is how RevenueDot (like RevenueCat) tells involuntary churn from a voluntary cancel.

Source#

Checked: 2026-10-03

Edit this page on GitHub ↗ View as Markdown Last updated