Subscription States logo

Subscription States

Organization
RevenueCat
subscription-states

Use this skill when mapping a Google Play subscription to the eight states (PENDING, ACTIVE, IN_GRACE_PERIOD, ON_HOLD, PAUSED, CANCELED, EXPIRED, PENDING_PURCHASE_CANCELED) and deciding what access your app should grant in each state.

Overview

PublisherRevenueCat
Repositoryplay-billing-skills
Skill namesubscription-states
Stars
58
Forks
0
Bundled files
Instructions only
LicenseApache-2.0
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by RevenueCat on GitHub. Read the source before you install it.

Installation

Install the Subscription States AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/RevenueCat/play-billing-skills/tree/main/google-play/subscription-states
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/RevenueCat/play-billing-skills.git /tmp/play-billing-skills
mkdir -p .claude/skills
cp -r /tmp/play-billing-skills/google-play/subscription-states .claude/skills/subscription-states
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Subscription States in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Subscription States on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Subscription States is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Subscription States

A Google Play subscription moves through eight states between the first purchase and final termination. Each state has a concrete access decision: grant, retain until expiry, or revoke. Get the mapping wrong and you either give away paid content or revoke access a user already paid for.

This skill walks you through confirming your app reads the correct state source, deciding the access rule for each state, and writing the mapping code.

Phase 1: Discovery

Before changing anything, confirm where your entitlement decision reads subscription state from. The authoritative source is the subscriptionState field on the SubscriptionPurchaseV2 resource returned by purchases.subscriptionsv2.get on the Google Play Developer API. The client Purchase object from queryPurchasesAsync() does not carry this field.

Answer these questions:

  1. Does your backend call purchases.subscriptionsv2.get (not the legacy purchases.subscriptions.get)?
  2. Does your entitlement function read subscriptionState from that response rather than inferring state from the RTDN notification type?
  3. Does the function check expiryTime when the state is SUBSCRIPTION_STATE_CANCELED?
  4. Does your client code treat a Purchase returned by queryPurchasesAsync() as "has a purchase," not "has access"?

If any answer is no, fix the source of truth first. Inferring state from the notification type is the single most common cause of wrong entitlement decisions, because two different transitions can produce the same RTDN type. For example, SUBSCRIPTION_RECOVERED fires both when a user fixes payment during account hold and when a paused subscription resumes, while a payment fixed during grace period arrives as SUBSCRIPTION_RENEWED.

Confirm the app is on subscriptionsv2 before moving on.

Phase 2: Plan the Decision Per State

Decide the access rule for each of the eight states. Use this table as the contract your code must match:

StateAccess DecisionReason
PENDINGDo not grantPayment has not settled yet.
PENDING_PURCHASE_CANCELEDDo not grant on this tokenPending payment canceled or expired; if linkedPurchaseToken is set, the previous subscription keeps its own state.
ACTIVEGrantPaid and within the current billing period.
IN_GRACE_PERIODGrantPayment failed, Google is retrying, user keeps access during the configured window.
CANCELEDRetain until expiryTimeUser canceled but already paid for the current period.
ON_HOLDRevokeGrace period expired without recovery.
PAUSEDRevokeVoluntary pause, no billing during the window.
EXPIREDRevokeSubscription has ended.

Three rules flow from this table:

  • Treat CANCELED as a delayed revocation, not an immediate one. Schedule the revocation for expiryTime.
  • Treat ON_HOLD as an immediate revocation. A default queryPurchasesAsync() omits subscriptions in account hold; if you query with includeSuspendedSubscriptions(true) (PBL 8.1+), they come back with isSuspended == true and must not grant access.
  • Treat PENDING as "no access yet." Wait for the transition to ACTIVE before entitling the user.

Plan the UI message for each revoking or warning state:

  • IN_GRACE_PERIOD: show a "fix your payment" prompt with the Play Store deep link.
  • ON_HOLD: show the same prompt, since recovery is still possible until the hold expires.
  • PAUSED: show the resume date, not a payment failure message.
  • CANCELED: show a resubscribe prompt that references expiryTime.
  • EXPIRED: show a fresh purchase offer.

Phase 3: Execute

