サブスクリプションの価格は、ほとんどの場合固定ではありません。市場環境は変化し、コストは変動し、ビジネス戦略も進化します。付加価値を反映するために価格を引き上げたり、より多くのユーザーを獲得するために価格を引き下げたりと、いずれサブスクリプション価格の調整が必要になるタイミングが訪れるでしょう。しかし、Google Play におけるサブスクリプション価格の変更は、ダッシュボード上の数値を更新するだけで完了するほど単純ではありません。

価格変更は、新規ユーザーと既存のサブスクライバーに対して異なる影響を与え、特定の通知フローが必要となります。また、ユーザーの信頼を維持し、Google Play のポリシーを遵守するためにも、慎重な対応が求められます。そこで本記事では、Google Play におけるサブスクリプション価格変更の仕組みを詳しく解説します。具体的には、以下の内容を取り上げます。

  • 新規および既存サブスクライバーそれぞれに対する価格変更の仕組み
  • オプトイン型とオプトアウト型の値上げの違い
  • 通知要件とそのタイムライン
  • 実装の詳細(具体的な例を交えた解説)
  • RevenueCat を活用して、サブスクライバー全体に対する価格変更をスムーズに管理する方法

価格変更が異なるサブスクライバーグループに与える影響

Google Play Console または API を通じてサブスクリプション価格を変更した場合でも、その影響はすべてのユーザーに同じように及ぶわけではありません。Google Play では、新規サブスクライバーと既存サブスクライバーを次のように異なる形で扱います。

新規サブスクライバー

新規サブスクライバーに対しては、価格変更は比較的早く反映され、通常は変更後数時間以内に有効になります。新しい価格が有効になると、新たにサブスクリプションを開始するユーザーは、更新後の価格を確認し、その価格で支払うことになります。このグループに対して特別な対応は必要ありません。購入画面に到達した時点で、常に現在の価格が表示されるだけです。

既存サブスクライバー:レガシー価格コホート

既存サブスクライバーはまったく異なる扱いになります。デフォルトでは、サブスクリプション価格を変更すると、現在のサブスクライバーは Google Play がレガシー価格コホートと呼ぶグループに分類されます。これらのユーザーは、価格変更の影響を受けることなく、更新のたびに引き続き元の価格を支払い続けます。このデフォルトの挙動は、想定外の請求変更からユーザーを保護すると同時に、いつ・どのように新しい価格へ移行させるかを、開発者がコントロールできるようにするためのものです。

このレガシーコホートの仕組みにより、Play Console 上で価格を変更しても、既存サブスクライバーが支払う金額が自動的に変更されることはありません。既存サブスクライバーを新しい価格へ移行させるには、明示的に移行を選択する必要があります。

レガシー価格コホートの終了

既存のサブスクライバーをレガシー価格から新しい価格へ移行する場合は、価格移行 API を使用します。これにより、新しい価格が現在ユーザーが支払っている金額より高いか低いかに応じて、「値上げ」または「値下げ」のフローが開始されます。

移行 API の利用

サブスクライバーを新しい価格へ移行するには、バックエンド側から monetization.subscriptions.basePlans.migratePrices エンドポイントを呼び出します。

1// Backend service for initiating price migration
2class PriceMigrationService(
3    private val androidPublisher: AndroidPublisher
4) {
5    fun migrateSubscribersToNewPrice(
6        packageName: String,
7        productId: String,
8        basePlanId: String,
9        regions: List<String>,
10        newPriceAmountMicros: Long,
11        currencyCode: String
12    ) {
13        val regionalConfigs = regions.map { regionCode ->
14            RegionalPriceMigrationConfig().apply {
15                this.regionCode = regionCode
16                this.priceIncreaseType = "OPT_IN" // or "OPT_OUT" if eligible
17                this.oldestAllowedPriceVersionTime = null // migrate all legacy cohorts
18            }
19        }
20
21        val request = MigratePricesRequest().apply {
22            this.regionalPriceMigrationConfigs = regionalConfigs
23        }
24
25        androidPublisher
26            .monetization()
27            .subscriptions()
28            .basePlans()
29            .migratePrices(packageName, productId, basePlanId, request)
30            .execute()
31    }
32}

