非自発的チャーンという問題

すべての解約が、離れたいと望んだ顧客によるものとは限りません。その大きな割合は、単なる支払いの失敗です。カードの有効期限切れ、残高不足、銀行による拒否などが原因です。ユーザーは解約を選んでいないのに、回復のしくみがなければ気づかないうちにアクセスを失い、収益も失われてしまいます。

RevenueCat の State of Subscription Apps 2026 によると、Google Play のサブスクリプション解約の約 3 分の 1 が、非自発的な請求失敗でした(App Store の割合のおよそ 2 倍です)。同レポートの言い方を借りれば、請求の修正は最も明確な成長のレバーの一つです。このコードラボでは、その方法を紹介します。

このコードラボで扱う内容は次のとおりです。

  • App Store Connect と Google Play で請求の猶予期間を有効にし、ストアがカードをリトライする間もアクセスを継続させます。
  • RevenueCat で請求リトライ中のアクティブなサブスクリプションを検知します(iOS、Android、React Native)。
  • Customer Center またはストアの managementURL で、ユーザーに支払い方法を修正してもらいます。
  • BILLING_ISSUE Webhook に対応し、アプリの外でユーザーを再エンゲージします。
設定が中心で、コードはわずか。 ここで最も大きな効果を生むのは、ストアとダッシュボードの設定です。アプリ内のコードは、短い検知チェックと、そのまま組み込める Customer Center だけです。

自発的チャーンと非自発的チャーン

まったく異なる 2 つの出来事が、どちらも「サブスクリプションが終了した」として表れます。両者は別々に扱いましょう。

種類 原因 取り戻す方法
自発的 ユーザーが解約をタップ 解約フローでのオファー、ウィンバック、価値のリマインド
非自発的 更新時に支払いが失敗 猶予期間、請求リトライ、支払い修正の促し

このコードラボが対象とするのは非自発的チャーンです。回復の連鎖はこうです。ストアは一定の期間(猶予期間)にわたってカードのリトライを続けます。その間、ユーザーはアクセスを保ちます。そしてアプリは、期間が終わる前に支払い方法を更新するよう促します。

RevenueCat が両者を自動で区別します。 自発的な解約ではエンタイトルメントに unsubscribeDetectedAt が設定され、非自発的な失敗では billingIssueDetectedAt が設定されます。後者はステップ 6 で使います(自発的チャーンは別の Customer Center コードラボで扱います)。

App Store Connect で猶予期間を有効にする

請求の猶予期間を設定すると、Apple が失敗した更新をリトライする間もサブスクライバーはアクセスを保てます。Apple が期限内に支払いを回復できれば、サービスにも収益にも中断は生じません。

  1. App Store Connect でアプリを開き、サイドバーの Subscriptions をクリックします。
  2. Billing Grace Period セクションで Set Up Billing Grace Period をクリックします。
  3. 期間を選びます。3、16、28 日のいずれかです(アプリのすべてのサブスクリプションに適用されます)。
  4. 更新の種類(すべての更新、または有料から有料への更新のみ)と環境(サンドボックスのみ、または本番とサンドボックス)を選びます。
  5. Confirm をクリックします。
週次サブスクリプションには上限があります。 16 日や 28 日を選んでも、週次サブスクリプションの猶予期間は最大 6 日までです。猶予がサブスクリプション自体より長くならないようにするためです。Account Holder、Admin、または App Manager のロールが必要です。
iOS 16.4 以降は自動でも支援します。 猶予期間が有効な場合、アプリ内で用意するしくみに加えて、iOS 自体がリトライ期間中に請求の問題を修正するよう顧客に促せます。

Google Play で猶予期間とアカウントの保留を有効にする

Google Play には 2 つの回復フェーズがあります。猶予期間中は、Google がカードをリトライする間もユーザーはアクセスを保ちます。それが失敗すると、サブスクリプションはアカウントの保留へ移り、Google がリトライを続ける間、ユーザーはアクセスを失います。どちらのフェーズでも回復できれば、サブスクリプションはそのまま継続します。

  1. Google Play Console で Monetize with Play → Products → Subscriptions に移動します。
  2. サブスクリプションを開き、自動更新の基本プランを選びます。猶予期間とアカウントの保留は基本プランごとに設定します。
  3. 猶予期間(更新の支払いが未解決の間、ユーザーがアクセスを保てる長さ)を設定します。
  4. アカウントの保留の長さ(アクセスを一時停止したあと、Google がリトライを続ける長さ)を設定します。
