Skip to main content

Google Play Billing

How an Android app sells subscriptions and one-time products with Play Billing, and how the server verifies, acknowledges and follows them. Read it before you create a product in Play Console or change the purchase flow.

How it works​

  • Step 5. The productId from the app only picks which API to call. UPG re-checks the answer. Otherwise a user buys the cheap product and claims the expensive one.
  • Step 8. Google refunds automatically a purchase that is not acknowledged within 3 days. Acknowledge after the commit, never inside the transaction, and retry.
  • Step 12 and 13. A Real-time Developer Notification (RTDN) only says "something changed for this token". The state always comes from a fresh pull.

Play Console words​

WordMeaning
Product (one-time)Bought once. Keyed by its purchase token
SubscriptionThe thing you sell, like "Pro"
Base planA billing period and price inside a subscription, like monthly or yearly
OfferA trial or discount on a base plan. A free trial is an offer phase priced at zero
Purchase tokenThe id of one purchase. Upgrades and resubscribes create a new token linked to the old one
License testerA Google account that buys for free on test tracks

The store rules​

As of October 2026. Source: Play payments policy and Play Billing Library deprecation.

  • Digital goods sold in the app use Play Billing. Some countries allow user-choice billing with a smaller service fee, with extra reporting. Treat it as a later step.
  • Google forces a newer Play Billing Library about once a year. When we shipped, Play rejected bundles below Billing Library 8.0.0 or with targetSdk below 36. Lagging blocks every app update, not only billing ones. Track the deadline.
  • Play has no family sharing for in-app purchases. A family plan is ours to build.

How to build it​

Server access​

Create one Google Cloud service account per app for the Play Developer API. In Play Console, under Users and permissions, give it:

  • View app information
  • View financial data
  • Manage orders and subscriptions

Catalog access and order access are separate grants. Listing products can work while reading a purchase fails with 401. Permission changes take up to 24 hours to apply. A 401 on day one is usually that, not a bug.

Real-time developer notifications​

  1. Create a Pub/Sub topic. Grant google-play-developer-notifications@system.gserviceaccount.com the Publisher role on it.
  2. Point the app's Monetization setup in Play Console at the topic.
  3. Create a push subscription to the UPG webhook with OIDC authentication. The push service account is a different account from the API one.
  4. On every push, verify the OIDC token: signature, the exact audience, and that the sender email is our push service account. Reject when any is unset.
const ticket = await oauthClient.verifyIdToken({ idToken, audience: pubsubAudience });
const p = ticket.getPayload();
if (!p?.email_verified || p.email !== pushServiceAccountEmail) reject();
// Dedupe on the Pub/Sub messageId. Then pull the state; never trust the message body.

A wrong pubsubAudience rejects every notification with what looks like a credential error. Copy it exactly.

Map the states​

Play state or notificationOur state
SUBSCRIPTION_STATE_ACTIVEactive
IN_GRACE_PERIODin_grace, access kept
ON_HOLDon_hold, no access
PAUSEDpaused, no access
CANCELEDAuto-renew off. Access stays until expiry
EXPIREDexpired
SUBSCRIPTION_RECOVEREDBack to active
Voided purchaserevoked, booked as a refund once

Token chains​

An upgrade or resubscribe gives a new purchase token with linkedPurchaseToken pointing at the old one. Key the subscription on the first token of the chain. Ignore notifications for tokens that were replaced, or one user ends up with two subscriptions.

Voided purchases​

Refunds and chargebacks do not always arrive as an RTDN you can act on. Run a daily job over the Voided Purchases API with a 30-day lookback, and book each one once, keyed on the Play orderId.

Rules​

Rule

Test only with a build that is signed by Play and installed from a Play test track. A locally installed debug build shows "product not available" for every product.

Rule

Refuse license-tester purchases on the live connection. They are free, and a tester account would otherwise get real entitlements.

Rule

Product ids are burn-once. Use a separate, clearly named product set for dev, for example com.example.app.subscriptions.dev, never the production ids.

Rule

Acknowledge every purchase on every path: validate, webhook and reconcile. Record an unacknowledged token as a debt and retry it on a schedule. Alert loudly inside 24 hours, because Google refunds it at 3 days.

Mistakes we made​

  • One-time purchases could never be acknowledged, so Google refunded them. We keyed them on orderId, but every purchases.products call, acknowledge included, is keyed on the purchase token.
  • A user's upgrade looked like a second subscription. The new token was treated as a new purchase instead of following linkedPurchaseToken to the first token.
  • Refunds were booked twice. The RTDN and the voided-purchases job each booked the refund under a different id. Both now use the Play orderId.
  • Pulled state was stamped with the expiry time. That made every later event look older and get dropped. Stamp a pull with the time you observed it.
  • "Products not available" for a day. The build was installed from Android Studio, not from Play. Then, after that was fixed, a 401 was permission propagation.
  • Play rejected the release bundle. The billing plugin used an old Billing Library and the app targeted an old SDK. The upgrade forced a framework, Gradle plugin and minSdk bump in the same week. Check the Play deadlines every quarter.

Checklist​

  • Service account with the three permissions, one per app; waited 24 hours.
  • Pub/Sub topic with Google's publisher, push subscription with OIDC.
  • OIDC audience and sender email checked on every push.
  • State always pulled with subscriptionsv2.get; message body never trusted.
  • Acknowledgement after commit, on every path, retried.
  • Subscriptions keyed on the first token of the chain.
  • Daily voided-purchases job, refunds keyed on orderId.
  • License testers refused on live; dev uses its own product ids.
  • Billing Library and targetSdk meet the current Play deadline.