App Store & Play Store Development

すべてのApp Store購入にappAccountTokenが必要な理由 —— なければ返金への反論は成立しません

顧客が返金をリクエストすると、AppleはCONSUMPTION_REQUESTを送信しますが、そこに顧客の名前は記載されません。appAccountTokenは、購入とユーザーを結び付けるUUIDです。設定しておけば実際のデータで回答できますが、設定していなければ推測に頼るしかありません。

5 min read
すべてのApp Store購入にappAccountTokenが必要な理由 —— なければ返金への反論は成立しません

返金を証拠で正当化するために、Appleが与えてくれる時間はわずか12時間です。目の前のトランザクションが誰の顧客のものか特定できなければ、その時間制限は意味をなしません。appAccountTokenは、その「名前」を提供するUUIDであり、購入が行われている最中にしか付与できません。

通知が届く場面を想像してみてください。Appleは、この購入者が実際にどれだけ製品を消費したのか、どのくらいの期間顧客であるのか、総額いくら支払っているのかを知りたがります。もっともな質問であり、その答えはすべてあなたのデータベースに保存されています。問題は、目の前にあるペイロードが提供するのはoriginalTransactionIdと製品識別子だけで、どちらもユーザーテーブルの何とも対応していないという点です。名前の書かれていない請求書を手にしているのに、必要な答えは名前ごとに整理されているのです。

このズレには解決策があり、その解決策には期限があります。appAccountTokenは、あなたのシステムが生成し、チェックアウト時にStoreKitに渡すUUIDで、Appleはそれを結果として生じるトランザクションに刻み込みます。その時点以降、その購入についてAppleから伝えられる情報はすべて、あらかじめ識別済みの状態で届きます。しかし、その瞬間を逃してしまうと、その販売は永久に匿名のままになります。以下では、このフィールドとは何か、どのように付与するか、アプリが関知しなかった購入を後から救済するエンドポイント、そして匿名の返金が実際にビジネスから奪うものの計算について説明します。

主なポイント

  • App Store購入にUUIDを付与すると、生成されるトランザクションはあなたの記録内の特定の1人のユーザーを正確に指し示します。Appleはその値をトランザクションに保持し、そのトランザクションが現れるあらゆる場所でその値を返します。

  • Appleが管理するのは形式だけであり、その形式はUUIDです。データベースのID、メールアドレス、つなぎ合わせた文字列などを渡すと、StoreKitは何も言わずにそれを破棄し、appAccountTokenがあるべき場所にnilを返します。

  • StoreKit 2では、Product.PurchaseOption.appAccountToken(_:)という1つの購入オプションとして公開されており、事前にそのアカウントに対して生成・記録しておいたUUIDを渡して使用します。

  • 最初の購入時に付与しておけば、Appleはそのサブスクリプションチェーンに含まれるすべての更新、すべての再試行された請求、すべてのアップグレードにわたって同じ値を伝播させます。

  • 2025年以降、Set App Account Tokenエンドポイントにより、アプリ内フローが関知しなかった購入(オファーコードの引き換えやプロモーション購入など)に、サーバー側からタグ付けできるようになりました。

  • AppleのCONSUMPTION_REQUESTに回答できるのは、このトークンがあってこそです。これがなければ、12時間の期限内に消費状況を詳細に説明することが求められているその顧客自身と、返金を結び付けることができません。

  • 識別できない返金は、反論できない返金です。証拠を持っていたはずの販売についてもお金が返還され、それらを提供するために既に費やしたコンピュート、API呼び出し、支払いも一緒に失われます。

このフィールドが実際に行うこと

Appleはこの値を無意味なものとして扱っており、それがまさにポイントです。チェックアウト時にStoreKitに渡したUUIDは、そのまま解釈も、参照先の検証も、何らかの実体に解決するかどうかの確認もされずに、トランザクションに書き込まれて無期限に保持されます。Apple自身の定義では、これを「トランザクションを自社サービス上のユーザーアカウントと関連付けるUUID」としています。解釈は完全にあなたの領分です。Appleは単にその値を保持し、要求があればそれを返すだけです。

UUID以外の値を渡すと、警告なしに消えてしまう

開発者がこのルールに気づくのは、たいていそれに違反した後です。妥当に思える値(内部のアカウントID、メールアドレス、いくつかのフィールドを連結した文字列など)を渡しても、StoreKitは何の異論も唱えません。例外も、警告も、購入の失敗も発生しません。トランザクションはappAccountTokenがnilのまま戻ってくるだけで、あると思っていた関連付けは実際には作られていません。Appleのデベロッパーフォーラムには、「appAccountToken is missing in the transaction payload」といった表現でこの混乱が繰り返し投稿されており、その原因はほとんどの場合、形式要件を満たしていない値を渡したことにあります。

その要件は厳密です。StoreKit 2が受け入れるのはRFC 4122に準拠したUUIDのみです。したがって安全なパターンは、サーバー側でUUIDを生成し、それをアカウントに紐付けて永続化し、それ以外の種類の値が購入呼び出しに渡ることを決してないようにすることです。

これを危険にしているのは「沈黙」です。形式が不正な値を渡してもエラーは発生せず、販売も失敗しません。失われるのは関連付けそのものだけで、他はすべて正常に進行する中で静かに破棄されます。StoreKitに渡る前にUUID形式かどうかを確認するコードは1行で済み、この失敗パターンを完全に取り除けます。

チェックアウト時にトークンを付与する

実装全体は、1つの購入オプションだけで完結します。登録時、あるいは顧客が初めてチェックアウトに到達した時点でサーバー側でUUIDを生成し、そのアカウントレコードに書き込み、以降の購入では常にその同じ値を渡します。

オプションのシグネチャはProduct.PurchaseOption.appAccountToken(_ token: UUID)であり、呼び出し自体はtry await product.purchase(options: [.appAccountToken(token)])のようになります。完了したトランザクションがどのように届くか(App Store Server APIに対して自分で検証する場合でも、サーバー通知として受け取る場合でも)にかかわらず、そのUUIDは常に中に含まれており、顧客の識別は単純なインデックス検索1回に帰着します。

顧客1人につき1つのトークンを、永続的に再利用する

トランザクションごとに新しいトークンを生成してしまうのは、よくある誤りです。そうすると、このメリットは静かに崩れてしまいます。Appleは、元の購入に付与されていたトークンを、そのチェーン内の更新、再試行された請求、アップグレードにわたって再現するため、アカウントごとに固定された恒久的なUUIDを使うことで、顧客の支払い履歴全体をつなげておけます。値をローテーションすると、各購入が孤立した存在になってしまいます。ここで重要なのは安定性なので、アカウント作成時にUUIDを1つ選んだら、それを変更しないようにしてください。

アプリが一切関知しなかった購入を救済するエンドポイント

かつては、2つの購入経路がこの仕組みを完全に無効にしていました。オファーコードの引き換えは、あなたのアプリではなくApp Store内で行われます。ストアの掲載ページからプロモーション付きのアプリ内購入を購入する場合も同様です。どちらの経路でも、あなたの購入処理コードは実行されないため、トークンを付与するタイミングが存在せず、それらのトランザクションは永久に匿名のままになっていました。

この制約は、WWDC 2025でのSet App Account Tokenエンドポイントの登場により解消されました。あなたのサーバーが、App Store Server APIに対してPUT /inApps/v1/transactions/{originalTransactionId}/appAccountTokenをUUIDをボディに含めて発行すると、Appleはそれをトランザクションに書き込みます。対象範囲はすべての製品タイプに及び、オファーコードの引き換えやプロモーション購入もしっかり含まれ、この方法で送信された値は、そのトランザクションが既に持っていたトークンを置き換えます。これにより、アプリが一切関与しなかった購入についても、完全にサーバー側で後から識別を行えるようになりました。

トークンをどこで付与するかは、購入がどのように行われたかによって異なります。

  • 通常のアプリ内購入の場合は、購入時にProduct.PurchaseOption.appAccountToken(UUID)を通じて、StoreKitの購入オプションに設定します。

  • オファーコードの引き換えの場合は、アプリ内にフックできる処理が存在しないため、サーバーからSet App Account Tokenエンドポイントを使って後から付与します。

  • App Storeからのプロモーション付きアプリ内購入の場合も、トランザクションがアプリの外で発生するため、同様にSet App Account Tokenエンドポイントで設定します。

  • サブスクリプションの更新の場合は、何もする必要がありません。トークンは元の購入から自動的に継承されます。

紐付けが切れていると、収益にどのような影響があるか

ここで問題にしているのは、記録を整然と保つことそのものではありません。重要なのは、返金の際にAppleがCONSUMPTION_REQUESTという形で開発者にちょうど1つの質問を投げかけてくるという事実であり、それに答えるには、話題になっているのがどの顧客なのかを把握している必要があるということです。

消耗型アイテムおよび自動更新されないサブスクリプションに対する返金リクエストがこの通知をトリガーし、それに対してSend Consumption Informationを使って12時間以内に応答します。Appleが求めているのは、その個人に関する具体的な情報です。製品の消費割合、アカウントの継続期間、累計利用額、配信状況などです。トークンはリクエスト自体のフィールドとしても登場しますが、より重要な役割は構造的なものであり、その通知をあなた自身のデータベース内の1行に変換できる唯一の手段だという点にあります。トークンを取り除けば、どれほど利用状況の追跡を徹底していても、そのリクエストには回答できなくなります。

何も回答しなかった場合の代償

返金が識別不能な状態で届いた場合、残された選択肢は2つしかなく、どちらも望ましくありません。空の消費情報を送信すれば、Appleはそれを利用量が最小限だったと解釈し、ヘビーユーザーであっても承認に傾いた判断が下されます。代わりに推定値を作り上げれば、プラットフォーム提供者に対して作り話を報告することになります。どちらの道を選んでも、本来は証明できたはずの販売が結局返金されてしまい、それらを提供するために既に支払ったコストはそのまま残ります。

痛手となるのは提供コストの部分であり、それは販売価格とは何の関係もありません。生成時に消費されたモデルのAPI呼び出し、生成された画像、レンダリング・書き出しされた動画、その先で支払われたクリエイターへの報酬——これらの資金はすべて提供の時点で既にあなたのアカウントから出ており、顧客に返金したところでそれを取り戻すことはできません。この問題を規模で考えてみてください。1人の匿名顧客が繰り返し返金を申請します。常習的に返金を求める顧客は、自分を識別できない事業者を狙って狙い撃ちします。この時点で、トークンを省略したことは小さな見落としではなく、開いたままの水道の蛇口のように機能し始めます。

Appleの質問は、ある1人の顧客の実際の消費に関するものであり、それに答えるにはその顧客が誰であるかを知っている必要があります。トークンがなければ、本来証明できたはずの販売を失うだけでなく、それらを提供するために既に投じたコンピュートやAPIコストも失うことになります。

Androidでも同じ仕組みが別の名前で動いている

Google Playでは、setObfuscatedAccountIdがこの役割を担っており、アカウント識別子を購入に紐付けることで、orders.reviewrefundを通じたチャージバック審査の際に実際に確認できる人物が存在するようにします。実装の詳細はかなり異なりますが、根底にある原則はそのまま通用します。つまり、トランザクション中にアイデンティティを紐付けておかなければ、後になって争議はデフォルトで負けてしまうということです。Appleにおけるその原則の実装がappAccountTokenであり、UUIDであることが求められます。