この移行は地域ごとに設定されるため、異なる市場に対して段階的に価格変更を展開したり、地域ごとの価格差を個別に管理したりすることが可能です。

値下げのフロー

新しい価格が、ユーザーが現在支払っている金額よりも低い場合、移行プロセスはシンプルで、ユーザーにとっても負担の少ないものになります。値下げは、ユーザーの明示的な同意を必要とせず、自動的に適用されます。

値下げの仕組み

サブスクライバーを低い価格へ移行すると、Google Play は値下げが行われたことを知らせるメール通知をユーザーに送信します。その後、次回の更新時から、ユーザーは新しい低い価格で支払うことになります。値下げは自動的に適用されるため、ユーザー側での操作は一切必要ありません。

ただし、タイミングに関して注意すべき点が 1 つあります。Google Play では、更新の最大 48 時間前(インドおよびブラジルでは最大 5 日前)に支払いが承認される場合があります。値下げが適用される前に、すでに高い価格で支払いが承認されていた場合、その更新分については高い価格が請求されますが、以降の更新からは低い価格が適用されます。

アプリ内での値下げ対応

実装の観点では、値下げに対して特別な対応はほとんど必要ありません。ユーザーに対して、この変更を知らせるコミュニケーションを行うことを検討するとよいでしょう。

1class PriceChangeManager(
2    private val backendApi: BackendApi
3) {
4    suspend fun checkForPriceChanges(userId: String): PriceChangeInfo? {
5        val subscriptionStatus = backendApi.getSubscriptionStatus(userId)
6        val priceChange = subscriptionStatus.pendingPriceChange ?: return null
7
8        return when {
9            priceChange.newPriceMicros < priceChange.currentPriceMicros -> {
10                PriceChangeInfo.Decrease(
11                    currentPrice = formatPrice(priceChange.currentPriceMicros),
12                    newPrice = formatPrice(priceChange.newPriceMicros),
13                    effectiveDate = priceChange.effectiveDate
14                )
15            }
16            else -> {
17                // Handle price increase (covered in next section)
18                handlePriceIncrease(priceChange)
19            }
20        }
21    }
22}
23
24// In your UI layer
25fun showPriceDecreaseNotification(info: PriceChangeInfo.Decrease) {
26    showBanner(
27        title = "Good news!",
28        message = "Your subscription price is decreasing from ${info.currentPrice} " +
29            "to ${info.newPrice} starting ${formatDate(info.effectiveDate)}."
30    )
31}

値下げは一般的にユーザーにとって前向きな変更であるため、主な対応ポイントは同意フローの管理ではなく、価格が変更されたことをユーザーに確実に認識してもらうことです。

値上げのフロー:オプトイン型

値上げは、ユーザーへの周知が必要であり、多くの場合、明示的な同意も求められるため、より複雑になります。値上げのデフォルト方式はオプトイン型で、ユーザーが新しい価格に明確に同意しない限り、課金されることはありません。

オプトインのタイムライン

オプトイン型の値上げフローは、明確に区切られたフェーズを持つ特定のタイムラインに沿って進行します。

フェーズ期間内容
フリーズ期間1 日目〜 7 日目Google Play からの通知は送信されない。開発者は独自にユーザーへ通知可能
通知期間8 日目〜 37 日目Google Play がメールおよびプッシュ通知を送信
有効日37 日目以降値上げが有効化され、次回更新時に新しい価格で請求

冒頭の 7 日間のフリーズ期間は、意図的に設けられています。この期間中、Google Play による自動通知が始まる前に、開発者が自社のチャネルを通じてユーザーに通知することが可能です。これにより、メッセージ内容をコントロールし、値上げによってユーザーが得られる価値を説明する余地が生まれます。

ユーザーによる同意の要件

オプトイン型の値上げでは、ユーザーが新しい価格に明示的に同意する必要があります。ユーザーは、Play ストアのサブスクリプション管理画面で価格変更の説明を確認し、同意するか、または拒否するかを選択します。

