powered by

Verify Apple in-app purchases

Updated Sep 30, 2026
Configure App Store Connect credentials, verify StoreKit 2 transactions server-side, and handle Apple's renewal and refund notifications.

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.

App Store Connect setup

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).

In the Anythink dashboard

  1. Go to Settings › Payments, Apple tab.
  2. Enter Bundle ID, Environment, Issuer ID, Key ID, and paste the private key's .p8 contents into Private key.
  3. Save. The page shows the App Store Server Notifications URL to register in App Store Connect (App Information › App Store Server Notifications) so Apple can reach your project's webhook handler.
  4. Select Send test notification to confirm Apple can reach that URL, then check its delivery status on the same page.

With the CLI

bash
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.

With an AI assistant (MCP)

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.

Verifying a transaction

Your app calls one endpoint after a StoreKit 2 purchase or restore completes:

http
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:

json
{ "original_transaction_id": "1000000123456789" }
  • The signed_transaction path verifies the JWS signature directly and needs only your bundle id — no App Store Connect API credentials required.
  • The 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:

json
{
  "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.

Ownership binding

Verify links the transaction to the calling user's Anythink account, not to any identifier embedded in the App Store receipt:

  • First verify for a transaction creates a SubscriptionUser link from the calling user to the new subscription.
  • A transaction already linked to a different user returns 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.
  • Verify also sets 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.

Idempotent verify

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:

  1. The orchestrator checks whether the calling user already has this subscription linked (AlreadyLinked) before attempting to link again.
  2. Subscription fields (status, expiry, plan enrichment) are re-applied from the fresh verification response either way, so a retried verify also picks up any status change since the first call.
  3. Plan switches on the same Apple subscription group are handled automatically: when a verify succeeds for a new product and the user has other subscriptions for the same Apple provider and a different product reference, those older ones are marked superseded and a plan_changed subscription event is recorded.

Server notifications

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 SUBSCRIBED notification arrives before your app has called verify for the same transaction, the handler returns 503 with Retry-After: 60 rather 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.

Mapping to subscriptions and entitlements

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.

Limits

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/relink recovery endpoint in Create plans and manage subscriptions, not through any self-service flow.

Permissions

To Permission
Read/write Apple IAP credentials, send test notifications Project administrator
Call verify as yourself anythink_subscription_plans:read, standard user access

Next steps

How payments and subscriptions work

Verify Apple in-app purchases | Anythink Docs