アカウントの保留は現在は自動計算されます。 2025 年 12 月 1 日以降、Google はアカウントの保留の長さを、固定の 30 日ではなく60 日から猶予期間を引いた値としてデフォルトで計算します。猶予期間とアカウントの保留の合計は、少なくとも 30 日である必要があります。値をハードコードせず、Console で実際の値を読み取ってください。
アクセスのルール: 猶予期間中はアクセスを維持し、アカウントの保留中はアクセスをブロックします。RevenueCat がこれを自動で処理します。エンタイトルメントは猶予期間中はアクティブのままで、保留になると非アクティブになります(詳しくは次のステップで)。

RevenueCat が請求の問題をどう表すか

検知をシンプルにする重要なポイントがこれです。サブスクリプションが猶予期間にある間、RevenueCat はエンタイトルメントを isActive: true のまま保ち(ユーザーはまだアクセスできます)、billingIssueDetectedAt に失敗が検知された時刻を設定します。

つまり、「支払っている顧客だが、カードが失敗したばかり」というシグナルは次のとおりです。

text
entitlement.isActive == true  AND  entitlement.billingIssueDetectedAt != null
        =>  active subscriber, in billing retry, nudge them to fix payment

エンタイトルメント上の関連フィールドは次のとおりです(名前はプラットフォーム間で共通です)。

  • isActive: 顧客が現在アクセスできるかどうか。猶予期間中は true のままです。
  • billingIssueDetectedAt: 支払い失敗の検知時に設定され、支払いが回復すると解除される日時。問題がないときは null です。
  • willRenew: サブスクリプションが更新される設定かどうか。
  • unsubscribeDetectedAt: 自発的な解約で設定されます(請求の問題と混同しないでください)。
CustomerInfo での猶予期間と保留。 App Store または Google Play の猶予期間中は、エンタイトルメントはアクティブで billingIssueDetectedAt が設定されています。Google がアカウントの保留へ移ると、エンタイトルメントはアクティブでなくなるため、通常のエンタイトルメントチェックがすでにアクセスをブロックします。

アプリ内で請求リトライを検知する

現在の CustomerInfo を読み取り、pro エンタイトルメントに請求の問題のシグナルがあるか確認します。

iOS (Swift)

swift
let info = try await Purchases.shared.customerInfo()

if let pro = info.entitlements["pro"], pro.isActive {
    if pro.billingIssueDetectedAt != nil {
        // Active subscriber whose payment is failing: show a "fix payment" banner.
        showBillingIssueBanner()
    }
}

Android (Kotlin)

kotlin
val info = Purchases.sharedInstance.awaitCustomerInfo()
val pro = info.entitlements["pro"]

if (pro?.isActive == true && pro.billingIssueDetectedAt != null) {
    // Active subscriber in billing retry.
    showBillingIssueBanner()
}

React Native (TypeScript)

tsx
const info = await Purchases.getCustomerInfo();
const pro = info.entitlements.active['pro'];

if (pro && pro.billingIssueDetectedAt != null) {
  // Active subscriber in billing retry.
  showBillingIssueBanner();
}
常に最新に保ちましょう。 アプリのフォアグラウンド復帰時に確認し、モバイルでは CustomerInfo の更新も購読して、支払いが回復した瞬間にバナーが消えるようにします(React Native のパターンは CustomerInfo リスナーガイドを参照してください)。

Customer Center でユーザーに支払いを修正してもらう

問題を検知したら、ワンタップで修正できる手段をユーザーに用意します。方法は 2 つあります。

選択肢 A: Customer Center(推奨)

Customer Center は、そのまま組み込める RevenueCat のセルフサービス型サブスクリプション管理 UI です。ユーザーをサブスクリプションの管理と支払いの更新へ案内し、購入の復元、解約、リテンションオファーも扱います。

swift
// iOS (SwiftUI) - RevenueCatUI
import RevenueCatUI

struct SettingsView: View {
    @State private var showCustomerCenter = false

    var body: some View {
        Button("Manage Subscription") { showCustomerCenter = true }
            .sheet(isPresented: $showCustomerCenter) {
                CustomerCenterView()
            }
    }
}
tsx
// React Native - react-native-purchases-ui
import RevenueCatUI from 'react-native-purchases-ui';

await RevenueCatUI.presentCustomerCenter();

Android では、purchases-uiCustomerCenter Composable を描画します。

