How Google Play reports subscription state
Play tells your backend about a subscription in two different ways, and they do not carry the same information. Knowing which one moved an entitlement explains most of what you will see in it.
Two inputs, one entitlement
The first input is a subscription query. When Tierux verifies or refreshes a purchase, it calls Google's purchases.subscriptionsv2.get with the purchase token and reads back the subscription's subscriptionState, the first line item's expiryTime and productId, and, on a plan change, a linkedPurchaseToken. This is the authoritative snapshot.
The second input is a Real-Time Developer Notification (RTDN). Play publishes it to Pub/Sub when something changes. It carries the package name, the purchase token, the subscription id, and a numeric notificationType — and nothing about the user. Tierux resolves the user by hashing the token and looking up the purchase it already verified, then resolves the entitlement through your product mapping. If the chain breaks — an unrecognized package, a token with no purchase record, an unmapped product — the notification is acknowledged without a write. The delivery mechanics — Pub/Sub authentication, idempotency, and ordering — are covered in chapter 4 of the verification guide.
Both inputs write the same entitlement record, with the same six statuses described in the entitlement state machine. What differs is how much each one knows.
From a subscription query
A fresh query maps Play's subscriptionState onto a Tierux status, and grants access only for the two states where Play says the subscriber is entitled:
Play subscriptionState | Tierux status | Access |
|---|---|---|
SUBSCRIPTION_STATE_ACTIVE | active | On |
SUBSCRIPTION_STATE_IN_GRACE_PERIOD | grace_period | On |
SUBSCRIPTION_STATE_ON_HOLD | account_hold | Off |
SUBSCRIPTION_STATE_PAUSED | account_hold | Off |
SUBSCRIPTION_STATE_CANCELED | cancelled | Off |
SUBSCRIPTION_STATE_EXPIRED | expired | Off |
✓ access on · ✕ access off
Other values — SUBSCRIPTION_STATE_UNSPECIFIED or a pending purchase, for example — become unknown and are treated as not valid. Two further checks apply on top of the table. If Play's expiryTime is already in the past, access is off and the status is rewritten to expired. And if Play said "active" but the check above turned access off, the status becomes unknown rather than staying active, so the record does not go on claiming a live subscription that did not hold up.
Note the SUBSCRIPTION_STATE_CANCELED row. A query of a canceled subscription turns access off, even though Play still considers the subscriber entitled until expiryTime. The cancellation notification below does not; it leaves access as it was.
From a notification
A notification maps each notificationType to an entitlement effect. "Unchanged" means the current access flag is preserved:
| Type | Notification | Tierux status | Access | Timeline event |
|---|---|---|---|---|
| 1 | SUBSCRIPTION_RECOVERED | active | On | — |
| 2 | SUBSCRIPTION_RENEWED | active | On | renewal |
| 3 | SUBSCRIPTION_CANCELED | cancelled | Unchanged | cancellation_pending |
| 4 | SUBSCRIPTION_PURCHASED | active | On | — |
| 5 | SUBSCRIPTION_ON_HOLD | account_hold | Off | billing_retry |
| 6 | SUBSCRIPTION_IN_GRACE_PERIOD | grace_period | On | grace_period |
| 7 | SUBSCRIPTION_RESTARTED | active | On | — |
| 10 | SUBSCRIPTION_PAUSED | unknown | Off | — |
| 12 | SUBSCRIPTION_REVOKED | cancelled | Off | refund |
| 13 | SUBSCRIPTION_EXPIRED | expired | Off | expired |
✓ access on · ~ access unchanged · ✕ access off
Types 8 (price change confirmed), 9 (deferred), and 11 (pause schedule changed) are informational: they are acknowledged and write nothing. A voidedPurchaseNotification, which is a separate message shape rather than a subscription type, is covered in chapter 4.
The four types that re-query Play
For types 1, 2, 4, and 7, the table row is a starting point, not the final answer. Tierux calls subscriptionsv2.get again and overwrites status, access, and expiresAt with the fresh result from the query table above. A "renewed" notification for a subscription Play now reports as expired is therefore written as expired, and no renewal is added to the timeline. If that re-query fails, the RTDN endpoint answers 503 so Pub/Sub redelivers the message later; reprocessing is safe because of the message-id and event-time guards.
The other types — cancel, hold, grace, pause, revoke, expire — are applied from the notification alone. They update status and access but leave expiresAt as last stored. That detail matters in chapter 2.
Where the two disagree
Because the two inputs carry different information, the same Play state can land as a different Tierux status depending on which one arrived last:
- Paused. A query reports
account_hold; the pause notification writesunknown. Access is off either way. - Canceled but not yet expired. The notification writes
cancelledand keeps access on. A later query — a restore or a support refresh — writescancelledwith access off.
The practical rule is the one the state machine chapter already gives: gate on active, and treat status as the explanation — except during grace, where active can read false until a refresh (see chapter 2). The billing timeline is the place to see the sequence of events that produced it.
What reaches your systems
Each applied notification with a timeline event in the table above is appended to the subscriber's billing timeline in the dashboard, and, if you have configured an outbound webhook, posted to it as a signed POST with the event type in the X-Tierux-Event header. That delivery is best-effort and at most once — a failed POST is logged and not retried — so treat it as a prompt to read the entitlement, not as the record itself. See the webhooks docs for the contract.
The App Store has its own notification vocabulary and its own mapping onto the same six statuses; that path is implemented with limitations. See the platform support matrix before relying on parity.
Glossary: RTDN
The notification channel this chapter maps, in one paragraph.
Blog: RTDN vs polling
Why lifecycle state should arrive by push rather than by periodic re-verification.
Docs: RTDN setup
Pointing Play Console at the shared Pub/Sub topic.
Guide: The entitlement state machine
The six statuses and why active and status are separate fields.
Platform support matrix
RTDN on Google Play, App Store Server Notifications on Apple — per-feature status.
Play state, normalized
Free tier — unlimited apps, 1 paywall. No credit card, no revenue share.
Start free