Last updated: August 14, 2026 · Last reviewed against Apple docs: May 17, 2026
How to handle Apple subscription webhooks
An Apple subscription webhook is an HTTPS POST request the App Store sends your server when an auto-renewable subscription is created, renewed, refunded, revoked, or changes renewal status. The body carries a signed JWS payload (signedPayload) your handler verifies against Apple's certificate chain, deduplicates, and uses to update subscription state in your app.
This doc covers what a correct handler has to do with that webhook. The rules apply whether you build the handler yourself or use a service that handles webhooks for you.
For the implemented Apple scope, see the subscription handling guide before configuring the integration.
What Apple sends to your webhook endpoint
In App Store Server Notifications V2, the request body is a JSON object with a single field, signedPayload, holding a JWS. The decoded payload (Apple's responseBodyV2DecodedPayload type) carries:
notificationType(the event category)subtype(a more specific qualifier, when present)notificationUUID(a unique ID per notification)datawithsignedTransactionInfoandsignedRenewalInfo, each itself a JWSversionandsignedDate
signedTransactionInfo decodes to a transaction object containing originalTransactionId, productId, expiresDate, appAccountToken, and similar fields. signedRenewalInfo decodes to a renewal object withautoRenewStatus, gracePeriodExpiresDate, and related fields. The outer data object contains the current status. Each JWS layer carries its own Apple signature, so a handler has to verify the outer payload and every nested layer separately.
Do not trust an Apple webhook until you verify the signedPayload JWS and every nested signed JWS inside it against Apple's certificate chain.
What a correct Apple webhook handler does
In short, verify the signedPayload JWS against Apple's certificate chain, deduplicate by notificationUUID, narrow to the notification types the app uses, and upsert one record per originalTransactionId.
- Receive the webhook at an endpoint dedicated to App Store Server Notifications V2.
- Read the raw request body before any framework parsing.
- Decode the JWS in
signedPayloadand verify the signature andx5cchain against a trusted copy of Apple's root CA. Verify the nestedsignedTransactionInfoandsignedRenewalInfoJWS the same way. - Confirm
bundleId,environment, and (when relevant)appAppleIdbelong to the project. - Accept only the notification types the integration actually uses.
- Deduplicate by
notificationUUIDbefore processing. - Link the App Store transaction to a user in your app via
appAccountToken. - Upsert one subscription record keyed by
originalTransactionIdso repeated notifications do not create duplicate rows.
How signedPayload verification works
Apple's signedPayload is a JWS (JSON Web Signature). The header includes an x5c certificate chain. The leaf in the chain is the certificate that signed the payload, the intermediate is Apple's WWDR certificate, and the chain terminates at the Apple Root CA. The leaf's signature has to verify against the payload bytes, every cert in the chain has to verify against its issuer, and the root has to match a copy of Apple's root certificate the handler trusts independently. Apple's docs are explicit that no certificate arriving in the request itself should be trusted on its own.
The same pattern applies to the nested signedTransactionInfo and signedRenewalInfo JWS values inside the decoded payload. Each is independently signed, and each has to be verified before the values inside can be trusted. A handler that verifies only the outer JWS and then trusts the decoded data inside misses the signatures on the transaction and renewal objects. For a detailed walk-through of the chain, see how Apple certificate chain verification works.
Notification types and the role of subtype
Apple sends a notification type per state transition. The types most relevant to subscription handling are:
SUBSCRIBED: initial purchase or resubscription.subtypeisINITIAL_BUYorRESUBSCRIBE.DID_RENEW: the subscription renewed successfully.DID_FAIL_TO_RENEW: a renewal failed. The subscription may be in billing retry or grace, depending on renewal info.DID_CHANGE_RENEWAL_STATUS: the user turned auto-renew on or off.subtypeisAUTO_RENEW_ENABLEDorAUTO_RENEW_DISABLED.DID_CHANGE_RENEWAL_PREF: the user changed which product the next renewal will charge.EXPIRED: the subscription expired.subtypeindicates why (VOLUNTARY,BILLING_RETRY,PRICE_INCREASE, orPRODUCT_NOT_FOR_SALE).GRACE_PERIOD_EXPIRED: the grace window ended without a successful renewal.DID_RECOVER: a previously failing renewal succeeded.REFUND/REFUND_REVERSED: Apple processed a refund, or reversed one.OFFER_REDEEMED: a promotional offer was redeemed.PRICE_INCREASE: a price change applies, withsubtypeindicating the user's consent state.RENEWAL_EXTENDED: a renewal date was extended administratively.
The subtype field qualifies the notificationType and is essential in several cases. SUBSCRIBED.INITIAL_BUY is a new subscriber; SUBSCRIBED.RESUBSCRIBE is a returning one. EXPIRED.VOLUNTARY is a user cancellation taking effect; EXPIRED.BILLING_RETRY is a payment failure timing out. Handlers that branch on notificationType alone collapse these distinctions and lose information.
Apple sends every notification type to the one App Store Server Notifications V2 URL configured for the app. There is no per-type filter at the App Store Connect level; the handler decides which types to act on and which to acknowledge and ignore.
How Apple retries failed webhooks
Apple retries unsuccessful production V2 notifications up to five times, at 1, 12, 24, 48, and 72 hours after the previous unsuccessful attempt. After the fifth retry, Apple stops. See Apple's notification delivery docs for the current schedule.
The sandbox environment does not follow this schedule. Sandbox delivery is best-effort and unsuitable for testing retry behavior end-to-end. Treat Apple webhook delivery as at-least-once in production. The same notification will arrive more than once.
Why idempotency is required
Because delivery is at-least-once, every Apple webhook handler has to be idempotent. The notificationUUID field gives the handler a stable key to deduplicate on.
Use two layers. First, deduplicate by notificationUUID before processing to skip obvious duplicates. Second, upsert the subscription record keyed by originalTransactionId so out-of-order or partial retries land on the same row.
Confirming the webhook belongs to the right app
A valid Apple signature proves the payload came from Apple. It does not prove the payload is for your app. The decoded payload and transaction info include identity fields the handler should match before applying any state change:
bundleId: the app's bundle identifier.environment:ProductionorSandbox.appAppleId: Apple's numeric app identifier (present on production notifications).
Confirm each value matches what the project expects. The check matters most when a team manages more than one app, or production and sandbox traffic flow through the same pipeline. Sandbox notifications applied to production users is the most common variant of this failure. For a step-by-step setup, see how to test Apple subscriptions.
The status integer is the source of truth
The decoded notification's data.status field is an integer with five values:
| Value | Apple state |
|---|---|
| 1 | Active |
| 2 | Expired |
| 3 | Billing retry period |
| 4 | Grace period |
| 5 | Revoked |
That integer is the authoritative subscription state, not the notification type. A DID_FAIL_TO_RENEW notification can carry status 3 (billing retry) or 4 (grace period) depending on the user's plan configuration; the type alone cannot distinguish them. Branch state transitions off status and use the notification type and subtype to log the reason.
Pair status with expiresDate from the transaction and gracePeriodExpiresDate from the renewal info when deciding access. A subscription in grace keeps access through gracePeriodExpiresDate when Apple provides it. Billing retry outside grace does not grant access.
Linking an App Store transaction to a user in your app
Apple identifies subscriptions with originalTransactionId, not a user. To link them to users in your app, generate a UUID at purchase time and pass it to StoreKit as appAccountToken when the user initiates the purchase. Apple includes that UUID in the signed transaction and renewal information, so the handler can look up the right user.
For a transaction that is missing a token, the Set App Account Token API can set or replace it. For auto-renewable subscriptions, the change applies to the current renewal and future renewals, not past transactions. A notification without a valid token cannot be linked to a user from that payload alone.
When to return 2xx and when to return non-2xx
A 2xx response tells Apple the notification was accepted. A non-2xx response makes Apple retry the delivery.
Return 2xx on success. For failures, the right code depends on whether retrying would help. If the same payload would fail the same way (a permanent failure), return 2xx so Apple stops trying. If the cause is temporary (a transient failure), return non-2xx so Apple tries again.
Return 2xx for:
- notifications whose type the handler does not process
- already-processed duplicate notifications (matched by
notificationUUID) - notifications missing
appAccountToken(the value will never appear on retry) - notifications whose payload cannot be mapped to a subscription (the payload will not change on retry)
- notifications whose JWS or chain verification fails because the payload itself is wrong (a forged or corrupt payload will not become valid on retry)
Return non-2xx for:
- transient certificate fetch failures or chain validation issues where a retry may succeed
- database or infrastructure failures during processing
- unhandled exceptions in the handler itself
After the fifth retry at 72 hours, Apple stops regardless of the response code. Returning 5xx for a permanent failure spends those 72 hours retrying an event that can never succeed, raising your error rate and hiding actual transient failures.
Common Apple webhook mistakes
- Parsing the payload before verifying the JWS. A handler that decodes
signedPayload, acts on the data, and verifies later has already trusted untrusted data. - Verifying only the outer JWS.
signedTransactionInfoandsignedRenewalInfoare independently signed JWS values inside the decoded payload. Each has to be verified before the values inside can be trusted. - Trusting
notificationTypealone. Thestatusinteger in the outerdataobject is the authoritative subscription state. Differentstatusvalues can ride on the same notification type. - Mixing production and sandbox. A handler that does not check the
environmentfield can apply sandbox state changes to production users, or vice versa. - Treating delivery as exactly-once. Apple retries unsuccessful deliveries; the same
notificationUUIDwill arrive more than once. - No
appAccountTokenassociated with the transaction. A valid notification with noappAccountTokencannot be linked to a user from that payload alone. Apple's Set App Account Token API can add one to supported current and future transactions, but it does not change past transactions. - Skipping
subtype. Several notification types carry materially different meanings depending onsubtype(SUBSCRIBED.INITIAL_BUYvs.SUBSCRIBED.RESUBSCRIBE,EXPIRED.VOLUNTARYvs.EXPIRED.BILLING_RETRY).
How this fits across providers
If your app also takes payments through Stripe or Google Play, the same problems (signature verification, idempotency, retries, ordering, user mapping) show up with Stripe subscription webhooks and Google Play Real-time Developer Notifications. The mechanics differ (signing schemes, retry schedules, event names). See how subscription access works for how apps typically check whether a user is subscribed once the webhooks have been processed.
Frequently asked questions
SubTru runs the Apple side of your subscription backend. Point your App Store Server Notifications V2 URL at SubTru, and pass an appAccountToken UUID to StoreKit at purchase so each transaction carries it back. SubTru verifies the signedPayload JWS against Apple's certificate chain, verifies the nested signedTransactionInfo and signedRenewalInfo, deduplicates by notificationUUID, handles retries, and normalizes the status integer into a single subscription state. SubTru's access check endpoint returns that state over HTTP, with the same contract whether you use Apple alone or all three providers. Your Apple Developer account is still yours. SubTru receives the notifications Apple sends and never sits in front of your account.