Map the state string to an access decision

Encode the decision table directly. Keep the function pure so it is easy to test.

kotlin
fun shouldGrantAccess(
    state: String,
    expiryTimeMillis: Long
): Boolean = when (state) {
    "SUBSCRIPTION_STATE_ACTIVE",
    "SUBSCRIPTION_STATE_IN_GRACE_PERIOD" -> true
    "SUBSCRIPTION_STATE_CANCELED" ->
        System.currentTimeMillis() < expiryTimeMillis
    else -> false
}

Everything not listed (ON_HOLD, PAUSED, EXPIRED, PENDING, and anything unknown) returns false. This is the safe default.

Gate the client path with the backend state

The client must not decide entitlement alone. The client cannot see grace period (it looks like a normal active purchase) and omits account hold unless includeSuspendedSubscriptions(true) is set, in which case check isSuspended.

kotlin
suspend fun checkEntitlement(
    client: BillingClient,
    api: YourBackendApi
): Boolean {
    val params = QueryPurchasesParams.newBuilder()
        .setProductType(ProductType.SUBS).build()
    val result = client.queryPurchasesAsync(params)
    val purchase = result.purchasesList.firstOrNull()
        ?: return false
    if (purchase.purchaseState == PurchaseState.PENDING) {
        return false
    }
    return api.verifyEntitlement(purchase.purchaseToken)
}

The client check filters out "no purchase" and pending payments. The backend call resolves the real subscription state.

Route the backend handler by state, not by notification type

When an RTDN arrives, fetch the subscription and branch on subscriptionState.

kotlin
fun handle(notification: SubscriptionNotification) {
    val token = notification.purchaseToken
    val sub = playApi.getSubscriptionV2(pkg, token)
    when (sub.subscriptionState) {
        "SUBSCRIPTION_STATE_ACTIVE" -> grant(token)
        "SUBSCRIPTION_STATE_IN_GRACE_PERIOD" -> grantWithWarning(token)
        "SUBSCRIPTION_STATE_CANCELED" -> scheduleRevoke(token, sub)
        "SUBSCRIPTION_STATE_ON_HOLD",
        "SUBSCRIPTION_STATE_PAUSED",
        "SUBSCRIPTION_STATE_EXPIRED" -> revoke(token)
        "SUBSCRIPTION_STATE_PENDING" -> hold(token)
        "SUBSCRIPTION_STATE_PENDING_PURCHASE_CANCELED" -> cleanUpPending(token, sub.linkedPurchaseToken)
    }
}

Schedule the CANCELED revocation at expiryTime so the user keeps the time they paid for.

kotlin
fun scheduleRevoke(token: String, sub: SubscriptionPurchaseV2) {
    val expiry = Instant.parse(
        sub.lineItems.first().expiryTime
    ).toEpochMilli()
    if (System.currentTimeMillis() >= expiry) {
        revoke(token)
    } else {
        db.setExpiry(token, expiry)
        scheduler.scheduleAt(expiry) { revoke(token) }
    }
}

Verify

Before shipping, confirm these behaviors with fixtures or a staging account:

  • A CANCELED subscription with expiryTime in the future still grants access.
  • An ON_HOLD subscription revokes access; queryPurchasesAsync() omits it by default or returns it with isSuspended == true.
  • A PENDING subscription does not grant access until it transitions to ACTIVE.
  • An unknown or new state string returns false rather than defaulting to true.

References

Frequently asked questions

What does the Subscription States AI skill do?

Use this skill when mapping a Google Play subscription to the eight states (PENDING, ACTIVE, IN_GRACE_PERIOD, ON_HOLD, PAUSED, CANCELED, EXPIRED, PENDING_PURCHASE_CANCELED) and deciding what access your app should grant in each state.

Why use Subscription States on TypingMind?

Because you install it once and use it with any model. Subscription States is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Subscription States in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/RevenueCat/play-billing-skills/tree/main/google-play/subscription-states. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Subscription States?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Subscription States?

As many as you like. As long as a model supports skills, you can use Subscription States with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Subscription States AI skill free?

Yes. It is published on GitHub by RevenueCat under the Apache-2.0 license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