新しい価格が適用される最初の更新までにユーザーが値上げに同意しなかった場合、そのサブスクリプションは自動的に解約されます。現在の請求期間が終了するまでは引き続き利用できますが、サブスクリプションは更新されません。

アプリ内でのオプトイン型値上げ対応

アプリ側では、保留中の値上げを検知し、ユーザーが同意プロセスを完了できるように案内する必要があります。

1class OptInPriceIncreaseManager(
2    private val billingClient: BillingClient,
3    private val backendApi: BackendApi
4) {
5    sealed class PriceIncreaseState {
6        object None : PriceIncreaseState()
7        data class Pending(
8            val currentPrice: String,
9            val newPrice: String,
10            val effectiveDate: Instant,
11            val inFreezePeriod: Boolean
12        ) : PriceIncreaseState()
13        object Accepted : PriceIncreaseState()
14        object Declined : PriceIncreaseState()
15    }
16
17    suspend fun checkPriceIncreaseStatus(userId: String): PriceIncreaseState {
18        val subscriptionStatus = backendApi.getSubscriptionStatus(userId)
19        val priceChange = subscriptionStatus.pendingPriceChange
20
21        if (priceChange == null || priceChange.newPriceMicros <= priceChange.currentPriceMicros) {
22            return PriceIncreaseState.None
23        }
24
25        return when (priceChange.state) {
26            "OUTSTANDING" -> {
27                val freezePeriodEnd = priceChange.initiatedAt.plus(Duration.ofDays(7))
28                PriceIncreaseState.Pending(
29                    currentPrice = formatPrice(priceChange.currentPriceMicros),
30                    newPrice = formatPrice(priceChange.newPriceMicros),
31                    effectiveDate = priceChange.effectiveDate,
32                    inFreezePeriod = Instant.now().isBefore(freezePeriodEnd)
33                )
34            }
35            "CONFIRMED" -> PriceIncreaseState.Accepted
36            "CANCELED" -> PriceIncreaseState.Declined
37            else -> PriceIncreaseState.None
38        }
39    }
40
41    fun showPriceIncreaseUI(
42        activity: Activity,
43        state: PriceIncreaseState.Pending
44    ) {
45        if (state.inFreezePeriod) {
46            // During freeze period, show your own messaging
47            showCustomPriceIncreaseDialog(
48                currentPrice = state.currentPrice,
49                newPrice = state.newPrice,
50                effectiveDate = state.effectiveDate,
51                onAcceptClick = { openPlayStoreSubscriptionSettings(activity) }
52            )
53        } else {
54            // After freeze period, can also use Google's in app messaging
55            showInAppMessage(activity)
56        }
57    }
58
59    private fun showInAppMessage(activity: Activity) {
60        val params = InAppMessageParams.newBuilder()
61            .addInAppMessageCategoryToShow(
62                InAppMessageParams.InAppMessageCategoryId.SUBSCRIPTION_PRICE_CHANGE
63            )
64            .build()
65
66        billingClient.showInAppMessages(activity, params) { result ->
67            // Handle the result
68            when (result.responseCode) {
69                InAppMessageResult.InAppMessageResponseCode.NO_ACTION_NEEDED -> {
70                    // No price change message needed or user already responded
71                }
72                InAppMessageResult.InAppMessageResponseCode.SUBSCRIPTION_STATUS_UPDATED -> {
73                    // User interacted with the message - refresh subscription status
74                    refreshSubscriptionStatus()
75                }
76            }
77        }
78    }
79
80    private fun openPlayStoreSubscriptionSettings(activity: Activity) {
81        val intent = Intent(Intent.ACTION_VIEW).apply {
82            data = Uri.parse(
83                "<https://play.google.com/store/account/subscriptions>"
84            )
85            setPackage("com.android.vending")
86        }
87        activity.startActivity(intent)
88    }
89}

値上げ時に価値を伝えるコミュニケーション

フリーズ期間は、なぜ価格が引き上げられるのかをユーザーに対して直接伝えるための重要な機会です。この期間に適切なコミュニケーションを行うことで、値上げへの同意率を大きく向上させることができます。

