Highlight
This session covers three core duties of the App Store server APIs: managing in-app purchases, signing requests, and taking part in refund decisions. The presenter, Riyaz, walks through this year’s substantive updates one by one.
Core content
Server engineers behind subscription apps have all hit this snag: a user buys both a monthly news subscription and an annual sports subscription inside the same app. Each subscription has its own transactionId and originalTransactionId, and to tell on your own backend that “these two subscriptions belong to the same customer account,” you have to keep an extra mapping table. Family Sharing transactions also lack appAccountToken. If the user buys outside the app — by redeeming an offer code, or via a promoted purchase straight from the App Store — you cannot set appAccountToken through StoreKit at all.
Apple now ships a one-stop identifier: appTransactionId. It is globally unique per Apple Account per app, lives on the AppTransaction, JWSTransaction, and JWSRenewalInfo objects, and stays stable across redownload, refund, repurchase, and storefront switches (08:30). Under Family Sharing, each family member gets their own appTransactionId, and family-shared transactions can use it for linking too — something appAccountToken cannot do. Together with the new Set App Account Token endpoint, the developer can also set or update the token on transactions where it was missing at the time of purchase.
Details
Roles of the three transaction identifiers (06:52):
transactionId: identifies a single purchase event (purchase, restore, and renewal each have their own).originalTransactionId: the lifecycle primary key for an auto-renewable subscription, unchanged across renewals.appAccountToken: a UUID generated by the developer to link an App Store transaction to the developer’s own user account.appTransactionId: the new field this year, unique per Apple Account per app, stable across all transaction objects.
Set App Account Token endpoint (05:11). Pass originalTransactionId in the path and a new UUID in the body to overwrite the appAccountToken on that transaction (or on the latest transaction of that subscription). For an auto-renewable subscription, the new token carries through to later renewals, upgrades, and downgrades. It works for all product types and also fixes account-attribution drift after an account change.
Get App Transaction Info endpoint (10:25). For the first time, the server can fetch AppTransaction directly without going through the device. Pass originalTransactionId, transactionId, or appTransactionId in the path; the response is the JWS signedAppTransactionInfo. Note: this endpoint does not return device-verification fields — those still come from the device.
Unified JWS signing format (12:17). All signing scenarios — promotional offer, introductory offer, and the Advanced Commerce API — now use JWS. Here is how to generate a promotional offer signature with the App Store Server Library (Java) (13:48):
PromotionalOfferV2SignatureCreator signatureCreator =
new PromotionalOfferV2SignatureCreator(privateKey, keyId, issuerId, bundleId);
String transactionId = "..."; // Optional; when specified, this offer is limited to that customer
String productId = "com.example.subscription";
String offerId = "PROMO_OFFER_2025";
String signedJws = signatureCreator.createSignature(productId, offerId, transactionId);
Key points:
PromotionalOfferV2SignatureCreatoris built from the private key, keyId, issuerId, and bundleId downloaded from App Store Connect; these four values keep anyone outside the developer from issuing promo offers.transactionIdis optional. Pass any transaction identifier belonging to that customer (includingappTransactionId) to lock the offer to that customer; omit it to leave the offer open to anyone.- The new signature takes fewer inputs than the old one — the previous promotional offer signature carried more fields, while this year’s V2 trims them down once it moves to JWS.
createSignaturereturns the JWS string you can hand straight to StoreKit.
Send Consumption Information V2 (15:55). The refund-decision endpoint drops from 12 fields to 5 (3 required + 2 optional):
customerConsented(required): whether the user agreed to send consumption data to the App Store. If false, the request is rejected; do not call this endpoint without consent.sampleContentProvided(required): whether sample content was offered before the purchase.deliveryStatus(required): whether the content was delivered, eitherDELIVEREDor one of the matchingUNDELIVEREDstates.refundPreference(optional): addsGRANT_PRORATED, alongside full and no refund — three options total.consumptionPercentage(optional, but required when pickingGRANT_PRORATEDfor a consumable, non-consumable, or non-renewing subscription): expressed in milli-percent, where25000means 25%.
New fields on REFUND notifications (20:00):
refundPercentage: the actual refund ratio — for example, 75 means 75% was refunded.revocationType:REFUND_FULL,REFUND_PRORATED, orFAMILY_REVOKE. WithREFUND_PRORATED, revoke only the matching share of the content (for example, deduct virtual currency proportionally);REFUND_FULLandFAMILY_REVOKErevoke everything immediately. For an auto-renewable subscription, treat a prorated refund the same way as a full refund — check the current subscription state and act on it.
The V1 endpoint is deprecated but still accepts requests; V2 and the Get App Transaction Info endpoint both ship later this year (18:48).
Key takeaways
1. Use appTransactionId as the primary index for customer-account linking
Why it matters: it gives a stable link to the same Apple Account per app across products (multiple subscriptions plus in-app purchases), across Family Sharing, and across storefronts. originalTransactionId covers only a single subscription group, and appAccountToken is simply missing on family-shared transactions.
How to start: at app launch, read AppTransaction.appTransactionId and send it to the server to bind to the customer’s account; later, when a JWSTransaction or JWSRenewalInfo arrives, look it up by the same field. Once the Get App Transaction Info endpoint ships, the server can query it directly without waiting for the app to report.
2. Use the Set App Account Token endpoint to fix attribution for out-of-app purchases
Why it matters: when a user redeems an offer code or makes a promoted purchase from the App Store, StoreKit is not in the loop, so these transactions used to come without appAccountToken. Account merges and ownership changes are common too.
How to start: when a new transaction arrives for an originalTransactionId, if appAccountToken is missing or does not match the current account, call Set App Account Token to fix it. For an auto-renewable subscription, set it once — the token carries through later renewals, upgrades, and downgrades.
3. Move to the V2 refund endpoint and handle prorated cases
Why it matters: V2 cuts the field count from 12 to 5, so integration cost drops sharply. GRANT_PRORATED plus refundPercentage lets you revoke consumed content precisely (for example, deduct virtual currency proportionally), keeping the user experience intact while cutting accounting losses.
How to start: call Send Consumption Information V2 through the App Store Server Library, fill in the three required fields, and pass a prorated preference where it fits. In the REFUND notification, read revocationType and refundPercentage, and add a separate prorated revocation branch for REFUND_PRORATED.
4. Wire refund notifications for every product type
Why it matters: V2 brings non-consumable and non-renewing subscription products into scope. Skipping these types means the server does nothing when those refunds happen, leaving books and entitlements out of sync.
How to start: audit the CONSUMPTION_REQUEST and REFUND branches and make sure every product type has a matching revocation path. REFUND_DECLINED needs no action, but log it for triage.
5. Use the unified JWS signature to simplify the signing service
Why it matters: once promotional offer, introductory offer, and Advanced Commerce API signatures share one format, the server only has to maintain a single signing path.
How to start: use the helpers from the App Store Server Library — for example, PromotionalOfferV2SignatureCreator — to produce JWS, instead of building the signature by hand. The private key, keyId, and issuerId come from App Store Connect; the bundleId comes from the project configuration.
Related sessions
- What’s new in StoreKit and In-App Purchase — this year’s overall StoreKit and in-app purchase updates; the client-side counterparts to this session land here.
- Explore App Store server APIs for In-App Purchase — the WWDC24 primer on the server APIs, useful background reading for this session.
- Discover Apple-Hosted Background Assets — a separate track for delivering server-side operational assets; combine it with purchase data to tailor the experience.
- Enhance child safety with PermissionKit — compliance support for purchases involving minors, often paired with refund handling.
Comments
GitHub Issues · utterances