Skip to main content
skill CX Codex

rc-cancellations-pauses-winback — Claude Code skill: RevenueCat

Surfaces cancellation, pause and winback state in Android apps on RevenueCat. Saves you from guessing what happened to a subscriber's access.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add RevenueCat/ai-toolkit --skill rc-cancellations-pauses-winback -a claude-code
Open on GitHub

What rc-cancellations-pauses-winback does

The skill walks through the subscription states RevenueCat puts in CustomerInfo. A cancellation with access left shows up as unsubscribeDetectedAt plus isActive == true, a payment problem as billingIssuesDetectedAt, and a pause as isActive == false while the product is still owned. From there the agent suggests what to render in the UI.

It runs in four phases: scope the state you need, confirm you already have fetched CustomerInfo, write the Kotlin, then verify. The management button gets its own section — managementURL is already typed Uri?, so pass it straight into the Intent; wrapping it in Uri.parse(url.toString()) is redundant and easy to get wrong. The pause resume date is not in the SDK at all. You pull it from /v1/subscribers/{app_user_id} as paused_expiration_time_ms, and that call belongs on your backend because it needs RC_SECRET_API_KEY.

A few things trip people up. Don't compute access by hand from expirationDate; read isActive and willRenew instead. Resubscribing before expiry clears unsubscribeDetectedAt on its own, but a resubscribe after expiry arrives as a RENEWAL webhook rather than INITIAL_PURCHASE, so your backend handler has to grant access on both. RevenueCat sends no dedicated pause webhook either — the state lands in CustomerInfo once the Google Play RTDN is processed.

What it helps with

  • show an "ends on this date" banner after a cancellation
  • open the Google Play subscription screen from a tap
  • tell a paused subscription apart from one that expired
  • fetch the pause resume date over the REST API
  • grant access when someone resubscribes after expiry

Example prompts for the agent

  • Add a cancellation banner to the subscription screen when unsubscribeDetectedAt is set and isActive is true
  • Build a "Manage subscription" button that opens managementURL
  • Write a backend call to /v1/subscribers/{app_user_id} and return paused_expiration_time_ms to the client

How to install rc-cancellations-pauses-winback

1Skills CLI (Vercel)

npx skills add RevenueCat/ai-toolkit --skill rc-cancellations-pauses-winback -a claude-code

2Manually: put the files in place

~/.claude/skills/rc-cancellations-pauses-winback/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/rc-cancellations-pauses-winback/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about rc-cancellations-pauses-winback

How do I install rc-cancellations-pauses-winback in Claude?

Run npx skills add RevenueCat/ai-toolkit --skill rc-cancellations-pauses-winback -a claude-code. The skill lands in ~/.claude/skills/rc-cancellations-pauses-winback/SKILL.md.

What does rc-cancellations-pauses-winback do?

It tells the agent how to handle cancellation, pause and winback state in an Android app on RevenueCat: which CustomerInfo fields to read, how to open the subscription management screen, and where the resume date comes from.

Do I need a RevenueCat secret key?

Yes, for the pause resume date. You request it from your backend via /v1/subscribers/{app_user_id} with an Authorization: Bearer $RC_SECRET_API_KEY header — the SDK does not carry that value.

Source text

SKILL.md · MIT
---
name: rc-cancellations-pauses-winback
description: Use this skill when surfacing cancellation, pause, and winback state on Android with RevenueCat. Covers reading unsubscribeDetectedAt, billingIssuesDetectedAt, pause state via periodType, managementURL for deep link, and pause resume date lookup via the REST API.
license: Apache-2.0; see LICENSE
metadata:
  author: RevenueCat
  source: revenuecat-handbook chapter 13
  keywords:
  - android
  - revenuecat
  - cancellation
  - pause
  - winback
  - management-url
---

# Cancellations, Pauses, and Winback

Detect cancellation, pause, and winback states on Android by reading `CustomerInfo`. Most of these events are passive: your app observes them rather than initiates them. Open Google Play's manage screen with `managementURL`. Look up the pause resume date from the REST API.

## Phase 1: Scope

Decide what state you need to surface to the user.

| State | Signal in CustomerInfo | Notes |
|---|---|---|
| Canceled, still has access | `entitlement.unsubscribeDetectedAt != null` and `isActive == true` | Show "ends on [expirationDate]" |
| Billing issue (grace or hold) | `entitlement.billingIssuesDetectedAt != null` | Payment failed, recovery in progress |
| Paused | `entitlement.isActive == false` while product is still owned | Resume date requires REST API |
| Active and renewing | `entitlement.willRenew == true` | Normal state |