1fun showCustomPriceIncreaseDialog(
2    currentPrice: String,
3    newPrice: String,
4    effectiveDate: Instant,
5    onAcceptClick: () -> Unit
6) {
7    showDialog(
8        title = "Subscription Update",
9        message = """
10            Starting ${formatDate(effectiveDate)}, your subscription will change
11            from $currentPrice to $newPrice per month.
12
13            Since you subscribed, we've added:
14            • Advanced analytics dashboard
15            • Offline mode for all content
16            • Priority customer support
17            • And 15+ other features
18
19            To continue enjoying these features, please confirm the new price
20            in your Play Store subscription settings.
21        """.trimIndent(),
22        positiveButton = "Review in Play Store" to onAcceptClick,
23        negativeButton = "Maybe Later" to { /* dismiss */ }
24    )
25}

値上げのフロー:オプトアウト型

特定の地域および一定の条件下では、Google Play でオプトアウト型の値上げが許可されています。オプトアウト型の値上げでは、価格変更についてユーザーに通知は行われますが、ユーザーが明示的に解約やプラン変更を行わない限り、自動的に新しい価格で課金されます。

適用条件

オプトアウト型の値上げは、すべてのケースで利用できるわけではありません。利用可否は、地域ごとの対応状況(オプトアウト型値上げに対応している国は一部に限られます)、この方式を使用できる頻度の制限、国ごとに定められた値上げ率または金額の上限、さらに開発者側に求められる追加の適格要件など、複数の要因によって決まります。これらの制約があるため、オプトアウト型の値上げは、価格変更の主な手段というよりも、補助的な選択肢として考えるべきものです。

オプトアウトのタイムライン

オプトアウト型の値上げは、オプトイン型とは異なるタイムラインで進行します。

項目オプトインオプトアウト
フリーズ期間7 日間なし
通知期間30 日間30 日または 60 日(国によって異なる)
ユーザーによる操作同意が必要回避するには解約が必要
デフォルトの挙動サブスクリプションが解約される新しい価格で課金される

オプトアウト型における通知期間は国によって異なります。30 日前の通知が求められる国もあれば、60 日前の通知が必要な国もあります。これらの地域ごとの要件は、オプトアウト移行を開始すると、Google Play によって自動的に処理されます。

オプトアウト型値上げへの対応

実装の観点では、オプトアウト型の値上げは、サブスクリプションを継続するためにユーザー側で特別な操作を行う必要がないため、比較的シンプルです。

1fun handleOptOutPriceIncrease(priceChange: PriceChangeInfo) {
2    // For opt-out increases, the state will be "CONFIRMED" rather than "OUTSTANDING"
3    // Users will be charged the new price automatically unless they cancel
4
5    showNotification(
6        title = "Subscription Price Update",
7        message = "Starting ${formatDate(priceChange.effectiveDate)}, " +
8            "your subscription will be ${priceChange.newPrice}/month. " +
9            "No action needed to continue your subscription."
10    )
11}

ただし、ユーザーの操作が不要であっても、今後予定されている価格変更については、ユーザーに対して明確に伝える必要があります。

アプリ内通知の要件

オプトイン型・オプトアウト型のいずれの値上げ方式を使用する場合でも、Google Play では、価格変更に関するアプリ内通知を表示することが求められます。この要件は、アプリが動作するすべてのデバイスタイプに適用されます。

必須となる通知表示先

価格変更に関する通知は、モバイルデバイス(スマートフォンおよびタブレット)、Android TV、その他のストリーミングデバイスで表示する必要があります。唯一の例外はウォッチデバイスで、画面サイズが限られているため、アプリ内通知は推奨されてはいるものの、必須ではありません。

通知のタイミング

オプトイン型の値上げの場合、推奨される対応は、フリーズ期間(1 日目から 7 日目)に、値上げの理由や提供価値を説明する独自のメッセージを表示し、その後、フリーズ期間終了後(8 日目以降)は、リマインダーの表示を継続しつつ、Google の In-App Messaging API を使用することです。

