What does the App Store notification EXPIRED mean?

The App Store notification EXPIRED means a subscription has expired and the customer should lose access.

Quick facts#

Notification notificationType EXPIRED
Where App Store Server Notifications v2 (responseBodyV2DecodedPayload)
Sent by The App Store server, to your notification URL
What Apple says The subscription expired. The subtype gives the reason: VOLUNTARY, BILLING_RETRY, PRICE_INCREASE or PRODUCT_NOT_FOR_SALE. With no subtype it expired for another reason.

What triggers it#

  • VOLUNTARY: the customer turned off renewal and the period ended.
  • BILLING_RETRY: billing retry ended without a successful charge.
  • PRICE_INCREASE: the customer did not consent to a price increase that needs consent.
  • PRODUCT_NOT_FOR_SALE: the product was not available for purchase when the renewal was due. No subtype means some other reason.

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 to the paid features and mark the subscription as ended.
  3. Use the subtype to choose the win-back message: a payment problem needs a billing prompt, a voluntary cancel needs an offer.
  4. Keep the customer's data; they can resubscribe, which arrives as SUBSCRIBED with subtype RESUBSCRIBE.
  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","data":{"environment":"Sandbox","bundleId":"com.example.app"}};

function decide(n) {
  if (n.notificationType !== "EXPIRED") return "ignore";
  const why = { VOLUNTARY: "offer a discount", BILLING_RETRY: "ask for a new payment method", PRICE_INCREASE: "explain the new price", PRODUCT_NOT_FOR_SALE: "offer another plan" }[n.subtype];
  return "remove access, " + (why ?? "ask why they left");
}

console.log(decide(notification));

Run-checked: npm run check:snippets runs this snippet with Node and compares its output with remove access, (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. Type overrides set the stored state from the subtype: VOLUNTARY turns renewal off and clears any billing issue, BILLING_RETRY marks a billing issue, PRICE_INCREASE stores the cancel reason PRICE_INCREASE. The EXPIRATION webhook event is recorded by RevenueDot's scheduler once access, grace period included, has ended, with expiration_reason taken from the stored state.

Source#

Checked: 2026-10-03

Edit this page on GitHub ↗ View as Markdown Last updated