Skip to content

In-app purchase onboarding (Apple + Google)

This guide walks you through the provider-side configuration needed for Paylera to receive purchase notifications from the Apple App Store and Google Play. Budget 30–60 minutes per provider the first time you do it — most of that is clicking through Apple and Google’s consoles, not anything on the Paylera side.

By the end you’ll have:

  • Apple App Store Server Notifications V2 pointing at Paylera’s ingress, plus an App Store Connect API key uploaded.
  • Google Play Real-time Developer Notifications publishing to a Pub/Sub subscription that Paylera polls, plus a service-account JSON uploaded.
  • A “Validate” check that confirms both sides round-trip a fixture notification end-to-end.

If you’re integrating Stripe or Toss instead of (or in addition to) the app stores, those flow through Paylera’s primary payment-provider configuration — see the Provider setup overview for that path. IAP and card/checkout providers can coexist on the same tenant.

Before you start

Have these ready:

  • Apple side: Admin access to App Store Connect, plus your iOS app’s Bundle ID(s) for both sandbox and production.
  • Google side: Owner access to the Play Console for the app, plus Owner or sufficient IAM on a Google Cloud project where you can create service accounts and Pub/Sub topics.
  • Paylera side: a Paylera tenant (sandbox is fine to start) and Owner or Admin role on it.

You do not need to redeploy your mobile app to complete this onboarding — the provider-side notifications flow server-to-server.


Part 1 — Apple App Store Connect setup

Apple’s “App Store Server Notifications V2” (ASSN V2) is a webhook that Apple POSTs to a URL you configure. Each notification is a JWS signed by an Apple-issued cert. To validate purchases beyond the notification itself (e.g. fetch transaction history) you also need an App Store Connect API key.

