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

The App Store subtype VOLUNTARY on EXPIRED means the subscription ended because the customer chose to stop it.

Quick facts#

Notification notificationType EXPIRED, subtype VOLUNTARY
Where App Store Server Notifications v2 (responseBodyV2DecodedPayload)
Sent by The App Store server, to your notification URL
What Apple says The subscription expired after the customer turned off subscription renewal.

What triggers it#

  • The customer cancelled earlier (you saw DID_CHANGE_RENEWAL_STATUS with AUTO_RENEW_DISABLED) and the paid period has now run out.
  • No payment problem is involved, so it is not billing related.
  • The customer can come back at any time, which arrives as SUBSCRIBED with subtype RESUBSCRIBE.

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 customer as churned by choice.
  3. Send your win-back message or an offer, for example a promotional or win-back offer.
  4. Do not ask for a payment update: their payment method is not the reason.
  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":"VOLUNTARY","data":{"environment":"Sandbox","bundleId":"com.example.app"}};

function decide(n) {
  const gone = n.notificationType === "EXPIRED" && n.subtype === "VOLUNTARY";
  return gone ? "remove access, mark churned by choice, send win-back" : "ignore";
}

console.log(decide(notification));

Run-checked: npm run check:snippets runs this snippet with Node and compares its output with churned by choice (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 stores renewal off and no billing issue. The scheduler records EXPIRATION with expiration_reason: UNSUBSCRIBE, because the earlier CANCELLATION already carried that reason.

Source#

Checked: 2026-10-03

Edit this page on GitHub ↗ View as Markdown Last updated