1class PriceChangeNotificationManager(
2    private val billingClient: BillingClient
3) {
4    fun showPriceChangeNotificationIfNeeded(
5        activity: Activity,
6        priceIncreaseState: PriceIncreaseState
7    ) {
8        when (priceIncreaseState) {
9            is PriceIncreaseState.Pending -> {
10                // Always show some form of notification for pending increases
11                if (priceIncreaseState.inFreezePeriod) {
12                    showCustomNotificationBanner(activity, priceIncreaseState)
13                } else {
14                    // Use Google's in app messaging
15                    showGoogleInAppMessage(activity)
16                }
17            }
18            else -> {
19                // No notification needed
20            }
21        }
22    }
23
24    private fun showCustomNotificationBanner(
25        activity: Activity,
26        state: PriceIncreaseState.Pending
27    ) {
28        // Show a subtle banner at the top of the screen
29        val banner = PriceChangeBanner(activity).apply {
30            setMessage(
31                "Your subscription price will change to ${state.newPrice} " +
32                "on ${formatDate(state.effectiveDate)}. Tap to review."
33            )
34            setOnClickListener {
35                openPriceChangeDetails(activity, state)
36            }
37        }
38        banner.show()
39    }
40
41    private fun showGoogleInAppMessage(activity: Activity) {
42        val params = InAppMessageParams.newBuilder()
43            .addInAppMessageCategoryToShow(
44                InAppMessageParams.InAppMessageCategoryId.SUBSCRIPTION_PRICE_CHANGE
45            )
46            .build()
47
48        billingClient.showInAppMessages(activity, params) { /* handle result */ }
49    }
50}

価格変更が重複した場合の対応

以前の価格変更がまだ保留中の状態で、新たな価格変更を開始した場合はどうなるのでしょうか。Google Play では、先に進行中だった価格変更をキャンセルし、新しい価格変更を適用することで対応します。

キャンセルおよび置き換えのフロー

価格変更が重複した場合、以前の価格移行は CANCELED としてマークされ、 SUBSCRIPTION_PRICE_CHANGE_UPDATED の Real-Time Developer Notification (RTDN) が送信されます。その後、新しい価格移行が有効になり、ユーザーは最新の価格変更にのみ対応すればよくなります。この挙動により、ユーザーが複数の連続した値上げに同意することを求められる状況が回避され、ユーザー体験の悪化を防ぐことができます。

価格変更ステータスの追跡

バックエンド側では、RTDN 通知を処理して価格変更のステータスを追跡する必要があります。

1// Backend notification handler
2class PriceChangeNotificationHandler(
3    private val subscriptionRepository: SubscriptionRepository,
4    private val playDeveloperApi: AndroidPublisher
5) {
6    fun handlePriceChangeNotification(notification: DeveloperNotification) {
7        val purchaseToken = notification.subscriptionNotification.purchaseToken
8
9        when (notification.subscriptionNotification.notificationType) {
10            NotificationType.SUBSCRIPTION_PRICE_CHANGE_UPDATED -> {
11                // Query the current state of the price change
12                val subscription = playDeveloperApi
13                    .purchases()
14                    .subscriptionsv2()
15                    .get(packageName, purchaseToken)
16                    .execute()
17
18                val priceChangeState = subscription.lineItems[0]
19                    .autoRenewingPlan
20                    ?.priceChangeDetails
21
22                if (priceChangeState != null) {
23                    subscriptionRepository.updatePriceChangeStatus(
24                        purchaseToken = purchaseToken,
25                        state = priceChangeState.priceChangeState,
26                        newPriceMicros = priceChangeState.newPrice?.priceMicros,
27                        expectedNewPriceChargeTime = priceChangeState.expectedNewPriceChargeTime
28                    )
29
30                    // Notify app layer to update UI if needed
31                    notifyPriceChangeUpdated(purchaseToken, priceChangeState)
32                }
33            }
34        }
35    }
36}

誤って行った価格変更からの復旧

ミスは起こり得るものです。意図せず価格を変更してしまった場合や、想定していない価格移行を開始してしまった場合、復旧の方法は変更の種類や、どれだけ時間が経過しているかによって異なります。

オプトイン型値上げの取り消し