1.1 Generate an App Store Connect API key

  1. Sign in to App Store Connect (https://appstoreconnect.apple.com).

  2. Go to Users and Access → Integrations → App Store Connect API → Keys.

  3. Click Generate API Key (or the + button if you’ve created keys before).

  4. Name it something recognizable — we recommend paylera-iap-{environment}.

  5. Under Access, choose the Admin role.

    <screenshot: App Store Connect “Generate API Key” dialog with role selector visible>

  6. Click Generate. Apple shows the key once — download the .p8 file immediately. You cannot re-download it.

  7. On the same page, note three values you’ll need shortly:

    • Key ID — shown in the row for the key you just created (10-character alphanumeric).
    • Issuer ID — shown at the top of the Keys page (UUID).
    • Team ID — Apple Developer Account → Membership → Team ID (10-character alphanumeric).

1.2 Find your Bundle IDs

In App Store Connect, open your app and note the Bundle ID under App Information. If you ship a separate sandbox/TestFlight build under a different Bundle ID, note that one too — Paylera treats them independently so sandbox traffic doesn’t pollute production aggregates.

1.3 Enable App Store Server Notifications V2

  1. In App Store Connect, open your app.

  2. Go to App Information → App Store Server Notifications → Production Server URL (and Sandbox Server URL separately).

  3. Set both URLs to Paylera’s ingress:

    Production: https://api.paylera.dev/v1/webhooks/apple_app_store
    Sandbox: https://api.paylera.dev/v1/webhooks/apple_app_store

    (Apple separates sandbox and production by which environment field is on the notification payload, not by URL.)

    If you operate your own relay (e.g. you want to MITM notifications through your infra first), use your relay URL here and forward to Paylera; see the Relay protocol guide for the format Paylera expects when notifications arrive via a relay.

  4. Under Version, choose Version 2. V1 is unsupported.

    <screenshot: App Store Connect ASSN V2 configuration with Production / Sandbox URL fields and Version 2 selected>

  5. Click Save.

1.4 Upload to Paylera

  1. In the Paylera tenant portal, go to Settings → IAP Credentials.

  2. Under Apple App Store, click Add credential.

  3. Fill in the form:

    FieldValue
    Key IDThe 10-char ID from step 1.1
    Issuer IDThe UUID from step 1.1
    Team IDThe 10-char ID from step 1.1
    Bundle ID (production)From step 1.2
    Bundle ID (sandbox)From step 1.2, if separate
    .p8 key fileDrag-and-drop the .p8 you downloaded

    The portal validates the .p8 parses as a valid EC private key at upload time. A malformed file is rejected here, not later when Apple sends the first notification.

  4. Click Save. The credential row appears with a green status dot and a Validate button — see §4.


Part 2 — Google Play setup

Google Play uses a more involved path: notifications publish to a Pub/Sub topic in your GCP project, Paylera subscribes to that topic via a service account you provision. The flow is Play Console → Pub/Sub topic → Paylera’s pubsub-ingress.

2.1 Create or pick a GCP project

You can reuse an existing project; nothing about IAP requires a dedicated one. The service account you create later will be scoped to just what Paylera needs, so blast radius is bounded regardless.

gcloud projects create paylera-iap-relay # if you want a fresh one

2.2 Enable the Play Developer API

In the GCP console (or via gcloud):

gcloud services enable androidpublisher.googleapis.com \
pubsub.googleapis.com \
--project=<project>

The Pub/Sub API is for the notifications transport; the Android Publisher API is for Paylera’s polling fallback when it needs to confirm a purchase against Google’s source of truth.

2.3 Create a service account

  1. In the GCP console, go to IAM & Admin → Service Accounts.

  2. Create service account, name it paylera-iap.

  3. Grant the service account these roles:

    • Pub/Sub Subscriber on the project (or just on the subscription you’ll create in step 2.5).
    • No other roles at the project level.
  4. After the account is created, click into it → KeysAdd key → Create new key → JSON. Download service_account.json.

    <screenshot: GCP service account Keys tab with “Create new key” highlighted>

    Store it somewhere short-term; you’ll upload it to Paylera in step 2.7 and then delete the local copy.

2.4 Grant the service account Play Console access

  1. Open Play Console (https://play.google.com/console).
  2. Settings → Developer account → Users and permissions.
  3. Invite new users, enter the service account’s email (looks like paylera-iap@<project>.iam.gserviceaccount.com).
  4. Grant View financial data and Manage orders and subscriptions at the app level for the app(s) you want Paylera to see.
  5. Save and accept the invite (the service account auto-accepts within a few minutes).

2.5 Create the Pub/Sub topic + subscription

In the GCP console:

  1. Pub/Sub → Topics → Create topic, name it play-rtdn-<app-package-name>.
  2. Subscriptions → Create subscription, name it paylera-<app-package-name>. Set the topic from step 1, and Delivery type: Pull. Leave the rest at defaults.
  3. Note both the topic name (full projects/<project>/topics/...) and the subscription name (full projects/<project>/subscriptions/...).

2.6 Tell Play Console to publish to the topic

  1. In Play Console, open your app.

  2. Monetize → Monetization setup → Real-time developer notifications.

  3. Paste the topic name from step 2.5 into the Topic name field.

  4. Click Send test notification — Play sends a testNotification envelope to the topic. You can check Pub/Sub → Topics → → Messages in the GCP console to see it land. (Paylera will also receive it once you’ve completed step 2.7; we deduplicate test envelopes silently.)

    <screenshot: Play Console RTDN configuration with topic name field and Send test notification button>

2.7 Upload to Paylera

  1. In the Paylera tenant portal, go to Settings → IAP Credentials.

  2. Under Google Play, click Add credential.

  3. Fill in the form:

    FieldValue
    Package nameYour app’s package name (e.g. com.example.app)
    Pub/Sub subscriptionFull subscription name from step 2.5
    service_account.jsonDrag-and-drop the JSON from step 2.3

    The portal validates service_account.json parses and contains "type": "service_account" at upload time. If you accidentally uploaded a user-account credential or a malformed file, you’ll see an error here.

  4. Click Save. Paylera’s pubsub-ingress picks up the new credential on its next sweep (≤30 seconds) and spins up a subscriber goroutine for the subscription.


Part 3 — Stripe and Toss (brief)

Stripe (cards / SEPA / wallets) and Toss (Korean card and KakaoPay) are configured through Paylera’s primary payment-provider section, not under IAP Credentials.

  • Stripe: Settings → Payment providers → Stripe → Connect. Follow the Stripe OAuth flow; Paylera handles webhook registration on its own ingress.
  • Toss: Settings → Payment providers → Toss → upload your Toss client key + secret key from Toss Payments dashboard.

Both providers run in parallel with IAP — a tenant can take in-app purchases through Apple/Google and card payments through Stripe with the same Paylera setup. Revenue from each provider lands in revenue_events tagged with the source, and the dashboard splits totals by source.


Part 4 — Testing your setup

After both Part 1 and Part 2 are complete, you have two ways to verify the wiring before you cut over real traffic.

4.1 The “Validate” button in the portal

In Settings → IAP Credentials, each credential row has a Validate button. Clicking it runs a synchronous check:

  • Apple: Paylera mints a JWT against your uploaded .p8, calls Apple’s App Store Server API GET /inApps/v1/lookup/<a known-bad order ID>, and confirms Apple returns a 404 (not a 401). A 404 means “we recognize your key but no such order exists” — exactly what we want. A 401 means the key/team/issuer triple is wrong.
  • Google: Paylera calls subscriptions.acknowledge on a known-nonexistent subscription ID. Google returns 404 if the service account works and has the right Play Console scopes; 403 if the service-account email isn’t bound in Play Console; 401 if the JSON is wrong.

Validation runs in <2 seconds and reports the specific failure mode if anything is off.

4.2 End-to-end fixture purchase

Even after Validate passes, a real purchase exercises the full notification path. Both providers have sandbox/test rails for this.

  • Apple sandbox: in App Store Connect, create a Sandbox Tester under Users and Access → Sandbox Testers. Sign your test device into that account and make a sandbox purchase. Apple emits ASSN V2 within ~30 seconds.
  • Google internal-test track: upload an internal test build to Play, add your tester email to the internal-test track, install via the test link, and make a purchase with a test card (or use a real card under Google Play license testing — Google won’t charge accounts on the license-testers list).

Within ~1 minute of the purchase, the event should appear in Paylera’s Revenue dashboard under the sandbox environment filter. If it doesn’t, check Troubleshooting below or refer to the operator revenue-ingest-stuck runbook.


Troubleshooting

Apple notifications return 401 at ingress

Most often a Bundle ID mismatch: Apple’s notification carries the Bundle ID it was issued against, and Paylera rejects notifications for Bundle IDs that aren’t registered on the tenant. Double-check that both your production and sandbox Bundle IDs are listed on the credential row in Settings → IAP Credentials. Sandbox builds in particular often have a .dev or .sandbox suffix.

Less commonly, the Apple notification’s signature doesn’t chain to a root Paylera recognizes (Apple rotated their CA). The ingress falls back to a live root fetch automatically; if you’re seeing 401s for >10 minutes, page on-call (see the revenue-ingest-stuck runbook — this is their problem, not yours).

.p8 upload rejected by the portal

The portal parses the .p8 as an EC private key before accepting it. Common causes of rejection:

  • You uploaded the certificate (.cer) instead of the private key (.p8).
  • The .p8 was opened in a text editor and re-saved with a BOM or CRLF line endings. Re-download from App Store Connect and upload directly without opening.

You only get one shot to download the .p8 from Apple — if you lost it, you have to revoke the key in App Store Connect and generate a fresh one.

Google service account returns 403

The service account exists and the JSON is valid, but Google rejects calls with “The current user has insufficient permissions.” This means step 2.4 didn’t take effect:

  1. Open Play Console → Users and permissions.
  2. Confirm the service account email is listed.
  3. Confirm it has View financial data and Manage orders and subscriptions for the specific app, not just the account-level default.
  4. Wait ~5 minutes after granting permissions for Google to propagate. Then click Validate again.

Pub/Sub subscription returns “permission denied”

Either the service account doesn’t have Pub/Sub Subscriber on the subscription, or the subscription name in the Paylera portal doesn’t match the actual GCP resource path.

The format Paylera expects is the full path: projects/<project>/subscriptions/<sub-name>. If you pasted just the short name, edit the credential and add the full path.

Test notification arrived at Pub/Sub but never appeared in Paylera

Two possibilities:

  • The subscription is the wrong type. Paylera pulls; if you created a push subscription instead, no one is pulling. Recreate as a pull subscription.
  • pubsub-ingress is at its per-instance tenant cap and hasn’t rotated your tenant onto a fresh replica yet. This auto-resolves within 30 seconds; if not, contact support.

What happens next

Once Validate passes and a fixture purchase round-trips:

  • Real-money purchases will start landing in revenue_events as soon as Apple/Google route them.
  • They’ll roll up into revenue_aggregates within 5 minutes.
  • The Revenue dashboard (and the useRevenue() React hook) will reflect them on the next render.

For the operational side — what to do if revenue stops flowing, how Paylera handles Apple’s CA rotations, what backfills look like — see the operator runbooks under docs/ops/runbooks/. You don’t need to learn any of that to run normally; it’s there for the on-call rotation if something breaks.