App Store Server API: verifying iOS transactions and checking subscription status server-side
A practical guide to the App Store Server API — how it differs from the App Store Connect API, how to authenticate, which endpoints matter for subscription entitlement management, and how to wire it into a reliable three-layer validation architecture.
Apple's developer ecosystem includes two server-side APIs with confusingly similar names. The App Store Connect API is a management API: it lets you read and write data in App Store Connect — pricing tiers, metadata, builds, users. The App Store Server API is a transaction API: it lets your backend server look up the real-time status of a specific customer's subscription, retrieve their full transaction history, or extend a renewal date. They share the same JWT authentication scheme but call completely different base URLs and serve entirely different purposes.
Understanding the App Store Server API is increasingly important as Apple pushes developers away from the legacy verifyReceipt endpoint (which Apple has signalled will eventually be deprecated) and toward a model where server-side subscription management is a first-class concern. This guide covers what the API does, how authentication works, which endpoints matter for subscription apps, and how to wire it into a reliable entitlement system. If you haven't generated an App Store Connect API key yet, the 5-minute API key setup guide covers that first step — you'll use the same key pair here.
What the App Store Server API does (and what it doesn't)
At its core, the App Store Server API answers one class of question: what has this specific customer purchased, and is it still valid? You pass Apple an originalTransactionId — the stable identifier assigned to the first transaction of a subscription — and Apple returns signed, verifiable data about the current state of that subscription.
What the API doesn't do: it does not give you aggregate revenue figures, sales trends, or anything about your app's performance as a whole. That is the domain of the App Store Connect API and the Sales and Trends reports. The Server API is deliberately narrow — its only concern is the transactional state of individual customers.
The production endpoint base URL is https://api.storekit.itunes.apple.com. The sandbox equivalent (for testing with sandbox Apple IDs) is https://api.storekit-sandbox.itunes.apple.com. It's a common mistake to test against the production URL and receive "transaction not found" errors — make sure your environment maps correctly.
Why server-side verification still matters with StoreKit 2
StoreKit 2 introduced client-side signed transactions — JWS tokens that your app can verify locally against Apple's public key without a server round-trip. This leads some developers to skip the server entirely. For simple apps, that works. For subscription businesses, it leaves meaningful gaps:
- The client can be tampered with. Jailbroken devices and certificate injection attacks can spoof StoreKit responses. Gating your backend API on client assertions means a single bypass defeats your entire paywall — something especially relevant for apps with meaningful server-side state.
- Your server often acts without a live client session. App Store Server Notifications processing, scheduled jobs that sync entitlement records, and support tooling that resolves a customer complaint all need to know subscription status without a live StoreKit session.
- Historical lookup is server-only. The transaction history endpoint is the only way to retrieve a complete, auditable record of everything a customer has purchased — useful for migrations, investigations, and support.
The right architecture combines both layers. Use StoreKit 2 on the client for instant, low-latency access gating. Use the App Store Server API on your backend to independently validate entitlements, power support tooling, and handle any flow that runs outside an active user session. Neither layer alone is sufficient for a serious subscription app.
Authentication: the same JWT scheme as App Store Connect API
Authentication uses the same JSON Web Token approach as the App Store Connect API — if you've already set up a key for pricing automation or data pulls, you can reuse it here with no changes to the key itself. The difference is in the JWT payload: the aud (audience) claim must be "appstoreconnect-v1", and you must add a bid claim containing the bundle ID of the specific app you're querying.
A minimal Python example using PyJWT:
import jwt, time
payload = {
"iss": ISSUER_ID, # from App Store Connect → Users and Access → Keys
"iat": int(time.time()),
"exp": int(time.time()) + 3600,
"aud": "appstoreconnect-v1",
"bid": "com.yourcompany.yourapp", # your app's bundle ID
}
token = jwt.encode(
payload,
PRIVATE_KEY_PEM, # the .p8 file contents as a string
algorithm="ES256",
headers={"kid": KEY_ID} # from the same page as ISSUER_ID
)
# Use as: Authorization: Bearer {token}
Tokens are valid for up to 3,600 seconds. Cache and reuse the same token until it's close to expiry — generating a new one per request is unnecessary overhead.
The five endpoints subscription developers use most
Apple's App Store Server API covers a range of use cases. The table below focuses on the endpoints that matter for subscription entitlement management:
| Endpoint | Method | What it returns | Primary use case |
|---|---|---|---|
/inApps/v1/subscriptions/{originalTransactionId} |
GET | Active and most-recent status for all subscriptions under this original transaction | Real-time entitlement check; support ticket resolution |
/inApps/v2/history/{originalTransactionId} |
GET | Paginated list of all signed transactions (newest first) | Entitlement migration; purchase history audit |
/inApps/v1/transactions/{transactionId} |
GET | A single signed transaction by its transaction ID | Validating a specific purchase before granting access |
/inApps/v1/subscriptions/extend/{originalTransactionId} |
PUT | Confirmation and updated expiry date | Compensating users for outages; retention tooling |
/inApps/v1/notifications/test |
POST | A test notification dispatched to your registered Server Notifications URL | Testing your notification handler without a real purchase event |
For day-to-day subscription management, /inApps/v1/subscriptions/{originalTransactionId} is the workhorse. It returns a data array — one entry per subscription group — each containing the latest renewal information and a status integer you can act on directly.
Decoding the subscription status response
The subscription status endpoint wraps its payload in signed JWS tokens — the same format StoreKit 2 uses on the client — to make responses tamper-evident. Each lastTransactions entry contains a status integer and two JWS strings: signedTransactionInfo and signedRenewalInfo. Decode them against Apple's public keys (available at https://appleid.apple.com/auth/keys) to get the underlying JSON.
The status codes map as follows:
- 1 — Active: The subscription is current. Grant full access.
- 2 — Expired: The subscription has lapsed. Move the user to the free tier.
- 3 — Billing retry: Apple is retrying the renewal charge (up to 60 days). You can optionally maintain access during this window; the choice is yours unless you've enabled Grace Period, which makes the decision for you.
- 4 — Grace period: Your app has enabled Billing Grace Period and Apple is retrying. Apple's guidance — supported by internal data they've shared at WWDC — is to maintain full access during this state to avoid punishing users for bank issues outside their control.
- 5 — Revoked: The subscription was refunded or a Family Sharing member left the group. Revoke access immediately.
Status 3 and 4 deserve close attention. Research from RevenueCat has shown that a significant share of "churned" subscribers are actually in billing retry — their cards declined but they haven't actively cancelled. Maintaining access during status 4 (Grace Period) and optionally status 3 can recover a meaningful fraction of these users before they even realize there was a problem. The grace period and billing retry deep-dive covers the setup and the revenue impact in detail.
On JWS decoding: Don't implement JWS certificate chain verification from scratch. Apple publishes official server libraries for Swift, Java, Python, Node.js, and Go at developer.apple.com/documentation/appstoreserverapi. These libraries handle certificate chain validation against Apple's root CA automatically — a step that's easy to skip and hard to get right manually.
Paginating transaction history
The /inApps/v2/history endpoint returns transactions newest-first, with up to 20 records per page. For long-tenured subscribers — or apps migrating away from legacy receipt validation — there may be dozens or hundreds of renewal entries. The response includes a hasMore boolean and a revision cursor. Pass revision as a query parameter on subsequent requests to fetch the next page.
Useful filter parameters:
productType=AUTO_RENEWABLE_SUBSCRIPTION— exclude consumables and non-consumables from subscription history queriesinAppOwnershipType=PURCHASED— separate a user's own purchases from Family Sharing entitlements they received from a family membersort=ASCENDING— useful when reconstructing a chronological event timeline for a support investigation
If you are migrating from the legacy verifyReceipt endpoint, the history endpoint is the authoritative replacement. The migration pattern is: for each subscriber record, look up their originalTransactionId, fetch their complete history, find the latest renewal entry, and use that as the new canonical entitlement anchor.
Wiring the API into a reliable entitlement system
The recommended architecture for production subscription apps treats the App Store Server API as one layer of a three-part entitlement stack:
- StoreKit 2 on the client (speed):
Transaction.currentEntitlementsgives you a signed, locally verifiable entitlement list within milliseconds of app launch. Use this for the first access decision — it's fast enough to avoid any perceptible gate. - App Store Server API on your backend (trust): After the client sends you the
originalTransactionId, your server independently calls/inApps/v1/subscriptions/{id}to confirm the status. Cache the result for 5–10 minutes to avoid unnecessary API calls, and refresh it when you receive a Server Notification event. - App Store Server Notifications for real-time events: Server Notifications v2 push events — renewals, cancellations, refunds, billing recovery — to your endpoint the instant they happen. Handling these correctly keeps your entitlement database in sync without polling.
This layered approach means your access gates are fast (StoreKit 2), your backend records are trustworthy (server API validation), and your database stays current (event-driven Server Notifications). Each layer handles the failure modes the other two can't catch.
Rate limits and common errors
Apple does not publish specific numeric rate limits for the App Store Server API, but the API returns standard HTTP 429 responses when volume thresholds are exceeded. The right operational pattern is: query on-demand (not continuously), cache responses aggressively, and rely on Server Notifications for real-time updates rather than polling the status endpoint on a timer. If you are operating at significant scale, third-party subscription platforms such as RevenueCat, Adapty, and Purchasely maintain their own managed connections to Apple's API and surface a normalized status endpoint to your backend — offloading rate limit management entirely.
The error codes most likely to appear in early integrations:
- 4290000 — Rate limit exceeded: Respect the
Retry-Afterresponse header and implement exponential backoff. - 4300007 — Original transaction ID not found: Usually a production vs. sandbox mismatch — confirm your base URL matches the environment the transaction was created in.
- 4300008 — Invalid original transaction ID: The field must be a numeric string. Check that you're not accidentally passing a product ID or App Store receipt field in its place.
- 401 Unauthorized: Expired JWT, wrong audience claim, or the key has been revoked in App Store Connect. Regenerate the token first, then verify the key status in Users and Access.
Sources and further reading
- Apple Developer Documentation: App Store Server API reference
- Apple Developer: Get All Subscription Statuses endpoint
- Apple Developer: Get Transaction History endpoint
- WWDC 2022 Session 10040: Meet the App Store Server Library
- RevenueCat: iOS in-app subscriptions guide
- Apple Developer Documentation: App Store Server Notifications
Share this post
Ready to put this into practice?
AppsOps is the first App Store ops dashboard — PPP-fair pricing for 175 App Store territories, AI metadata localization in 39 languages, AI screenshot localization for 14 Apple device classes, and one-click App Store Connect API push — all from one dashboard, all for $19/month.
Try AppsOps free — no card →