サブスクリプションの価格は、ほとんどの場合固定ではありません。市場環境は変化し、コストは変動し、ビジネス戦略も進化します。付加価値を反映するために価格を引き上げたり、より多くのユーザーを獲得するために価格を引き下げたりと、いずれサブスクリプション価格の調整が必要になるタイミングが訪れるでしょう。しかし、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 が提供するテストツールを使って、各フローを十分に検証することが重要です。

