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#
- Verify the
signedPayloadwith Apple's App Store Server Library (SignedDataVerifier.verifyAndDecodeNotification) before you act on anything in it. - Remove access to the paid features and mark the subscription as ended.
- Use the subtype to choose the win-back message: a payment problem needs a billing prompt, a voluntary cancel needs an offer.
- Keep the customer's data; they can resubscribe, which arrives as
SUBSCRIBEDwith subtypeRESUBSCRIBE. - 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#
// 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.
Related#
- What does the App Store notification EXPIRED with subtype VOLUNTARY mean?
- What does the App Store notification EXPIRED with subtype BILLING_RETRY mean?
- What does the App Store notification SUBSCRIBED mean?
- Which webhook events does RevenueDot send?
- Set up App Store notifications in RevenueDot
Source#
- Apple: notificationType
- Apple: Responding to App Store Server Notifications
- Apple: app-store-server-library-node (SignedDataVerifier)
- RevenueDot server: stores/apple/notifications.ts (how the notification is applied)
- RevenueDot server: stores/apple/map.ts (how the transaction becomes a stored purchase)
- RevenueDot core: events.ts (how a change becomes a webhook event)
- RevenueDot server: services/tick.ts (the scheduled EXPIRATION)
Checked: 2026-10-03