Backend win back campaign segmentation uses the `CANCELLATION` webhook `cancel_reason` field (`UNSUBSCRIBE`, `BILLING_ERROR`, `DEVELOPER_INITIATED`, `PRICE_INCREASE`).

## Phase 2: Prepare

Confirm you already have a fetched `CustomerInfo` from `Purchases.sharedInstance.getCustomerInfo(...)` or a listener. You do not need to call `BillingClient.queryPurchasesAsync` with `setIncludeSuspendedSubscriptions(true)`. RevenueCat resolves pause state from the server side subscription state.

Rules:

- `managementURL` is already `Uri?`. Pass it straight to the Intent. Do not call `Uri.parse(url.toString())`.
- Do not compute access manually from `expirationDate`. Read `isActive` and `willRenew`.
- Pause resume date is not in the SDK. Fetch it from `/v1/subscribers/{app_user_id}` in your backend.

## Phase 3: Execute

### Detect cancellation with remaining access

```kotlin
val entitlement = customerInfo.entitlements["pro_access"]
if (entitlement?.unsubscribeDetectedAt != null && entitlement.isActive) {
    entitlement.expirationDate?.let { expiry ->
        showCancellationBanner(expiry)
    }
}
```

`unsubscribeDetectedAt` is set when RevenueCat receives the `SUBSCRIPTION_CANCELED` RTDN. `isActive` stays `true` until the billing period ends.

### Detect pause

```kotlin
val entitlement = customerInfo.entitlements["pro_access"]
val hasAccess = entitlement?.isActive == true
// When paused, isActive == false. Pause resume date is not in the SDK.
```

### Open the subscription management screen

```kotlin
customerInfo.managementURL?.let { url ->
    startActivity(Intent(Intent.ACTION_VIEW, url))
}
```

`managementURL` is typed as `Uri?`. Use it directly. Wrapping it with `Uri.parse(url.toString())` is redundant and error prone.

### Look up pause resume date via REST

From your backend, call the subscribers endpoint and read `paused_expiration_time_ms` on the subscription:

```bash
curl -H "Authorization: Bearer $RC_SECRET_API_KEY" \
  https://api.revenuecat.com/v1/subscribers/$APP_USER_ID
```

The subscription object contains `paused_expiration_time_ms` when paused. Expose this to your client through your own endpoint.

### Resubscribe before expiry

Google's resubscribe before expiry flow fires `SUBSCRIPTION_RESTARTED`. RevenueCat clears `unsubscribeDetectedAt` and sets `willRenew = true`. No extra code required, just re-read `customerInfo`.

### Resubscribe after expiry

A resubscribe after expiry fires a `RENEWAL` webhook, not `INITIAL_PURCHASE`. Grant entitlement access on both events in your backend handler.

## Phase 4: Verify

| Check | How |
|---|---|
| Cancellation banner shows | Cancel in Play Store, pull fresh `CustomerInfo`, confirm `unsubscribeDetectedAt != null` and `isActive == true` |
| Management deep link opens | Tap the link, verify Play subscriptions screen opens for the correct product |
| Pause is reflected | Pause in Play Store sandbox, confirm `entitlement.isActive == false` |
| Pause resume date | Call `/v1/subscribers/{id}`, confirm `paused_expiration_time_ms` is present |
| Win back accepted | Accept a Play configured win back offer, confirm the purchase surfaces as a normal subscription in `CustomerInfo` |

## Notes

- Deferral and revocation are done through the Google Play Developer API (`purchases.subscriptionsv2.defer`, `purchases.subscriptionsv2.revoke`) from your backend. RevenueCat processes the resulting RTDN and updates `CustomerInfo` automatically.
- Win back campaigns need no SDK code. They are configured in Play Console or the RevenueCat dashboard.
- RevenueCat does not currently emit a dedicated pause webhook. Pause state appears in `CustomerInfo` once the Google Play RTDN is processed.

## References

- [Full chapter](https://www.revenuecat.com/guides/revenuecat-android-sdk/cancellations-pauses-and-winback)

© RevenueCat · MIT · RevenueCat/ai-toolkit

Similar skills

Everything for Mobile →

From the same set25

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor