すべての記事
Deep dive読了まで 8 分

返金後にアクセスを取り消す。AppleとGoogleが自動ではやってくれない一手

AppleとGoogleはどちらも、顧客が購入物を保持したまま返金を行うことがある。ここでは、アクセスが自動的に取り消されるのはどの場合か、サーバー側で対応が必要なのはどの場合か、そしてほとんどの実装が見落としている一つの通知について、正確に解説する。

夜のデスクの上で光るスマートフォン。返金された購入物へのアクセスがまだ取り消されていない瞬間を表している

要点

  • Google Playでは、注文を返金してもそれだけではユーザーのアクセスは削除されない。アクセスが削除されるのは、Play Consoleで「Remove entitlement」にチェックを入れた場合、またはAPI呼び出しでrevoke=trueを渡した場合のみで、これはデフォルトの動作ではない。
  • revokeオプションを使わずに発行された返金は、Voided Purchases APIに一切表示されない。そのフィードだけを読む取り消し処理は、そうした注文をすべて静かに見逃すことになる。
  • Appleは非消費型アイテムとサブスクリプションについてはライセンスを自動的に取り消せるが、消費型アイテムは使用された時点で提供済みとみなされるため、StoreKitにはそれを取り消す仕組みがない。残高の差し引きは自社サーバーで行う必要がある。
  • Appleは自らが行った返金を取り消すこともある。REFUND_REVERSED通知は、以前に承認された返金が取り消されたことを意味し、revocationDateはnullに戻り、削除したアクセスや残高はすべて復元しなければならない。
  • GoogleのvoidedReasonフィールドは、remorseやaccidental_purchaseからfraud、friendly_fraud、unacknowledged_purchaseまで9種類の値を持つため、同じフィードがアクセスを取り消すべきタイミングだけでなく、どの取り消しが不正利用でどれが自社の実装ミスなのかも教えてくれる。
  • 返金済みの購入がアクセスを保持し続ける一日一日が、すでに返金した売上に対して計算資源、ストレージ、API呼び出しの費用を払い続ける一日である。

返金の承認とアクセスの削除は別々の2つのステップであり、自動的に行われるのはそのうち一方だけである。処理済みの返金イコール取り消し済みの購入だと思い込んでいる開発者は、主要な2つのストアどちらについても、それぞれ異なる理由で誤っている。自動化がどこで止まるのかを正確に把握することが、数ヶ月後になって実は取り消されていなかったと気づくのではなく、返金後にきちんとアクセスを取り消すために必要なことである。

なぜ返金はデフォルトでアクセスを取り消さないのか

AppleもGoogleも、お金とアクセスを別々の2つの操作として扱う。お金の側はストアのポリシーであり、返金が承認されればそれで確定し、台帳が更新される。アクセスの側は任意であったり、開発者の仕事であったり、あるいはその両方であったりする。これはプラットフォームと購入の種類によって異なる。どちらのストアも、返金が完了した瞬間にアクセスを消したいはずだとは想定していない。なぜなら、部分返金や好意による返金、サポート対応としての返金など、正当な理由で商品を取り上げるべきではないケースが数多くあるからだ。

Google Play。誰も気づかないチェックボックス

Play Consoleでは、注文の返金は2段階のフォームになっている。返金額を選び、続けて「Remove entitlement」にチェックを入れるかどうかを別途決める。多くの返金申請は顧客が購入物を保持したままで済む好意的な対応であるため、チェックを入れないままにするのが、多くのサポート担当者が取るデフォルトの経路である。問題は、返金確認画面のどこにも、これが顧客が最終的なつもりだった返金の後もサブスクリプションやアプリ内アイテムを保持し続ける方法でもある、とは書かれていないことだ。

APIの経路にも同じ落とし穴がある

同じ選択はコード上にも存在する。orders.refundエンドポイントはオプションのrevokeクエリパラメータを受け取る。これがtrueのとき、そのアイテムへのアクセスは即座に終了し、定期購読の場合は今後のすべての支払いもあわせてキャンセルされる。このパラメータを省略するとデフォルトでfalseとなり、返金は登録され、購入記録は更新されるが、ユーザーは以前とまったく同じように製品を使い続けられる。