オプトイン型の値上げでは、取り消しを行うタイミングが非常に重要になります。7 日以内(フリーズ期間中)に取り消した場合、Google Play からユーザーへの通知は一切送信されていないため、この変更は実質的にユーザーから見えないものになります。7 日を過ぎてから取り消した場合、新しい価格でまだ課金されていないユーザーについては価格変更がキャンセルされますが、すでに通知を受け取っているユーザーも存在する可能性があり、混乱を招くことがあります。

オプトアウト型値上げの取り消し

オプトアウト型の値上げの場合、元の価格に戻すことで、新しい価格でまだ課金されていないユーザーに対しては値上げがキャンセルされます。ただし、支払い承認のタイミングには注意が必要です。地域によっては、更新の最大 5 日前に支払いが承認されることがあり、すでに承認されているユーザーについては、引き続き課金される可能性があります。

値下げの取り消し

値下げをキャンセルして元の高い価格に戻す必要がある場合は、まず Play Console 上で元の価格に戻し、その値上げをオプトイン型にするかオプトアウト型にするかを選択します。その後の結果は、取り消しとユーザーの更新タイミングとの関係によって決まります。取り消しからユーザーの更新までの期間が通知ウィンドウ(国によって 30〜60 日)よりも長い場合、ユーザーは次回の更新時に元の価格を支払います。一方、その期間が通知ウィンドウより短い場合は、ユーザーは一度だけ低い価格で課金され、その後、通常の値上げ通知フローに進むことになります。

分割払いサブスクリプションと価格変更

サブスクリプションで分割払いプラン(一定回数の支払いを前提とするプラン)を使用している場合、価格変更の挙動は通常のサブスクリプションとは異なります。

分割払いサブスクリプションでは、価格変更は、現在有効な契約期間が終了した時点でのみ適用されます。分割払いの途中にあるユーザーに対して価格を変更することはできず、新しい価格は、契約が終了した後、最初の更新時に有効になります。たとえば、12 か月の分割払いプランの後に月次の自動更新へ移行するサブスクリプションの場合、どのような価格変更を行っても、その変更が反映されるのは、12 か月の契約期間が完了し、月次更新フェーズに移行してからになります。

価格変更のテスト

本番環境のサブスクライバーに価格変更を展開する前に、Google Play が提供するテストツールを使って、各フローを十分に検証することが重要です。

ライセンステスターの利用

ライセンステスターは、実際のサブスクライバーに影響を与えることなく、価格変更に関する通知を受け取ることができます。Play Console でライセンステスターを設定し、通知の配信やタイミング、アプリ内メッセージの表示、同意および拒否のフロー、バックエンドにおける状態遷移などを確認してください。

Play Billing Lab

Google は、課金シナリオをテストするための Play Billing Lab アプリを提供しています。これを使用して価格変更のシナリオをシミュレーションし、アプリが各状態を正しく処理できているかを検証します。

監査ログ

Play Console では、すべての価格変更について変更履歴が記録されます。このログを利用することで、価格がいつ更新されたのか、誰が変更を行ったのか、どの地域が影響を受けたのかを確認できます。この監査ログは、問題の調査や、誤って行われた変更の履歴を振り返る際に非常に有用です。

RevenueCat が価格変更管理をどのように簡素化するか

大規模なサブスクライバーベースに対して価格変更を管理するには、高い複雑性が伴います。具体的には、ユーザーごとの移行ステータスの追跡、RTDN 通知の処理、適切なアプリ内メッセージの表示、そしてさまざまなエッジケースへの対応が必要になります。RevenueCat は、こうした複雑さの多くを抽象化しつつ、価格変更を効果的に管理するための追加ツールを提供します。

自動的な状態管理

RevenueCat は、Google Play の RTDN 通知を開発者に代わって処理し、保留中の価格変更を含む最新のサブスクリプション状態を維持します。通知を受信・処理するための独自のインフラを構築する代わりに、RevenueCat に対して現在のカスタマー状態を問い合わせるだけで済みます。