Customer Center には Pro または Enterprise プランが必要です。ダッシュボードの Project Settings → Monetization Tools → Customer Center で設定します。

選択肢 B: ストアの管理ページを直接開く

Customer Center を使わない場合は、customerInfo.managementURL でユーザーをストアのサブスクリプション管理ページへ直接送れます。この URL は、RevenueCat が正しいストア向けに自動で埋めてくれます。

tsx
// React Native
import { Linking } from 'react-native';

const info = await Purchases.getCustomerInfo();
if (info.managementURL) {
  await Linking.openURL(info.managementURL);
}

iOS では customerInfo.managementURLUIApplication.shared.open(_:) で開き、Android では ACTION_VIEW インテントで開きます。 サブスクリプション管理ガイドを参照してください。

managementURL は null になることがあります。アクティブなストアサブスクリプションがないときや、Amazon や Stripe ではサポートされていません。開く前に必ず null をチェックしてください。

請求の問題の Webhook に対応する

アプリ内バナーが役立つのは、アプリを開くユーザーだけです。それ以外のユーザーに届けるには、サーバーで RevenueCat の Webhook を使い、メールやプッシュ通知をトリガーします。

  • BILLING_ISSUE: 支払い失敗が検知された時点で発火します。RevenueCat は問題ごとに 1 回送信します。猶予期間が設定されていれば、回復の期限である grace_period_expiration_at_ms が含まれます。
  • cancel_reason: BILLING_ERROR を伴う CANCELLATION: これも失敗が検知されたときに発火します。
  • expiration_reason: BILLING_ERROR を伴う EXPIRATION: 猶予期間が回復のないまま終了した場合のみ発火します。ここでようやくアクセスを削除します。
  • RENEWAL: 支払いが回復したときに発火し、billingIssueDetectedAt が解除されます。
typescript
// Your webhook endpoint (Express-style)
app.post('/revenuecat/webhook', (req, res) => {
  const event = req.body.event;

  switch (event.type) {
    case 'BILLING_ISSUE':
      // Card failed but access continues. Email "update your payment method"
      // with the deadline from event.grace_period_expiration_at_ms.
      sendFixPaymentEmail(event.app_user_id, event.grace_period_expiration_at_ms);
      break;
    case 'RENEWAL':
      // Payment recovered. Stop the dunning sequence.
      clearDunning(event.app_user_id);
      break;
  }

  res.sendStatus(200);
});
タイミングは猶予期間に左右されます。 猶予期間が設定されていると、EXPIRATION (BILLING_ERROR) は猶予期間が終わるまで遅延され、督促(dunning)シーケンスが効く時間が生まれます。設定がなければ BILLING_ISSUE と同時にすぐ発火し、アクセスは一度に失われます。これこそ、ステップ 3 と 4 で設定した猶予期間が重要な理由です。

サンドボックスでテストする

リリース前にループ全体を検証しましょう。

  1. App Store Connect で Sandbox(または本番とサンドボックス)の猶予期間を有効にし、サンドボックステスターで動作を試せるようにします。
  2. Google Play では、Closed testing トラックでライセンステスターを使い、サブスクリプションを猶予期間と保留に入れます。
  3. 更新の失敗を発生させ、アプリで pro エンタイトルメントがまだ isActivebillingIssueDetectedAt が設定されていること、そしてバナーが表示されることを確認します。
  4. サーバーが grace_period_expiration_at_ms 付きの BILLING_ISSUE Webhook を受け取ったことを確認します。
  5. 支払いを回復させ、バナーが消えて RENEWAL Webhook が届くことを確認します。
サンドボックスの時間は圧縮され、ストアが制御します。 サンドボックスでの更新やリトライの間隔は加速され、正確なタイミングはストアが決めます。特定のスケジュールを前提にせず、実際の動作を確認してください。

まとめ

非自発的チャーンを取り戻す一連の回復フローを構築しました。

  • 両ストアの猶予期間が、カードのリトライ中も支払っているユーザーのアクセスを保ちます。
  • RevenueCat がその状態をアクティブなエンタイトルメント + billingIssueDetectedAtとして表し、これをアプリ内で検知します。
  • Customer Center(または managementURL)が、ワンタップで支払いを更新する手段をユーザーに与えます。
  • Webhook がアプリ外の督促を動かし、猶予期間がそのタイミングに余裕を持たせます。

これは、実施できる変更の中でもとりわけ効果の高いものの一つです。離れたいとは思っていなかった顧客から、収益を取り戻すのですから。

さらに進む