アクションアクセスは削除されるかVoided Purchases APIに表示されるか
Play Console返金、「Remove entitlement」未チェックいいえいいえ
Play Console返金、「Remove entitlement」チェック済みはい、即座にはい
orders.refund API、revokeパラメータ省略(デフォルトはfalse)いいえいいえ
orders.refund API、revoke=trueはい、即座にはい

この最後の行こそが、実装を落とし穴にはめるものだ。Voided Purchases APIは、Play Consoleを手動でポーリングせずに取り消しパイプラインを構築できるようにするために存在するが、実際にrevocationを伴って無効化された注文しか一覧に含めない。チェックボックスなしで、あるいはrevoke=trueなしで処理された返金は、バグではなく設計上、このフィードから見えない。

緑色のランプが並ぶ中に赤いステータスランプが一つだけ灯っているサーバーラック。静かに下される取り消しの判断を表している

voidedReasonフィールドの読み方 購入がvoided(無効)として表示される場合、voidedReasonフィールドがその理由を、9種類の値で教えてくれる。other、remorse、not_received、defective、accidental_purchase、fraud、friendly_fraud、chargeback、unacknowledged_purchaseである。これは単なる記録作業ではない。remorseとaccidental_purchaseは通常の返金である。fraud、friendly_fraud、chargebackは、単純に取り消して終わりにする経路ではなく、不正利用の審査キューに回す価値がある。同じアカウントが統計的に繰り返す可能性が高いからだ。

このフィードを直接使ってシステムを構築する場合、運用上重要な点が一つある。purchases.voidedpurchases.listは1日あたり6,000クエリ、任意の30秒間で30クエリという上限がある。タイトなループでポーリングする設計はすぐにこの上限に達してしまう。想定されている使い方は、継続的なポーリングではなく、ページネーショントークンを使ったスケジュール実行のプルである。

Apple。App Storeはライセンスを取り消せるが、消費型アイテムは取り消せない

AppleのApp Store Server Notifications V2は、返金が確定した瞬間にREFUND通知を送信する。これは消費型、非消費型、自動更新型サブスクリプション、非更新型サブスクリプションのいずれについても変わらない。ただしこの通知の後に何が起きるかは、購入の種類に完全に依存する。StoreKit自体の取り消しの仕組みは、4種類のうち2種類しかカバーしていないからだ。

なぜ消費型アイテムだけが例外なのか

非消費型アイテムやサブスクリプションには、Appleのシステムがオフに切り替えられる恒常的な権利(エンタイトルメント)がある。消費型アイテムにはそれがない。一度使用されれば提供済みとみなされ、StoreKit側にはもう取り消すべきものが残っていない。顧客が500コイン購入し、それを使い切ってから返金を受けた場合、Appleから見ればそのコインはすでに消えている。それがまだ残っている唯一の台帳は、あなた自身のものだ。REFUND通知は、該当する元のトランザクションIDを見つけて、その残高を自社の記録から差し引くための唯一の合図である。Apple側では何もしてくれない。

通知意味サーバーが行うべきこと
REFUNDAppleが消費型、非消費型、自動更新型、または非更新型の購入を返金した非消費型とサブスクリプションのアクセスを即座に取り消す。消費型の残高は自社で差し引く
REFUND_DECLINEDAppleが顧客の返金リクエストを却下した何もしなくてよい。アクセスは一度も危険にさらされていない
REFUND_REVERSEDAppleがすでに承認していた返金を取り消したREFUND通知によって削除したアクセスや残高をすべて復元する
CONSUMPTION_REQUESTAppleが返金を判断する前に証拠を求めている12時間以内に応答する。これはREFUNDの前に発生するものであり、後ではない

ほとんど誰も対応していない通知

REFUND_REVERSEDは、ほとんどの実装が完全に見落としている通知だ。これはAppleが自らの判断を取り消したことを表しており、最初の実装段階で想定して設計する開発者はほとんどいないケースだからだ。これが届くと、そのトランザクションのrevocationDateはnullに戻る。Appleから見れば、その購入は再び有効になったということだ。元のREFUNDが届いたときにサーバーがエンタイトルメントを削除したり、サブスクリプションの記録をキャンセルしたり、消費型の残高をゼロにしたりしていた場合、REFUND_REVERSEDはそのすべてを元に戻すべき合図である。

