All posts
3 min readBy AnggaRevenueCat / AdMob / Google Play / monetization / mobile

Mobile in-app purchases: what nobody warns you about

AdMob, RevenueCat, and Google Play Billing in practice. Products versus entitlements versus offerings, the 'purchase unavailable' debugging, license testers, and real-time notifications over Pub/Sub.

The game took weeks. Wiring up purchases so they actually worked, end to end, took about as long as everything else combined. Here is the map I wish I had.

Three concepts that are not the same

RevenueCat models purchases with three layers, and conflating them is the source of most "why doesn't this work" pain:

  • Products - mirrors of the items in the store (a "remove ads" one-time buy, a "pro" subscription with monthly and yearly base plans).
  • Entitlements - what the user owns after buying. The app checks these to unlock features. We use no_ads and pro.
  • Offerings - what is for sale on your paywall. The app reads offerings.current to show prices.

Miss the offering and the app shows "purchases unavailable". Miss the entitlement attach and a purchase succeeds but unlocks nothing. I hit both, on different apps, and each looked like a different bug. They were the same mistake at different layers.

The chain you must complete, per app:

Play product  ->  RevenueCat product  ->  attached to an Entitlement (what buying grants)
                        \-------------->  packaged into the current Offering (what is sold)

The errors and what they meant

  • "Pembelian tidak tersedia" (purchase unavailable) - no current offering, or the RevenueCat key was missing from the build. Fix the offering, set it as current, rebuild.
  • "Product already purchased" on tapping buy again - the purchase went through at Google but the entitlement was never attached in RevenueCat, so the app never saw it as owned. The robust fix is to catch that specific error and call restore instead of surfacing it, so a stuck user self-heals by tapping the button once more.
  • Empty product import - the Play Developer API takes up to 24 to 36 hours to propagate service-account credentials before it returns products. Closed testing is enough to import; you do not need production. You just need to wait.

License testers change reality

When you buy as a license tester (an account on your Play Console license-testing list), Google compresses subscription periods so you can test renewals in minutes instead of months. A monthly plan shows "per 5 minutes", a yearly shows "per 30 minutes". The price is real, the period is accelerated. The first time you see "Rp 75.000 / 30 min" you will think something is broken. Nothing is broken.

Crucially, license testing is account-level and applies on every track including production. A normal user in your open beta who buys is charged real money. A closed tester who is not also a license tester pays full price. Keep those lists straight.

Real-time developer notifications

When a subscription renews, cancels, or refunds, Google publishes a message to a Cloud Pub/Sub topic, and RevenueCat subscribes to it. That is how entitlements update within seconds instead of waiting to poll. Setting it up means:

  • A topic in your Google Cloud project.
  • Granting Google's notification service account the Pub/Sub Publisher role on that topic. This step is the one everyone forgets, and without it the "send test notification" button just fails.
  • Pasting the topic name into Play Console.

The unglamorous truth

Mobile IAP is not hard code. It is a distributed system with three dashboards (Play Console, RevenueCat, Google Cloud), eventual consistency measured in days, and error messages that point at the wrong layer. Budget real time for it, test with a license-tester account, and verify the full chain (product, entitlement, offering) for every single product on every single app.

Next: a discount that spans all four apps, built with no backend and no sign-in.