Skip to content

Apple App Store

Kasskeep verifies StoreKit 2 signed transactions, receives App Store Server Notifications V2 and calls the App Store Server API. Channel id: app_store, platform: ios.

  • Verifies the JWS certificate chain up to the Apple root certificates (Apple Root CA G2 and G3 are bundled) and the signature.
  • Checks that bundleId matches the app and, in production, the App Store app id.
  • Checks that appAccountToken, when present, equals the submitting app_user_id (case-insensitive).
  • Records the transaction once per app, platform, environment and transaction id. Newer signed data always wins over older data, compared by signedDate.

In App Store Connect open your app, then App Information → App Store Server Notifications:

  • Choose Version 2.
  • Set both the production and the sandbox server URL to:
https://api.kasskeep.com/v1/stores/apple/<app_id>/notifications

Each environment accepts only one URL. Use the App Store Server API’s Request a Test Notification to confirm the connection; Kasskeep records the TEST notification.

Open Users and Access → Integrations → In-App Purchase:

  1. Generate an In-App Purchase key and download the .p8 file. It can only be downloaded once.
  2. Note the Key ID and the Issuer ID at the top of the page.
  3. Note the app’s Apple ID under App Information → General Information.

Upload them:

Terminal window
npx @kasskeep/cli apps update --id app_xxx \
--apple-issuer-id 57246542-96fe-1a63-e053-0824d011072a \
--apple-key-id 2X9R4HXF34 \
--apple-private-key-file ./AuthKey_2X9R4HXF34.p8 \
--apple-app-apple-id 1234567890 \
--apple-environment production

Or with the admin API:

PUT /v1/admin/tenants/:tenant_id/apps/:app_id/credentials/apple
{"issuer_id": "...", "key_id": "...", "private_key": "-----BEGIN PRIVATE KEY-----...", "app_apple_id": 1234567890, "environment": "production"}

Credentials are write-only: GET .../credentials returns the issuer id, key id and a fingerprint of the private key, never the key.

Without the API key, receipts and notifications still work. The key enables:

App Store Server API Used for
Get All Subscription Statuses Latest renewal info when a subscription is submitted
Get Transaction History Completing a user’s history on restore
Get Notification History Hourly reconciliation (first run looks back 7 days, then overlaps by one hour)
Set App Account Token Binding a transaction without appAccountToken (for example an offer code redeemed outside the app) to the first user who submits it
Send Consumption Information Answering CONSUMPTION_REQUEST within 12 hours
Setting CLI flag Values
apple_environment --apple-environment production (default) accepts production and sandbox data and stores them separately; sandbox accepts only sandbox
apple_refund_preference --apple-refund-preference DECLINE, GRANT_FULL or GRANT_PRORATED. When set, Kasskeep answers consumption requests automatically with customerConsented: true, so only set it if your terms cover this. Leave it empty to only send the refund.consumption_requested event
  • Use StoreKit 2 and set appAccountToken to the user’s UUID. The SDK does this for you.
  • Submit the transaction to Kasskeep and call finish() only after a successful response. Scan unfinished transactions on every launch.
Notification Result
SUBSCRIBED, DID_RENEW, OFFER_REDEEMED Transaction and subscription written; subscription.started, subscription.renewed or subscription.product_changed
DID_CHANGE_RENEWAL_PREF Upgrade switches product immediately; downgrade updates the next renewal product
DID_CHANGE_RENEWAL_STATUS subscription.canceled or subscription.uncanceled
DID_FAIL_TO_RENEW, GRACE_PERIOD_EXPIRED, EXPIRED Billing retry, grace period and expiry
PRICE_INCREASE Consent status recorded, no event
REFUND, REVOKE, REFUND_REVERSED refund, revoked, refund.reversed
CONSUMPTION_REQUEST refund.consumption_requested
ONE_TIME_CHARGE One-time purchase recorded
RENEWAL_EXTENDED Expiry updated, no event
REFUND_DECLINED, RENEWAL_EXTENSION, TEST, METADATA_UPDATE, MIGRATION, PRICE_CHANGE, RESCIND_CONSENT, EXTERNAL_PURCHASE_TOKEN Recorded only

Notifications are deduplicated by notificationUUID. A failed notification is processed again when Apple retries it. Processing for one original transaction id is serialized with a Postgres advisory lock. The endpoint returns 400 when the signature is invalid and 500 when processing fails, so Apple retries.