Connect Apple App Store — set up your account once, connect every app in seconds
Apple's server credentials are account-level. The issuer ID, the two API key IDs, and their two .p8 files are created once in App Store Connect and authenticate every app under your Apple Developer account. So you set them up in Crossdeck once — Crossdeck verifies them live against Apple — and from then on each app connects with just its Bundle ID and App Store ID, which Crossdeck pre-fills from onboarding. One click. Rotate the keys later and every connected app refreshes at once. The credentials are shared, but revenue can never leak between apps — each app's webhook is bound cryptographically to that app's own Bundle ID.
TL;DR
- Set up your Apple Developer account once. Enter the issuer ID, both API key IDs, and both
.p8files a single time. Crossdeck verifies them live with Apple and stores them as your account. - Connect each app in seconds. After the account exists, connecting an app needs only its Bundle ID and App Store ID — and Crossdeck pre-fills both from onboarding. One click, no re-typing keys.
- The credentials are account-level, not per-app. Apple's issuer ID, the two keys, and the two
.p8files authenticate every app under your Apple Developer account. That's why you create them once. - Two keys, two jobs. The App Store Server API key handles transactions and subscription status; the App Store Connect API key reads your product catalog. Apple splits these across two APIs; your account holds one of each.
- Rotate once, every app refreshes. Update your account's keys and Crossdeck re-pushes them to every connected app automatically. No per-app rotation.
- Each app pastes its own Server Notification URL. After connecting, paste that app's
appleWebhook/{projectId}URL into App Store Connect as both the Production and Sandbox V2 URL. - Shared keys never leak revenue between apps. Each app's webhook is its own URL, and Apple's
SignedDataVerifierbinds every transaction to that app's own Bundle ID — a transaction for the wrong app is rejected. See the isolation guarantee. - Keys live in Google Cloud Secret Manager. Never in Firestore, never in the browser, never in an app bundle.
Your Apple keys are account-level
This is the thing to internalise before anything else: Apple's server credentials belong to your Apple Developer account, not to any one app. A single issuer ID, a single App Store Server API key, and a single App Store Connect API key — each with one .p8 file — authenticate every app in your account. There is no per-app key to generate. You make this one set once, and it works for app number one, app number fifty, and every app you ship next year.
That's why Crossdeck's setup is two layers. You configure the account once (the shared credentials). Then you connect each app, which only ever needs that app's two identifiers.
The one set of credentials
| Credential | What it is | Scope |
|---|---|---|
| Issuer ID | A UUID like 57246542-96fe-1a63-e053-0824d011072a. The same value authenticates both keys. |
One per account |
App Store Server API key + its .p8 |
Authenticates the App Store Server API — transactions, authoritative subscription status, notification history. This is the truth Crossdeck reconciles every webhook against. | One per account |
App Store Connect API key + its .p8 |
Authenticates the App Store Connect API — your product catalog (every in-app purchase and subscription you've defined). | One per account |
Where to create them in App Store Connect
Everything lives under App Store Connect → Users and Access → the Integrations tab. The page opens with App Store Connect API selected, and the Issuer ID (the UUID) is shown right there — copy it once.
- App Store Connect API key (catalog). Under Team Keys, click plus (+), name it (e.g.
Crossdeck Catalog), set Access to App Manager or Admin, and Generate. Download theAuthKey_<KeyID>.p8file — the 10-character code in the filename is its Key ID. - App Store Server API key (transactions). In the Keys sidebar, click In-App Purchase, then Generate In-App Purchase Key (Account Holder or Admin only). Name it, generate, and download its
AuthKey_<KeyID>.p8— a different Key ID from the catalog key.
.p8 downloads only once.
Apple lets you download a key's private .p8 file a single time. Save both somewhere safe the moment they download. Lose one and you revoke it in App Store Connect and generate a replacement — then update your account in Crossdeck (see Rotating your keys).
On Users and Access your account also shows a 10-character Team ID like 8LAB5VPTNK. Crossdeck never uses that. The value it needs is the Issuer ID — the UUID on the Integrations tab.
Set up your account (once)
In the Crossdeck dashboard, open your Apple Developer account setup. Enter the three account-level credentials — once:
| Field | What to enter |
|---|---|
| Issuer ID | The UUID from the Integrations tab. |
| App Store Server API key | Its Key ID plus the .p8 file you downloaded for the In-App Purchase key. Crossdeck auto-fills the Key ID from the filename. |
| App Store Connect API key | Its Key ID plus the .p8 file for the catalog key. |
Click save. Crossdeck verifies the credentials live against Apple before storing anything: it makes an authenticated read of /v1/apps on the App Store Connect API to prove that key, its Key ID, and the issuer ID are valid. On success the two .p8 files go straight into Google Cloud Secret Manager (one account secret each) and the non-secret config — issuer ID and both Key IDs — is recorded as your account. The key bytes never touch Firestore, never persist in the browser.
Setup takes API keys and identifiers only — never your Apple sign-in. The keys are independently revocable from Users and Access → Integrations without touching your Apple Account, and Crossdeck never reads back the key material once it's in Secret Manager.
Connect an app (every time after)
With the account in place, connecting an app is the fast part. On the app's Payment rails page, the Apple card needs only two per-app facts — and Crossdeck pre-fills both from onboarding:
| Field | What it is |
|---|---|
| Bundle ID | Your app's reverse-DNS identifier, e.g. com.yourcompany.app. From App Store Connect → your app → App Information. |
| App Store ID | Your app's numeric ID, e.g. 1234567890. Digits only. Also on the App Information page. |
Click Connect. Crossdeck pulls your saved account credentials, proves them against Apple for this app — it calls requestTestNotification on the App Store Server API with the app's Bundle ID, which proves the key, Key ID, issuer ID, and Bundle ID all work together — then writes the verified rail. No keys to upload, no issuer ID to re-type. The Apple card flips to Verified.
Paste this app's Server Notification URL
To get live events flowing the other way — renewals, cancellations, refunds — register this app's own notification URL in App Store Connect. After connecting, Crossdeck shows it on the Apple rail card. It carries the app's project ID:
https://api.cross-deck.com/appleWebhook/<projectId>
Each app has its own URL — this is the routing that keeps apps isolated. Copy it, then in App Store Connect:
- Open Apps and select this app.
- In the sidebar, under General, click App Information.
- Scroll to App Store Server Notifications.
- Choose Version 2 — not Version 1 (deprecated). Crossdeck's receiver only handles V2.
- Paste the URL into both the Production Server URL and the Sandbox Server URL, and Save.
Crossdeck reads the environment from each signed payload and stamps records accordingly, so production and sandbox never cross-pollute even though they share the URL. Setting both fields means you never have to come back when you start sandbox testing.
You can connect before setting the URL — verification doesn't depend on it (see Troubleshooting). But live notifications won't reach Crossdeck until the URL is registered, so set it as soon as the rail is verified.
Rotating your keys
Because the keys are account-level, rotation is a single action. Generate a fresh key in App Store Connect, then update your account in Crossdeck with the new Key ID and .p8. Crossdeck re-verifies the new credentials against Apple, then re-pushes them to every app you've already connected — each app's stored secrets and config are updated in place. You do not reconnect apps one by one.
Re-push updates only the account-level fields (issuer ID, the two Key IDs, and the two .p8 secrets) on every verified Apple-rail app. Each app's own facts — Bundle ID, App Store ID, environment, verification status — are untouched. Rotation never connects, disconnects, or re-points an app; it only refreshes the shared credentials they all read.
Once you've confirmed the new key works, revoke the old one in App Store Connect → Users and Access → Integrations. Apple's side is then authoritative — a revoked key can't authenticate, regardless of what's stored anywhere.
Does sharing keys leak revenue between my apps?
No. This is the first question every multi-app developer asks, so here is the plain answer with the mechanism. Your apps share one set of credentials, but a transaction from app A can never appear under app B. Three independent layers enforce it, and the middle one is cryptographic:
- Routing — each app has its own webhook URL. Apple delivers an app's notifications to the URL you registered for that app:
appleWebhook/{projectId}. App A's events arrive at app A's project, app B's at app B's. There is no shared inbox to sort. - Cryptographic verification — every transaction is bound to one app's Bundle ID. When a notification arrives, Crossdeck builds Apple's official
SignedDataVerifierusing that project's own Bundle ID and App Store ID. Apple's verifier checks the signed payload's app identifier against that Bundle ID. Any transaction whose Bundle ID doesn't match is rejected withINVALID_APP_IDENTIFIER— the JWS fails verification and nothing is written. A misrouted or forged transaction for another app cannot pass. - Query scoping — reads are pinned to one App Store ID. When Crossdeck reads catalog or transaction state, it queries with that app's numeric App Store ID, so an app only ever pulls its own data.
The credentials authenticate access to your account; they do not decide which app a transaction belongs to. That decision is made per-project by Apple's signature verifier against each app's own Bundle ID. So you get one-time setup convenience and hard per-app isolation at the same time — there is no trade-off between them.
Troubleshooting
I connected an app but haven't set its Server Notification URL yet
That's fine — the app connects anyway. Connecting verifies your keys; the notification URL is a separate, after-the-fact step. When Crossdeck calls requestTestNotification during connect and the app has no URL set, Apple returns SERVER_NOTIFICATION_URL_NOT_FOUND. That is not a credential rejection — it's the opposite: Apple authenticated the request and resolved your Bundle ID to a real app before noticing there was no URL to deliver to. Crossdeck treats it as proof the keys are valid and connects the rail. Paste the URL afterward (see Connect an app). If we refused, you'd be deadlocked: you can't set the URL before you have the URL to set.
"Set up your Apple Developer account first"
You tried to connect an app before your account credentials exist. Account mode needs the saved issuer ID and both keys to pull from. Complete Set up your account once, then connecting any app needs only its Bundle ID and App Store ID.
"Apple rejected the App Store Connect API key — verify the .p8 key, key ID, and issuer ID"
Your account setup failed the live /v1/apps check. Re-check the .p8, Key ID, and Issuer ID for the App Store Connect API key. The two most common causes: an Issuer ID that's actually the 10-character Team ID instead of the UUID, and a key generated with too little access — set it to App Manager or Admin.
"Apple rejected these credentials — verify the .p8 key, key ID, issuer ID, and bundle ID"
The App Store Server API key failed when connecting a specific app. Because the account-level keys are already proven, the value to suspect first is this app's Bundle ID — it must match App Store Connect exactly. Confirm the Bundle ID on the app's App Information page; copy, don't retype.
"appStoreAppId must be the numeric App Store app ID"
The App Store ID field must be digits only (e.g. 1234567890). You've likely entered the Bundle ID by mistake. The numeric App Store ID is on the app's App Information page.
"p8Key does not look like a .p8 private key"
The file you uploaded during account setup isn't a private key. Upload the AuthKey_….p8 file Apple gave you — a PEM file containing a BEGIN PRIVATE KEY block. A certificate (.cer) or provisioning profile is not the same thing.
"Could not sign a request with this key — check the .p8 file is intact"
The .p8 is the right kind of file but is corrupted or truncated — often from copy-pasting its contents instead of uploading the file. Because Apple only lets you download a key once, you can't re-download it: revoke that key in App Store Connect, generate a fresh one, and update your account (which re-pushes to every connected app).
A connected app is verified, but no webhook events appear
Almost always the Server Notification URL. Confirm in App Store Connect → that app → App Information → App Store Server Notifications that the URL is set, that it's Version 2, and that it's that app's exact URL (api.cross-deck.com/appleWebhook/<projectId> — the project ID must match). Remember each app has its own URL.
What's next
- Rail webhooks — Apple — the full App Store Server Notifications V2 taxonomy: every notification type, the state it drives, and the verify → dedupe → reconcile → project pipeline.
- Migration — how discovery and backfill reconstruct an existing subscriber base, and the
/v1/migration/usersupload for subscribers outside the 180-day window. - Products & catalog — what to do with the in-app purchases and subscriptions that just mirrored in, and mapping them to entitlements.
- Entitlements & gating — turning Apple subscription state into the capabilities your app gates on.
- Identity verification — how
appAccountTokenlinks an Apple subscriber to a user in your app. - Connect Stripe — the OAuth-based rail, for comparison.
Last updated when account-level Apple credentials shipped — set up once, connect each app with Bundle ID + App Store ID, rotate once for all apps (June 12, 2026). Code references: backend/src/auth/apple-account.ts, backend/src/auth/apple-connect.ts, backend/src/lib/apple-verifier.ts, backend/src/webhooks/apple.ts, backend/src/lib/apple-store-connect-api.ts.