このギャップをどう埋めるか。段階的な手順

  • Google Playでは、特定のサポート案件で顧客にアクセスを保持させる必要がある場合を除き、Play Console経由かAPI経由かにかかわらず、すべての返金経路のデフォルトをrevoke=trueにする。
  • purchases.voidedpurchases.listは、タイトなループではなく、ページネーショントークンを使ったスケジュール実行でポーリングし、1日6,000リクエスト、30秒あたり30リクエストの範囲内に収める。
  • voidedReasonの値がfraud、friendly_fraud、chargebackの場合は、自動的な取り消しと完了処理ではなく、審査キューに回す。
  • Appleでは、App Store Server Notifications V2を購読し、REFUND通知が対象を示した瞬間に非消費型とサブスクリプションのアクセスを取り消す。
  • すべての消費型購入について元のトランザクションIDを保存しておき、REFUND通知が届いたときに手動で調べなくても正確な差し引き対象の残高を見つけられるようにする。
  • REFUND_REVERSEDを明示的に処理し、すべての取り消しをそのトランザクションIDまたは注文IDに紐づけて記録することで、取り消しの取り消しが発生したときに何を削除したのかを正確に復元できるようにする。

このギャップが実際にもたらすコスト

これは顧客に対する公平さの問題ではなく、お金がなくなった後に何が動き続けているかという問題である。返金済みのサブスクリプションがAPIを呼び出し続け、データを保存し続け、計算資源を消費し続けることは、道徳的な問題ではなく、コストの明細項目である。すでに売上を返した購入のために、インフラ費用を払い続けているということだ。ストアはこのコストを代わりに追跡してはくれない。ストア側から見れば、お金が動いた瞬間に返金は完了しているからだ。アクセスの側を管理するのは、完全に自分たちの責任である。

RefundHaltが取り消し処理を返金パイプラインの一部として扱い、後回しの作業とはみなさない理由もここにある。REFUNDイベントとアクセスの変更は一つのアクションとして記録され、REFUND_REVERSEDイベントは、誰かがAppleが返したはずのものから顧客がまだ締め出されていることに気づくのを待つのではなく、自動的に状態を復元する。

よくある質問

Google Playで購入を返金すると、アクセスは自動的に取り消されますか。
いいえ。アクセスが削除されるのは、Play Consoleで「Remove entitlement」にチェックが入っている場合、またはorders.refund APIの呼び出しにrevoke=trueが含まれている場合のみだ。どちらもデフォルトではないため、返金が処理されても顧客がそのまま製品を使い続けられることがある。
App Storeで消費型のアプリ内課金が返金されると何が起きますか。
AppleはREFUND通知を送信するが、消費型アイテムは使用された時点で提供済みとみなされるため、StoreKitにはそれを取り消す方法がない。開発者側のサーバーが元のトランザクションを見つけ、手動で残高を差し引く必要がある。
REFUND_REVERSED通知とは何ですか。特別な対応が必要ですか。
REFUND_REVERSEDは、Appleがすでに承認していた返金を取り消したことを意味する。revocationDateはnullに戻り、その購入は再び有効になる。元のREFUNDによって削除されたアクセスや残高はすべて復元する必要があるが、ほとんどの実装はこの通知をまったく受け取っていない。
返金されたGoogle Playの注文が、なぜVoided Purchases APIに表示されないことがあるのですか。
このAPIは、revocationを伴って無効化された注文だけを一覧表示する。revokeオプションなしで返金が処理された場合、その注文はこのエンドポイントから一切返されない。したがって、このAPIだけに頼るパイプラインは、そうした返金を完全に見逃すことになる。
GoogleのVoided Purchases APIはどのくらいの頻度でポーリングできますか。
このエンドポイントには、1日あたり6,000クエリ、任意の30秒間で30クエリというレート制限がある。そのため、継続的なポーリングではなく、ページネーショントークンを使ったスケジュール実行のプルを想定した設計になっている。

出典と参考資料

RefundHalt

App Store と Google Play の返金を自動処理

続きを読む

次の返金リクエストは、すでに向かってきています。

異議を申し立てられなかった返金について、もう 1 通のサポートメールを読む時間で RefundHalt を設定できます。