1fun checkForPriceChanges() {
2    Purchases.sharedInstance.getCustomerInfoWith { customerInfo ->
3        val entitlement = customerInfo.entitlements["premium"]
4
5        // RevenueCat's CustomerInfo reflects current subscription state
6        // including any pending price changes processed from RTDN
7
8        if (entitlement?.isActive == true) {
9            // Check for billing issues that might indicate price change problems
10            entitlement.billingIssueDetectedAt?.let { issueDate ->
11                showBillingRecoveryUI(customerInfo.managementURL)
12            }
13        }
14    }
15}

ユーザー操作のための管理 URL

RevenueCat の CustomerInfo には、 managementURL プロパティが含まれており、Google Play のサブスクリプション管理画面への直接リンクが提供されます。ユーザーは、この画面で価格変更への同意または拒否を行います。

1fun guideToPriceChangeAcceptance(customerInfo: CustomerInfo) {
2    val managementUrl = customerInfo.managementURL
3
4    showDialog(
5        title = "Action Required",
6        message = "Please review the upcoming changes to your subscription.",
7        positiveButton = "Open Settings" to {
8            openUrl(managementUrl)
9        }
10    )
11}

クロスプラットフォームでの一貫性

複数のプラットフォームにまたがるアプリの場合でも、RevenueCat は Android、iOS、その他のプラットフォーム間でサブスクリプション状態の一貫性を確保します。ユーザーがいずれかのデバイスで価格変更に同意すると、その更新された状態は追加の実装なしで、すべてのデバイスに反映されます。

サーバーサイド処理のための Webhook

RevenueCat は、サブスクリプションイベントを正規化された形式でサーバーに通知する Webhook を提供します。これは、生の RTDN 通知を直接処理するよりもシンプルで、価格変更に関連するイベントも含まれています。

1// Your webhook handler
2fun handleRevenueCatWebhook(event: WebhookEvent) {
3    when (event.type) {
4        "RENEWAL" -> {
5            // Renewal completed - check if price changed
6            val transaction = event.transaction
7            if (transaction.priceInPurchasedCurrency != previousPrice) {
8                // Price change took effect
9                updateUserPricing(event.appUserId, transaction.priceInPurchasedCurrency)
10            }
11        }
12        "CANCELLATION" -> {
13            // User canceled, might be due to price change rejection
14            val reason = event.cancellationReason
15            if (reason == "PRICE_INCREASE") {
16                // Track churn related to price changes
17                analytics.trackPriceChangeChurn(event.appUserId)
18            }
19        }
20    }
21}

価格変更の影響を把握するための分析機能

RevenueCatのChart では、価格変更の影響を把握するための分析データを確認できます。これには、価格変更のタイミングと関連づけた解約率、値上げ通知を受け取ったサブスクライバーのコンバージョン率、価格変更前後の収益への影響分析などが含まれます。これらのインサイトは、価格戦略に関する意思決定を行い、今後の価格変更において最適なタイミングやコミュニケーション方法を見極めるのに役立ちます。

RevenueCat を使う際のベストプラクティス

RevenueCat を価格変更管理に利用する場合は、カスタマー属性を活用して保留中の価格変更があるユーザーを特定し、フリーズ期間中に対象を絞ったコミュニケーションを行うなど、積極的な情報提供を重視してください。また、価格変更通知から同意に至るまでのコンバージョン率や、影響を受けたユーザーの解約率といった主要な指標を継続的にモニタリングすることも重要です。さらに、managementURL を通じて、ユーザーが Play ストアのサブスクリプション設定へ簡単にアクセスできる導線を常に用意しておくべきです。最後に、RevenueCat が提供する正規化されたデータを活用することで、値上げが保留中の状態でプラン変更が行われた場合などのエッジケースにも、無理なく対応できます。

サブスクリプション価格変更のベストプラクティス

これまでに解説してきた仕組みを踏まえ、価格変更を成功させるために実践すべき重要なポイントを紹介します。

コミュニケーション戦略を計画する

価格変更を開始する前に、影響を受けるユーザーにどのように伝えるかを計画しておきましょう。ユーザーが得られる価値を説明するメッセージを用意し、フリーズ期間を使って独自の案内を行うかどうかを検討します。また、価格変更に敏感なユーザー向けに、年額プランや既存価格を維持する選択肢などの代替案を提示することも考えられます。