すべての購入を識別可能に保つための3つの実践

  • アカウントごとに1つのUUIDを生成し、永続化する。顧客ごとに1つの恒久的なトークンを、登録時または初回チェックアウト時に作成し、そのレコードに書き込み、それ以降のすべての購入で再利用します。

  • 値を渡す前に検証する。購入処理のコード内で、それが本当にUUIDであることを確認し、形式が不正な識別子が静かにトランザクション上のnilトークンへと変わってしまうことを防ぎます。

  • アプリの外で購入されたものを後から補完する。オファーコードの引き換えやプロモーション購入に関するサーバー通知がトークンを持たずに届いた場合は、Set App Account Tokenエンドポイントを使って正しいトークンを付与します。

この3つすべてを実践すれば、識別は事後に試みるものではなくなります。Appleからトランザクションが渡される時点で、それがすでに備えている性質になります。返金リクエストも含めてです。

Refund Sensorは、まさにこの前提の上に直接構築されています。私たちは各トランザクションおよびサーバー通知からappAccountTokenを抽出し、そのアカウントに対して既に記録されている消費データと結び付け、その購入者を正確に表す数値を使って、12時間の期限内に十分な余裕を持ってAppleのリクエストに応答を提出します。名前を特定できる返金こそが、反論できる返金であり、それこそが唯一反論する価値のある返金です。

これらのルールが記載されている公式ドキュメント

よくある質問

これは、あなたのシステムが所有し、StoreKitが単に記録するだけのUUIDです。チェックアウト時にこの値を渡すと、Appleはそれを結果として生じるトランザクションに書き込み、以降はその購入が現れるあらゆる場所——トランザクション情報、関連するサーバー通知、同じチェーン内の各更新——に再び現れます。実質的な効果として、返金リクエストを含め、後でAppleから伝えられるどんな情報も、あなた自身のデータベース内の正しい行に結び付けることができるようになります。

ほぼ間違いなく形式の問題です。StoreKit 2が受け入れる標準はRFC 4122であり、その検証に失敗した値はエラーを発生させずに破棄され、購入自体は何も問題がなかったかのように完了する一方で、nilが残ります。内部レコードのID、メールアドレス、つなぎ合わせた文字列は、いずれもこのように失敗します。もう1つの可能性は、そもそもあなたのアプリがその購入を一切処理していなかったというケースで、よくある原因はオファーコードの引き換えであり、その場合はトークンを渡すためのコードが一度も実行されません。

2025年以降は可能です。Appleは、まさにこの状況のためにApp Store Server APIにSet App Account Tokenを導入しました。/inApps/v1/transactions/{originalTransactionId}/appAccountTokenへのPUTリクエスト(UUIDをボディで渡す)により、既存のトランザクションにトークンを書き込んだり、既にあるトークンを置き換えたりできます。すべての製品タイプが対象であり、この機能は特にオファーコードやプロモーション購入といった、アプリが一度も処理しなかった購入を想定して設計されています。

はい、例外はありません。そのUUIDがあなた側で何を指しているかは完全にあなたの問題であり、Appleはそれを一切検査しませんが、値自体はUUID形式を満たしている必要があります。これを満たさない場合、StoreKitはその値を保持せず、トランザクションにはnilが残ります。

返金はCONSUMPTION_REQUESTをトリガーし、Appleはその特定の購入者が製品をどのように利用したかを説明する回答を12時間以内に求めます。そのような回答を作成するには、通知内のトランザクションを、実際に照会可能な顧客レコードへと変換する必要があり、それこそがトークンの機能です。トークンはリクエスト内のフィールドとしても渡されます。これがなければ、回答は空欄になるか作り話になるしかなく、空欄の回答は通常、返金が承認される結果につながります。

#appaccounttoken#storekit#refunds#in-app purchases#ios
Refund SensorRefund Sensor TeamRefund defense for App Store and Google Play developers