Sell on Google Play

You need: payments composed, a Play Console account, and a Google Cloud project.

Time: an hour and a half. This is the fiddliest of the rails, because it spans two consoles and a message queue.

1. Your application id is checked on every notification

Selling on the App Store is the sibling rail, native Android is the client half, and testers is the closed-test requirement Google puts in front of both.

A notification for another app is refused with 401 even when Google signed it correctly — because a Google signature proves who delivered a notification and never what it is about.

Every Play API call is scoped to it too.

2. Create the products

Under Monetize → Products.

For a subscription the id you want is the subscription id, not the base plan id. For a one-time product it is the product id.

payments({
  billingSubject: "user",
  rails: { google: true },
  products: {
    pro_monthly: {
      type: "subscription",
      name: "Pro",
      entitlements: ["pro"],
      google: { productId: "pro_monthly" },
    },
  },
}),

3. Service account, in two consoles

This is two consoles, and the second half is the step most often missed.

In the Google Cloud console:

  1. Enable the Google Play Android Developer API
  2. Create a service account. No project roles are needed — its permissions come from the Play Console rather than from IAM
  3. Create a JSON key on it and download it. You get it once

Then in the Play Console, under Users and permissions, invite that service account’s email address and grant it, at the app level:

  • View financial data, orders, and cancellation survey responses
  • Manage orders and subscriptions

Without the financial-data grant, every purchase lookup answers 401, and the failure is reported with that hint attached. The grant can take a few hours to take effect — that is Google’s own propagation rather than a misconfiguration, and it is worth knowing before you spend an evening on it.

From the JSON key you need two fields: the client email, and the private key. It is PKCS#8; store it with its newlines intact, or escaped, which is how the file holds it.

4. Create the Pub/Sub topic

Notifications are published to a topic you own.

Create one, then grant Google’s publisher the right to write to it: add Google’s notification service account as a Pub/Sub Publisher on the topic.

Play will not accept the topic without it.

5. Point Play at the topic

Monetize → Monetization setup, and paste the topic’s full resource name.

Then Send test notification. It is recorded and answered 200 with nothing projected — a test notification is authentic and concerns no purchase.

6. The push subscription, and the audience claim

This is where the security boundary lives.

Create a push subscription on the topic:

FieldValue
Delivery typePush
Endpoint URLYour Worker’s Google webhook path
Enable authenticationOn
Service accountThe one from step 3
AudienceThe endpoint URL, exactly

The audience is not optional and it is not decoration.

Google signs the token on a push with the same handful of keys it uses for every Pub/Sub push subscription on the planet. So a valid signature says only that Google minted the token.

The audience claim is what says it was minted for your endpoint. It is checked against your configured value and a mismatch is refused with 401 — which means an endpoint that skipped the check would accept notifications from any Google customer who pointed a subscription at it.

The token’s email claim is checked against your configured service account too, which is Google’s own recommendation. Use the same service account for the push subscription and for the API grant, or the check refuses legitimate deliveries.

One subscription per environment, each with its own endpoint and its own audience.

7. The account identifier, and why it must be unguessable

Before launching the purchase flow:

BillingFlowParams.newBuilder()
  .setProductDetailsParamsList(params)
  .setObfuscatedAccountId(accountIdFromYourServer)
  .build()

A random value your server minted for the subject that will hold the purchase — never one anybody else can derive. Not the subject’s id, not a hash of an email or a domain.

The identifier is what attributes a notification that arrives before the app has submitted anything. So a guessable one is a way to aim at a specific account: somebody who can work out that subject’s identifier could make one real purchase carrying it and claim the link first.

Under organization billing the target is a whole company rather than one person, so a derivable identifier is worth more to an attacker.

8. Store the credentials

The application id, the service account email, the private key, and the Pub/Sub audience — inside one typed secret:

pithy secrets create payments-provider-credentials --env prod

The secret is environment-scoped, and each environment has its own push subscription with its own audience.

Sandbox never grants production

Every purchase carries its store environment, and a mismatch is refused. The environment comes from this deployment’s own var, never from the payload.

Only a Worker deployed to production is production. That asymmetry is deliberate: treating production as sandbox loses a purchase reconciliation repairs; the other way round hands out entitlements for test transactions.

The checklist people actually need

ESC