地域ごとに段階的に展開する

グローバルに価格を引き上げる場合は、地域ごとに段階的に展開することを検討してください。まずは規模の小さい市場でテストを行い、反応を確認したうえで、初期のフィードバックをもとにメッセージを調整します。最大規模の市場に影響が及ぶ前に、問題へ対応する時間を確保することが重要です。

同意率をモニタリングする

オプトイン型の値上げに対して、どれだけのユーザーが同意し、どれだけのユーザーがサブスクリプションを解約しているかを追跡します。

1class PriceChangeAnalytics(
2    private val analytics: AnalyticsService
3) {
4    fun trackPriceChangeOutcome(
5        userId: String,
6        originalPrice: Long,
7        newPrice: Long,
8        outcome: PriceChangeOutcome
9    ) {
10        analytics.track(
11            event = "price_change_outcome",
12            properties = mapOf(
13                "user_id" to userId,
14                "original_price_micros" to originalPrice,
15                "new_price_micros" to newPrice,
16                "increase_percentage" to calculatePercentage(originalPrice, newPrice),
17                "outcome" to outcome.name
18            )
19        )
20    }
21}
22
23enum class PriceChangeOutcome {
24    ACCEPTED,
25    DECLINED,
26    SUBSCRIPTION_CANCELED,
27    NO_RESPONSE_BEFORE_DEADLINE
28}

同意率が想定より低い場合は、コミュニケーション戦略の見直しや、値上げ幅そのものの再検討が必要になるかもしれません。

代替案を用意する

より高い価格を支払う意思がないユーザーでも、別のプランであれば継続する可能性があります。価格を抑えたプランへのダウングレード導線を用意したり、実質的な月額単価を下げた年額プランを提供したり、解約を防ぐための「ライト」プランを新たに用意することも有効です。

アプリ内での移行をスムーズに処理する

価格変更中のあらゆる状態を、アプリが適切に処理できるようにしておきましょう。値上げが保留中のユーザーには分かりやすいメッセージを表示し、値上げを拒否したユーザーが即座に利用できなくなることがないよう配慮します。また、同意したユーザーにとっては、価格の切り替えが違和感なく行われることが重要です。

まとめ

Google Play におけるサブスクリプション価格変更は、ユーザーを保護しつつ、開発者に柔軟性を提供するために設計された、慎重に構成されたプロセスです。理解しておくべき重要なポイントは、既存サブスクライバーを自動的な価格変更から守るレガシー価格コホートの仕組み、オプトイン型とオプトアウト型の値上げの違いとそれぞれのタイムライン、デバイスタイプを横断して適用される通知要件、そしてタイミングや適用可否に影響する地域ごとの違いです。

値下げの場合、プロセスは比較的シンプルです。ユーザーは次回の更新時から自動的に低い価格で課金されます。一方、値上げの場合は、デフォルトでオプトイン型フローが適用され、7 日間のフリーズ期間の後に 30 日間の Google Play 通知期間を経て、有効日を迎えます。このフローでは、ユーザーの明示的な同意が必要になります。

価格変更を直接実装するには、RTDN 通知の処理、バックエンドでのサブスクリプション状態の管理、そしてユーザーが同意プロセスを進められるようにするための UI 構築が必要です。RevenueCat は、これらの通知処理を自動で行い、正規化されたデータ、分析機能、さらにユーザー操作のための管理 URL を提供することで、この複雑さを大幅に軽減します。その結果、開発者はビジネスや、より重要な取り組みに集中することができます。

どのようなアプローチを取るにしても、価格変更を成功させるためには、ユーザーが受け取る価値について明確に伝えることが不可欠です。フリーズ期間は、Google Play の自動通知が始まる前に、メッセージの方向性を自らコントロールできる貴重な機会です。この期間を有効に活用することで、アプリが提供する価値を反映した価格調整を行いながら、ユーザーの信頼を維持することができます。サブスクリプション価格変更に関する完全なドキュメントについては、Android Developer の公式ドキュメントおよび RevenueCat のサブスクリプション管理ガイドを参照してください。