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.
What Kasskeep does
Section titled “What Kasskeep does”- Verifies the JWS certificate chain up to the Apple root certificates (Apple Root CA G2 and G3 are bundled) and the signature.
- Checks that
bundleIdmatches the app and, in production, the App Store app id. - Checks that
appAccountToken, when present, equals the submittingapp_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.
1. Notification URL
Section titled “1. Notification URL”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>/notificationsEach 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.
2. API key
Section titled “2. API key”Open Users and Access → Integrations → In-App Purchase:
- Generate an In-App Purchase key and download the
.p8file. It can only be downloaded once. - Note the Key ID and the Issuer ID at the top of the page.
- Note the app’s Apple ID under App Information → General Information.
Upload them:
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 productionOr 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 |
App settings
Section titled “App settings”| 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 |
Client requirements
Section titled “Client requirements”- Use StoreKit 2 and set
appAccountTokento 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 handling
Section titled “Notification handling”| 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.