Appleの返金フローを扱ったことがあれば、ドキュメントに記載されている12時間の応答期限を目にしたことがあるはずです。ただし、実際にどう応答すればよいかは、必ずしも明確にされていません。
CONSUMPTION_REQUESTは通知であり、それに対する返信は別のAPIへの呼び出しで、専用のキーを使い、そのボディ形式は2021年以降2回変更されています。以前の記事では 通知自体に何が含まれているかを取り上げました。今回はその続きとして、実際に応答する段階について解説します。
要点のまとめ
App Store Server APIへの1回のPUTリクエスト、それがすべての仕組みです。
各リクエストはアプリ内課金キーから生成したJWTで署名されます。発行から60分を超えたトークンは拒否されます。
v2のボディには5つのフィールドがあり、そのうち3つが必須です。
Appleは202を返すのみで、それ以上の情報はありません。返金の可否は後ほど、別の通知として届きます。
通知が届いた瞬間から12時間が期限で、そのウィンドウ内であれば再送信も可能です。
Apple消費リクエストとは何か
専門用語を取り除けば、Apple消費リクエストとは一つの問いかけです。顧客がAppleに返金を求めたとき、Appleは顧客に回答する前にあなたのサーバーに向かって尋ねます。この人は支払った対価を受け取ったのか、そしてそれを使用したのか、と。
Appleはあなたが伝えた内容を、この顧客がどれくらいの頻度で返金を求めてきたかなど、すでに把握している情報と組み合わせ、独自に判断を下します。
この最後の部分を多くの人が誤解しています。あなたが何かを承認したり却下したりするわけではなく、事実を提供するだけです。
AppleがCONSUMPTION_REQUESTを送信する先は、App Store Connectに登録したサーバーURLで、バージョン2のサーバー通知として届きます。
2021年当初は消費型アイテムに対してのみ発行されていました。2024年にサブスクリプションが追加され、2025年12月にはAppleがこのエンドポイントのバージョン2を公開し、そのドキュメントでは非消費型アイテムや自動更新しないサブスクリプションも含め、すべての商品タイプを対象とすると説明しています。通知のリファレンスページには今でも当初の2種類しか記載されていませんが、新しい方のページを信頼し、4種類すべてに対応する形で実装することをおすすめします。
名称について触れておくと、「Apple refund API」で検索する人がいますが、独立したものとしてそのようなAPIは存在しません。この呼び出しはApp Store Server APIの一部です。
送信する前に必要なもの
これから挙げる4つのうち3つは急いでは用意できないものなので、最初のリクエストを送る前に必ず読んでおいてください。
1つ目はキーです。具体的には、App Store ConnectのUsers and Accessで作成するアプリ内課金キーです。Appleは.p8ファイルを一度しか渡してくれないので、そのキーIDと発行者IDも併せて必ず保管しておく必要があります。
2つ目は、トランザクションID(Appleから渡されるもの)から、自社のデータベース内の顧客(必要なもの)へとたどる仕組みです。最もすっきりした方法はappAccountTokenで、購入時に設定しておけば、そのトランザクションに関するすべての通知に含まれて返ってきます。
この用語に馴染みがない場合は、先に appAccountTokenが返金対応になぜ重要なのかをお読みください。このマッピングがなければ、この先の内容は机上の空論になってしまいます。
3つ目、そしてこれは深夜2時に慌てても何とかなるものではありません。同意です。
Appleのドキュメントでは、顧客がこのデータ共有に同意していない場合は一切応答してはならないと明確に述べられています。また、より法律的な言い回しで、何をもって有効な同意とするかは事業を展開する地域によって異なり、Apple側ではアドバイスできないとも記載されています。
一般的な方法は利用規約にオプトインの文言を盛り込むことですが、それはリクエストが届く前に整備しておく必要があります。
4つ目はトランザクションIDそのものです。これは通知が届き、Appleの署名を検証したあとで初めて得られます。
消費情報を送信する手順
「send consumption information apple」と検索する人の多くは、Appleのリファレンスページにたどり着きます。そこにはフィールドが列挙されているだけで、説明は唐突に終わっています。ここでは実際の手順を順を追って説明します。
まず通知が本物かどうかを確認します。 signedPayloadはJWSであり、AppleのApp Store Server Libraryがこれを検証し、デコード済みのトランザクション(transactionIdやappAccountTokenを含む)を返します。
自社側でその購入を確認します。 確認すべき内容は、商品を配信済みか、おおよそどの程度使用されたか、事前にサンプルやトライアルを提供していたか、そしてAppleにどう判断してほしいか、です。
続いてトークンです。 ヘッダー:algはES256、kidはキーID、typはJWTです。ペイロード:issは発行者ID、iatは現在時刻、expは最大でも60分後、audはappstoreconnect-v1という文字列そのまま、bidはバンドルIDです。正確な構造はApple のJSON Web Token生成ガイドに記載されていますが、ライブラリがこれを自動生成してくれます。
PUTリクエストを送信します。 本番環境のURLは https://api.storekit.apple.com/inApps/v2/transactions/consumption/{transactionId}で、Authorizationヘッダーに Bearerトークンとして JWTを設定し、以下のようなボディを送ります。
{
"customerConsented": true,
"deliveryStatus": "DELIVERED",
"consumptionPercentage": 94000,
"sampleContentProvided": false,
"refundPreference": "DECLINE"
}
Appleは202 Acceptedを返しますが、得られる情報はそれだけです。 時刻、ボディ、トランザクションIDを必ず記録しておきましょう。1か月後に「このケースについてAppleに何を伝えたのか」と誰かに聞かれることになるからです。
そして待ちます。 判断結果はREFUNDまたはREFUND_DECLINED通知として届きます。REFUNDであれば、当日中にコンテンツやエンタイトルメントを取り消してください。
「Appleが実際に使ってくれる消費情報をどう送信すればよいのか」と疑問に思っていたなら、答えはこれです。1回のリクエスト、5つのフィールド、そして期限内であること。
Appleはその12時間以内であれば更新したペイロードの送信も認めているので、最初の呼び出しが情報不足のまま送信されてしまった場合は、2回目を送ればよいのです。
フィールド名だけでは分からない点がいくつかあります。consumptionPercentageはミリ単位で表され、ここでつまずく人が多いのですが、94000は94パーセント、100000はすべてを意味し、deliveryStatusがDELIVERED以外の場合は必ず0にする必要があります。
未配信を表す選択肢には、UNDELIVERED_QUALITY_ISSUE、UNDELIVERED_WRONG_ITEM、UNDELIVERED_SERVER_OUTAGE、UNDELIVERED_OTHERがあります。
refundPreferenceは任意項目で、GRANT_FULL、GRANT_PRORATED、DECLINEのいずれかを指定します。自動更新型サブスクリプションでGRANT_PRORATEDを指定する場合、パーセンテージは省略すれば、Appleが残り期間から自動的に算出します。
Send Consumption Informationのリファレンスにエラーの全一覧が掲載されています。
レスポンスコードの意味
正常なレスポンスは202だけです。それ以外はすべて、Appleがあなたの何が間違っていたかを、たいていは名前付きで伝えているものです。
400番台のエラーが最も有用です。ボディにエラー名が記載されているためです。
ConsumptionPercentageOutOfRangeErrorは、ほとんどの場合、94000ではなく94を送信してしまったことを意味します。
UndeliveredConsumptionPercentageNonZeroErrorは、アイテムが届いていないと伝えたにもかかわらず、一部使用されたと主張してしまった場合のエラーです。
InvalidCustomerConsentedErrorは名前のとおりです。
401はトークンの問題です。キーが間違っている、有効期限が1時間を超えている、バンドルIDが一致していない、あるいはサーバーの時計が大きくずれていてiatが未来の時刻に見えてしまっている、といった原因が考えられます。
404はトランザクションIDが見つからなかったことを意味します。私がまず疑うのは、サンドボックスのトランザクションを本番URLに送ってしまった(あるいはその逆)というケースです。
429はレート制限です(本番環境ではアプリごとに1秒あたり50リクエスト、サンドボックスではその10分の1)。再試行する前にRetry-Afterヘッダーを確認してください。
500はApple側の問題なので、時間を置いて再試行してください。
サンドボックスでのテスト方法
これをテストするのは見た目ほど難しくありませんが、一つだけ注意点があります。
App Store Connectにサンドボックス用のサーバーURLを設定し、サンドボックスのApple Accountでサインインして何かを購入したあと、アプリの返金シート(beginRefundRequest)を開いてリクエストを送信します。
Appleの 返金リクエストのテストガイドには、テスト環境の挙動が説明されています。理由を適当に選べば返金は自動的に承認され、Otherを選んでDECLINEと入力すれば却下され、GRANT_PRORATEDと入力すれば日割りでの返金になります。
注意点はこのルールにあります。サンドボックスはシートに入力した内容をもとに判断を下すのであって、サーバーが送信したデータではありません。そのため、自社のデータがAppleを何か説得できているかどうかは分かりません。
テストできるのは仕組みの部分です。通知が届くこと、api.storekit-sandbox.apple.comへのPUTが202を返してくること、そしてREFUNDまたはREFUND_DECLINEDが届いたときに適切に処理されることです。
また、サンドボックスでは返金に関するメールは送信されないので、届くのを待っても無駄です。
Apple返金の自動化:手作業をやめるべきタイミング
手作業でこれを行うということは、誰かが四六時中アラートを監視し、12時間のタイムリミットが切れる前にクエリを実行して5つのフィールドを送信する、ということです。
月に数件程度のApple返金リクエストであれば問題ありません。しかし、誰も確認していない最初の連休には、話はまったく違ってきます。
Apple返金リクエストを自動化するとは、ここまで述べてきたすべてのプロセスから人の手を取り除くことです。通知が届き、配信状況や利用データをルールがペイロードへと変換し、数分以内に呼び出しが送信され、判断結果がケースに記録され、REFUNDであれば失効処理が自動的にトリガーされます。
これを構築すること自体は大きなプロジェクトではありません。むしろ、その後Appleの変更(v1からv2への移行など)に追従し続けることのほうが、気づかないうちにコストを積み重ねていく部分です。
自分で構築したくない場合は、このために作られたサービスについて Apple返金管理ツールの比較記事でまとめています。Refund Sensorが適しているケースと適していないケースについても触れています。
この話題が出るたびに一つだけ念を押しておきたいことがあります。Apple返金の自動化が変えるのは「どれだけ確実に応答できるか」であって、「誰が決めるか」ではありません。このAPIのどこにも、開発者に最終決定権を与える要素はないのです。
まとめ
この呼び出し自体は小さなものです。署名付きトークン、1回のPUT、5つのフィールド、そして記録すべき202、それだけです。
成否を分けるのは別のところにあります。すでに利用規約に盛り込まれた同意、すべての購入に紐づくappAccountToken、数秒で呼び出せる利用記録、そして通知が届いた深夜3時に代わりに起きてくれる仕組み、これらです。
これらを整えておけば、この対応は慌てて行うものではなくなり、サーバーが淡々とこなす日常的な処理になります。
よくある質問
これはCONSUMPTION_REQUESTに応答するために使用するApp Store Server APIのエンドポイントです。消費情報を含むPUTリクエストを送信すると、Appleは202 Acceptedを返します。
CONSUMPTION_REQUESTが届いてから12時間の猶予があります。Appleはこの同じ12時間の間であれば、更新したペイロードを再送信することを認めています。
はい、可能です。Appleは12時間のウィンドウ内であれば、更新したペイロードの送信を認めています。これは最初の送信が不完全だった場合に役立ちます。
いいえ、違います。202 Acceptedは、Appleが情報を受け取ったことを意味するにすぎません。結果は後からREFUNDまたはREFUND_DECLINED通知として届きます。
はい、必要です。消費情報を送信する際にはcustomerConsentedフィールドが必須です。顧客が同意していない場合、Appleのドキュメントでは応答しないよう指示されています。



