AnythinkPay verifies Apple in-app purchase transactions server-side and folds them into the same subscription and entitlement model as Stripe — your app doesn't need its own receipt-verification code or a separate Apple entitlement check.
For the shared concepts (plans, subscription statuses, entitlement), see Create plans and manage subscriptions and Offer codes, free trials and entitlements.
You need, from App Store Connect:
| Value | Where to find it |
|---|---|
| Bundle ID | Your app's bundle identifier |
| Environment | sandbox for TestFlight and simulator testing, production for release |
| Issuer ID | App Store Connect › Users and Access › Integrations › App Store Connect API |
| Key ID and private key (.p8) | The same Integrations page — generate an App Store Connect API key with at least the App Manager role |
Anythink also needs a subscription group and product created in App Store Connect that mirrors your Anythink plan (see Create plans and manage subscriptions for the plan side).
.p8 contents into Private key.anythink fetch /integrations/anythinkpay/apple-iap/credentials
anythink fetch /integrations/anythinkpay/apple-iap/credentials --method PUT --body \
'{"bundle_id":"com.example.app","environment":"sandbox","asc_issuer_id":"...","asc_key_id":"...","asc_private_key_pem":"-----BEGIN PRIVATE KEY-----..."}'
anythink fetch /integrations/anythinkpay/apple-iap/test-notification --method POST
GET credentials returns has_private_key and is_configured booleans rather than the key itself — the private key is encrypted at rest and never read back. Sending null for asc_private_key_pem on a PUT leaves the stored key untouched (for editing just the issuer/key id); sending an empty string clears it.
Note: Apple IAP credential fields include a private key, which shouldn't be typed into a chat prompt. Set credentials through the dashboard or CLI directly rather than asking an assistant to enter them for you.
Your app calls one endpoint after a StoreKit 2 purchase or restore completes:
POST /org/{orgId}/integrations/anythinkpay/subscriptions/apple/verify
Authorization: Bearer <user's Anythink token>
Content-Type: application/json
{ "signed_transaction": "<JWS from StoreKit 2 Transaction.currentEntitlements or a purchase result>" }
or, for a restore where you only have Apple's identifier:
{ "original_transaction_id": "1000000123456789" }
signed_transaction path verifies the JWS signature directly and needs only your bundle id — no App Store Connect API credentials required.original_transaction_id path looks the transaction up via the App Store Server API, so it does need the Issuer ID, Key ID and private key configured.A successful verify returns:
{
"subscription_id": "b6b8...",
"bound_user_id": 61,
"provider": "apple",
"product_id": "com.example.app.pro_monthly",
"plan_name": "Pro monthly",
"status": "trialing",
"expires_at": "2026-10-06T17:49:06Z",
"has_access": true,
"matched_known_plan": true
}
matched_known_plan is false when the product id in the transaction doesn't match any plan's Apple product ID — the subscription still records, but your app should treat it as an unrecognised product rather than silently granting access to a feature.
Verify links the transaction to the calling user's Anythink account, not to any identifier embedded in the App Store receipt:
SubscriptionUser link from the calling user to the new subscription.409 Conflict — this is a deliberate first-wins policy (the comment in SubscriptionAppleController calls it out explicitly), not a bug: whoever verifies a given original_transaction_id first keeps it. A genuine restore onto a new device by the same person succeeds, because the App Store Server API always returns the same original_transaction_id for that person's purchase, and repeated verifies from the same user resolve as already-linked, not a conflict.expected_app_account_token to the calling user's Anythink id where StoreKit 2's appAccountToken is used, so Apple's own anti-fraud check on the transaction lines up with the Anythink account making the call.Calling verify twice with the same transaction — a retry after a network timeout, or the app calling Transaction.currentEntitlements again on launch — is safe and returns the same subscription, not a duplicate:
AlreadyLinked) before attempting to link again.superseded and a plan_changed subscription event is recorded.Register the notification URL from Settings › Payments › Apple in App Store Connect. AnythinkPay's notification handler maps Apple's App Store Server Notifications V2 types to subscription status and event history:
| Apple notification | Subtype | Resulting status | Event |
|---|---|---|---|
SUBSCRIBED |
— | active |
created |
DID_RENEW |
— | active |
renewed |
OFFER_REDEEMED |
— | active |
offer_redeemed |
EXPIRED |
— | expired |
expired |
GRACE_PERIOD_EXPIRED |
— | expired |
expired |
REFUND |
— | revoked |
refunded |
REVOKE |
— | revoked |
revoked |
DID_FAIL_TO_RENEW |
GRACE_PERIOD |
grace_period |
grace_period |
DID_FAIL_TO_RENEW |
(other) | in_billing_retry |
renewal_failed |
DID_CHANGE_RENEWAL_STATUS |
AUTO_RENEW_DISABLED |
cancelled |
cancelled |
DID_CHANGE_RENEWAL_STATUS |
(other) | active |
resumed |
DID_CHANGE_RENEWAL_PREF |
UPGRADE |
— (status unchanged) | plan_changed |
DID_CHANGE_RENEWAL_PREF |
DOWNGRADE |
— (status unchanged) | plan_change_pending |
A DID_RENEW, SUBSCRIBED or OFFER_REDEEMED notification also records a Payment row (deduplicated against verify by Apple's originalTransactionId, so a renewal doesn't get double-counted if both verify and the notification observe it).
TEST notifications (from Send test notification) don't map to a subscription — they're recorded for the dashboard's test-notification status check and acknowledged.
Note: If a
SUBSCRIBEDnotification arrives before your app has called verify for the same transaction, the handler returns503withRetry-After: 60rather than dropping the notification — Apple retries, giving verify time to land first. Every other notification type for an unknown transaction is acknowledged and ignored, since there's nothing in Anythink yet for it to update.
Apple and Stripe subscriptions share one Subscription row shape and one access rule — see the status table in Create plans and manage subscriptions. Your app's entitlement check (GET /subscriptions/me/entitlement) doesn't need to know which provider a subscriber used.
Limit: Re-syncing a subscription's live status on demand (AnythinkPay › Subscriptions › Resync) only supports Stripe today — Apple relies entirely on notifications arriving. If a notification is missed (for example, the notification URL was misconfigured for a period), the subscription's status won't self-correct until the next notification for it arrives.
Limit: Android has no in-app purchase provider in AnythinkPay — only iOS goes through Apple IAP; Android goes through Stripe web checkout (see Take payments with Stripe).
Limit: Verify's ownership binding is first-wins with no admin override in the app itself — if a transaction gets linked to the wrong Anythink user (for example a shared test device), a project administrator has to fix it with the
admin/relinkrecovery endpoint in Create plans and manage subscriptions, not through any self-service flow.
| To | Permission |
|---|---|
| Read/write Apple IAP credentials, send test notifications | Project administrator |
| Call verify as yourself | anythink_subscription_plans:read, standard user access |