Last updated: August 10, 2026 · Last reviewed against Google docs: May 17, 2026
How to handle Google Play subscription webhooks
What developers call a "Google Play subscription webhook" is technically a Cloud Pub/Sub push delivery of a Real-time Developer Notification (RTDN). Google Play publishes an RTDN to a Pub/Sub topic when an Android subscription's state changes, and a Pub/Sub push subscription forwards each message to a configured HTTPS endpoint. The request your handler receives is a Pub/Sub message, not a direct webhook from Google.
This doc covers what a correct handler has to do with that message. The rules apply whether you build the handler yourself or use a service that handles it for you.
If you use SubTru, see its guide to how subscriptions work in SubTru for the implemented Google Play scope.
What Google Play sends to your webhook endpoint
Google Play publishes each event to a Cloud Pub/Sub topic configured in the Play Console. A Pub/Sub push subscription forwards the message to a configured HTTPS endpoint as a JSON POST request.
The request body is a Pub/Sub envelope with:
message.messageId(the Pub/Sub-assigned message ID, the canonical dedup key)message.publishTime(when Pub/Sub published the message)message.data(the base64-encoded RTDN payload)subscription(the Pub/Sub subscription resource name)
The Authorization header carries a Google-signed JWT (Bearer <token>) that authenticates the push subscription as Google's.
The base64-decoded RTDN payload has version, packageName, eventTimeMillis, and a subscriptionNotification object with version, notificationType (an integer event code), and purchaseToken.
RTDN is a signal that something changed. The current subscription state lives in the SubscriptionPurchaseV2 resource your handler fetches with purchases.subscriptionsv2.get using the purchaseToken.
What a correct Google Play webhook handler does
In short, verify the Pub/Sub push JWT, base64-decode the RTDN payload, deduplicate by Pub/Sub messageId, fetch the current subscription with purchases.subscriptionsv2.get, link the purchase to a user, persist durable state, and acknowledge new purchases only after the write succeeds.
- Receive the request at an endpoint dedicated to Pub/Sub push delivery.
- Read the raw request body before any framework parsing.
- Verify the Pub/Sub push JWT in the
Authorizationheader against Google's public keys. - Parse the Pub/Sub envelope, deduplicate by
messageId, and base64-decode the RTDN payload. - Verify
packageNamematches a package the handler is configured for. - Call
purchases.subscriptionsv2.get(packageName, purchaseToken)to fetch the current subscription resource. - Link the purchase to a user via the obfuscated account identifier, or via
linkedPurchaseToken/outOfAppPurchaseContexton resubscribe flows. - Persist durable state, then acknowledge new purchases when Google reports
ACKNOWLEDGEMENT_STATE_PENDING.
Verifying the Pub/Sub push JWT
Cloud Pub/Sub push subscriptions can be configured with OIDC authentication. When that is enabled, every push request carries an Authorization header with a Google-signed JWT identifying the push subscription as Google's. The handler verifies the JWT against Google's JWKS endpoint before trusting anything else about the request.
A correct verifier checks:
- Issuer (
iss): must beaccounts.google.comorhttps://accounts.google.com. - Audience (
aud): must match the audience configured on the Pub/Sub push subscription. If the audience field is left blank, Pub/Sub uses the push endpoint URL. - Email: the
emailclaim must match the service account configured for push authentication on the push subscription, andemail_verifiedmust be true.
A handler that skips any of these can be called by anyone who can reach the public endpoint. The handler's URL is public by definition, so JWT verification is what separates Google's push deliveries from arbitrary HTTP requests.
RTDN is a signal, not the current state
Google's RTDN reports that an event happened on a subscription. It does not include the resulting subscription state. The right pattern:
- decode the RTDN to extract the
purchaseToken - call
purchases.subscriptionsv2.get(packageName, purchaseToken) - treat the returned
SubscriptionPurchaseV2resource as the source of truth
RTDN delivery is independent of subscription-state changes. Multiple events can be in flight at once, or arrive out of order. The subscriptionState field in the fetched resource tells the handler what state the subscription is in now, regardless of which notification triggered the fetch. notificationType is still useful for logging and audit, but access decisions should come from the fetched state.
Subscription states that affect access
The subscriptionState field on the fetched SubscriptionPurchaseV2 resource is the authoritative status. Mapping it to access:
| State | Has access? | Notes |
|---|---|---|
SUBSCRIPTION_STATE_ACTIVE | Yes | Trialing when the line item's offer phase is a free trial. |
SUBSCRIPTION_STATE_IN_GRACE_PERIOD | Yes | Renewal failed, but access continues during Google's grace period. |
SUBSCRIPTION_STATE_CANCELED | Yes (until expiryTime) | User canceled; access continues through the paid period. |
SUBSCRIPTION_STATE_ON_HOLD | No | Grace ended, and access is suspended until the user resolves payment. |
SUBSCRIPTION_STATE_PAUSED | No | The subscription is paused. |
SUBSCRIPTION_STATE_EXPIRED | No | Subscription ended. |
SUBSCRIPTION_STATE_PENDING | No | Purchase not yet confirmed; payment is pending. |
SUBSCRIPTION_STATE_PENDING_PURCHASE_CANCELED | No | Pending purchase was canceled before confirming. |
The two pending states are non-entitlement because the purchase has not yet been confirmed. The user has agreed to pay, but Google has not collected the payment. Treating them as active would grant access for a payment that may never settle.
Acknowledging new purchases
Google requires new subscription purchases to be acknowledged within three days. An unacknowledged new purchase is automatically refunded and revoked after the window. Renewals do not need to be acknowledged. Only initial purchases and resubscribes do.
The acknowledgement call is purchases.subscriptions.acknowledge. A correct handler checks the acknowledgementState field returned by purchases.subscriptionsv2.get and only acknowledges when it reads ACKNOWLEDGEMENT_STATE_PENDING. Other values mean the purchase is already acknowledged.
Order matters. Write durable state first, then acknowledge. Acknowledging before the write is unrecoverable. Google considers the purchase confirmed, and if the write then fails, the system has no record of a purchase Google holds the user accountable for. The reverse order is safe. If acknowledge fails after the write, the next delivery will retry the acknowledgement.
Why idempotency is required
Cloud Pub/Sub push delivery is at-least-once, which means the same message can arrive more than once. A few reasons this could occur are if the handler's response was slow, an ack deadline expired, or Pub/Sub retried before learning the previous delivery succeeded.
Use two layers of deduplication.
- Deduplicate by Pub/Sub
messageIdbefore processing, to skip obvious duplicates. - Upsert the subscription record keyed by
purchaseToken, so out-of-order or partial retries land on the same row.
Live and test mode
Google Play license-tester purchases use the same RTDN shape as production purchases. The difference appears after the handler fetches the current SubscriptionPurchaseV2 resource: testPurchase is present only for test purchases. Check that field before writing subscription state. For a step-by-step setup, see how to test Google Play subscriptions.
Linking a Google Play purchase to a user
Google Play identifies subscriptions by purchaseToken, not by a user. To link them to users in your app, the handler needs an identity bridge.
The cleanest source is the obfuscated account identifier. The Android client sets it on the purchase via BillingFlowParams.Builder.setObfuscatedAccountId(...) with a stable, non-PII identifier for the user. Google returns the obfuscated account ID on every subsequent SubscriptionPurchaseV2 fetch in externalAccountIdentifiers.obfuscatedExternalAccountId.
Two other identity sources help in resubscribe and upgrade flows:
linkedPurchaseTokenis set on the new purchase when Google issues a new token for an upgrade, downgrade, or some resubscribe flows. It lets the handler match the new purchase to the previous subscription record.outOfAppPurchaseContextis included on certain re-purchase flows. It may contain prior obfuscated account IDs or expired purchase tokens that bridge back to the original user.
If none of those sources resolves the user, the safe answer is to log the unbound purchase, leave it unacknowledged, and surface the failure for manual remediation. Auto-binding by guessing creates a worse failure mode than a stopped pipeline.
Common Google Play webhook mistakes
- Treating RTDN as the full purchase record. RTDN is a trigger. The current subscription state comes from
purchases.subscriptionsv2.get, not from the notification payload. - Skipping Pub/Sub push JWT verification. The handler URL is public. JWT verification is what separates Google's push deliveries from any other HTTP request that reaches the endpoint.
- Ignoring duplicate delivery. Pub/Sub redelivers messages whenever an ack deadline is missed or an error response is returned. Deduplicate by
messageIdbefore processing. - Acknowledging before durable processing. If the write fails after acknowledge succeeds, the handler has no record of a purchase Google considers confirmed. Persist first, then acknowledge.
- Flattening Google state too early. Apps using the access check abstraction still benefit from logging the exact
SUBSCRIPTION_STATE_*value for support and debugging. It carries information the normalized state hides, like the difference betweenON_HOLDandPAUSED. - Auto-binding unclear purchases. A valid purchase with no resolvable identity should stop the pipeline and surface for manual remediation, not get attached to a best-guess user.
- Ignoring
testPurchase. License tester purchases can reach the same RTDN topic as production purchases. ChecktestPurchaseon the fetchedSubscriptionPurchaseV2resource before applying access.
How this fits across providers
If your app also takes payments through Stripe or the App Store, the same problems (signature verification, idempotency, retries, user mapping) show up with Stripe subscription webhooks and Apple App Store Server Notifications V2. The mechanics differ across providers (Stripe signs the raw body with HMAC, Apple ships a JWS with an x5c certificate chain, Google uses an OIDC JWT for push authentication plus a follow-up Developer API call). See how subscription access works for how apps typically check whether a user is subscribed once the webhooks have been processed.
Frequently asked questions
SubTru ingests Google Play subscription webhooks at a project-scoped endpoint. It verifies the Pub/Sub push JWT against Google's public keys, calls purchases.subscriptionsv2.get to fetch the current SubscriptionPurchaseV2 resource, normalizes the result into the same subscription state the access check endpoint returns, and acknowledges new purchases only after durable state is written. When a valid Google purchase cannot be linked to a user, SubTru stops the pipeline and surfaces the unbound purchase for manual remediation rather than guessing. Your Google Play Console and service account stay yours. SubTru sits alongside them, ingesting RTDN and querying the Play Developer API, without taking over the account.
Sources
- Google Play: Real-time developer notifications reference
- Google Play: Subscription lifecycle
- Google Play Developer API: purchases.subscriptionsv2.get
- Google Play Developer API: SubscriptionPurchaseV2 resource
- Google Play Developer API: purchases.subscriptions.acknowledge
- Google Cloud Pub/Sub: Authentication for push subscriptions
- Android: BillingFlowParams.Builder.setObfuscatedAccountId