所有文章
Deep dive閱讀時間 7 分鐘

為每一筆 App Store 購買附加 appAccountToken,否則你無法為退款辯護

當顧客申請退款時,Apple 會向你的伺服器發送一個 CONSUMPTION_REQUEST,但這筆交易從未說明他們是誰。appAccountToken 就是那個把購買連回你的使用者的 UUID。設定它,你就能用真實資料回應 Apple。略過它,你就只能靠猜。

一把帶編號的黃銅鑰匙擱在深色帳本上,旁邊是一支顯示購買收據的智慧型手機,說明 appAccountToken 如何將一筆 App Store 購買連結到某個使用者帳戶

重點摘要

  • appAccountToken 是你附加到 App Store 購買上的一個 UUID,讓最終產生的交易指回你自己系統中確切的那個使用者。Apple 會把它儲存在交易上,並在該交易出現的任何地方將其回傳。
  • Apple 強制執行的唯一格式規則是:該值必須是有效的 UUID。傳入其他任何東西,一個 id、一個電子郵件、一串串接的字串,StoreKit 都會默默丟棄它,並把 appAccountToken 回傳為 nil。
  • 在 StoreKit 2 中,你用一個購買選項 Product.PurchaseOption.appAccountToken(_:) 來設定它,使用你為該帳戶產生並儲存的一個穩定 UUID。
  • 只需在原始購買時設定一次,Apple 就會在訂閱鏈中的每一次續訂、扣款重試和升級中攜帶同一個權杖。
  • 自 2025 年起,Set App Account Token 端點讓你的伺服器能夠為在你的 App 之外完成的購買附加權杖,例如優惠代碼兌換和推廣購買,這些是應用程式內流程永遠無法觸及的。
  • appAccountToken 正是讓 Apple 的 CONSUMPTION_REQUEST 變得可回應的東西。沒有它,你就無法在 12 小時視窗內把退款對應到那個你本應描述其使用情況的顧客。
  • 一筆你無法識別的退款,就是一筆你無法辯護的退款。你會把錢退還給那些你本有證據可以留住的購買,還搭上你為交付它們已經花掉的運算資源、API 呼叫和分潤。

顧客向 Apple 申請退款,Apple 向你的伺服器發送一個 CONSUMPTION_REQUEST,而你有十二個小時用真實資料回答這個人是如何使用產品的。然後你打開通知,才意識到你根本不知道他們是誰。這筆交易帶著一個 originalTransactionId 和一個產品 id,卻沒有任何東西指向你自己資料庫中的那個帳戶。這道缺口正是 appAccountToken 要填補的,而如果你在購買時沒有設定它,事後你就無法為那筆銷售填補它。

appAccountToken 是你附加到一筆購買上的 UUID,讓最終產生的 App Store 交易帶上一個指回你系統中確切使用者的指標。設定它,Apple 日後就那位顧客提出的每一個退款問題,都會帶著他們的身分而來。略過它,你就只能靠猜。以下講的是這個欄位是什麼、如何設定它、那個拯救 App 之外購買的新端點,以及當退款到來時這道缺失的連結究竟要付出什麼代價。

appAccountToken 到底是什麼

appAccountToken 是一個不透明的 UUID,由你產生並在購買那一刻傳給 StoreKit。Apple 會把它儲存在交易上,並在那筆購買的交易資訊中回傳,而且它會一直留在那裡。用 Apple 的話說,它是「將交易與使用者在你自己服務上的帳戶關聯起來的 UUID」。唯一的格式規則是它必須是一個 UUID。Apple 不會讀取它,不會驗證它對應到什麼,也不在意它在你這邊代表什麼。它是一條由你掌控的連結。

因為它存在於交易之上,所以交易出現在哪裡,它就會跟著回到哪裡。伺服器通知中簽章的交易、App Store Server API 的 Get Transaction Info 回應,以及訂閱鏈中的每一次續訂,只要你在原始購買時設定了它,就都會攜帶同一個權杖。一個 UUID,只附加一次,便在這段關係的整個生命週期中跟隨著顧客的帳單。

它必須是一個真正的 UUID,否則會無聲無息地消失

Apple 強制執行的唯一規則是格式。StoreKit 2 要求一個 RFC 4122 UUID。如果你傳入一串串接的字串、一個整數 id 或一個電子郵件位址,StoreKit 不會擲出錯誤。它會丟棄這個值,交易回傳時 appAccountToken 被設為 nil。開發者不斷踩到這個坑,而症狀總是相同,在 Apple 自己的論壇上以「appAccountToken is missing in the transaction payload」的某種變體出現,幾乎總是因為傳入的值不是有效的 UUID。在伺服器端產生一個真正的 UUID,將它與帳戶對應儲存,永遠不要給 StoreKit 別的東西。

如何在購買時設定它

在 StoreKit 2 中,它是單一購買選項。在使用者註冊或首次進入結帳時在你的伺服器上產生 UUID,把它儲存在他們的帳戶記錄上,並把同一個值傳入購買呼叫。

其簽章是 Product.PurchaseOption.appAccountToken(_ token: UUID),而一筆購買看起來像 try await product.purchase(options: [.appAccountToken(token)])。當交易回傳時,無論是透過 App Store Server API 驗證的,還是由伺服器通知送達的,它都會攜帶那個 UUID,而你的伺服器用一次查詢就能找到這位顧客。

每個帳戶使用一個穩定的權杖

不要為同一使用者的每一筆購買產生一個新的權杖。Apple 會在同一條鏈中的續訂、扣款重試和升級中回傳該權杖,所以每個帳戶一個穩定的 UUID,會給你一條從首次購買貫穿至未來每一個事件的清晰線索。一個每筆購買都變化的權杖會切斷這條線索,並使整個目的落空。一個帳戶,一個權杖,每次該帳戶購買時都重複使用它。

拯救 App 之外購買的那個端點

直到 2025 年之前一直有個漏洞。如果顧客兌換了優惠代碼,或直接從 App Store 購買了一個推廣的應用程式內購買項目,你的 App 從未執行過購買流程,所以沒有任何地方可以設定 appAccountToken。那些交易到來時是匿名的,而且一直保持匿名。

WWDC 2025 用 Set App Account Token 端點填補了這道缺口。你的伺服器在 App Store Server API 上呼叫 PUT /inApps/v1/transactions/{originalTransactionId}/appAccountToken,請求主體中帶上 UUID,Apple 便會在那筆交易上設定權杖。它適用於每一種產品類型,涵蓋優惠代碼兌換和推廣購買,而你發送的值會覆蓋交易上已有的任何權杖。現在你可以事後從你的伺服器為一筆購買建立連結,而無需它經過你的 App。

深色書桌上堆著一疊紙本銷售收據,顧客姓名一欄留空,其中一張被暖色聚光燈照亮,說明一筆到來時沒有 appAccountToken 來識別買家的 App Store 購買
購買通路你在哪裡設定 appAccountToken備註
應用程式內購買購買時的 StoreKit 購買選項Product.PurchaseOption.appAccountToken(UUID)
優惠代碼兌換Set App Account Token 端點,伺服器端沒有應用程式內流程可掛接,所以事後設定
來自 App Store 的推廣應用程式內購買Set App Account Token 端點,伺服器端購買發生在你的 App 之外
訂閱續訂無需操作自動從原始購買中沿用

缺失的連結在哪裡讓你損失金錢

權杖的意義不在於整潔的記錄。而在於 Apple 面向開發者的那個退款問題,也就是 CONSUMPTION_REQUEST,只有當你能找到它所關乎的那位顧客時才可回答。

當買家就一個消耗型項目或一個非續訂訂閱申請退款時,Apple 會向你的伺服器發送一個 CONSUMPTION_REQUEST 通知,並給你十二個小時用 Send Consumption Information 回覆。你的回答是關於那位特定顧客的資料:他們消耗了多少產品、他們的帳戶存續時長、他們的終身消費、他們的交付狀態。appAccountToken 本身就是那個請求中的欄位之一,更關鍵的是,它正是通知中的交易對應到那個你即將描述其使用情況的帳戶的方式。沒有權杖,就沒有查詢,就沒有準確的回答。

一份空白的回答究竟要付出什麼代價

一筆無法識別的退款迫使你做出糟糕的選擇。你可以用空白來回答這個消耗資訊請求,而這會被解讀為低消耗,並推動 Apple 傾向於核准退款,包括核給那些大量使用了產品的顧客。或者你可以靠猜。無論哪種方式,你都是在退還那些你本有證據可以辯護的購買,而你早已為服務它們付出了真實的成本。

那個成本不是銷售價格。一個執行了一批模型 API 呼叫、產生了圖片、匯出了影片或觸發了創作者分潤的消耗型項目,在它被交付的那一刻就花掉了真金白銀。退款退還的是顧客的付款。它不會退還供應商的帳單。把一位無法識別的顧客乘以他們提交的每一筆退款,再乘以每一個指望你不知道他們是誰的連環退款者,你略過的那個權杖就成了你從未寫下的最昂貴的一行程式碼。

同樣的理念在 Android 上也存在,只是換了個名字

Google Play 用 setObfuscatedAccountId 解決同一個問題,它把一個帳戶識別碼附加到購買上,這樣 Google 透過 orders.reviewrefund 進行的拒付審查就能對著一個真實的使用者來回答。不同的商店,不同的機制,同樣的教訓:在購買時附加身分,否則你日後無法為爭議辯護。在 App Store 上,那個工具是 appAccountToken,而且它必須是一個 UUID。

讓權杖保持到位的三個習慣

  • 為每個帳戶產生一個 UUID 並儲存它。每位顧客一個穩定的權杖,在他們註冊時或首次結帳時建立,儲存在他們的記錄上,並在每一筆購買時重複使用。
  • 在傳入之前先驗證它。在你的購買程式碼中確認該值是一個真正的 UUID,這樣一個格式錯誤的 id 就永遠不會默默變成交易上的 nil 權杖。
  • 為 App 之外的購買補寫權杖。當一個伺服器通知就一次沒有權杖的優惠代碼兌換或推廣購買而到來時,呼叫 Set App Account Token 端點來附加正確的那個。

做到這三點,Apple 日後發給你的每一筆交易,包括退款請求,都會到來時就已經與它所屬的那位顧客綁定好了。

這就是 RefundHalt 所依賴的基礎工作。我們從每一筆交易和伺服器通知中讀取 appAccountToken,把它與我們已經為該帳戶追蹤的使用情況連結起來,並在十二小時視窗內用顧客的真實數字回答 Apple 的消耗資訊請求。權杖就是那條線索。設定它一次,你的退款辯護就有了可以抓住的東西。

常見問題解答

App Store 中的 appAccountToken 是什麼?
appAccountToken 是你產生並透過 StoreKit 附加到一筆購買上的 UUID,讓最終產生的 App Store 交易指回你自己系統中某個特定的使用者帳戶。Apple 會把它儲存在交易上,並在交易資訊、伺服器通知以及同一條鏈中的每一次續訂裡回傳它,這讓你能夠把任何未來的事件,包括退款請求,連結到正確的那位顧客。
為什麼我的 appAccountToken 是 nil 或缺失?
幾乎總是因為你傳入的值不是有效的 UUID。StoreKit 2 要求一個 RFC 4122 UUID,並會默默丟棄其他任何東西,所以一串串接的字串、一個整數 id 或一個電子郵件會回傳一個 nil 的 appAccountToken,而購買仍然成功。另一個常見原因是在你的 App 之外完成的購買,例如優惠代碼兌換,那裡沒有執行任何應用程式內流程來設定權杖。
我可以在購買之後設定 appAccountToken 嗎,例如為優惠代碼?
可以,自 2025 年起。App Store Server API 上的 Set App Account Token 端點讓你的伺服器能夠透過呼叫 PUT /inApps/v1/transactions/{originalTransactionId}/appAccountToken 並在請求主體中帶上 UUID,為一筆已有的交易附加或覆蓋權杖。它適用於每一種產品類型,並且專為在你的 App 之外完成的購買而設計,例如優惠代碼兌換和推廣的應用程式內購買。
appAccountToken 必須是一個 UUID 嗎?
是的。Apple 強制執行的唯一格式規則是該值必須是有效的 UUID。除此之外它是不透明的,所以它可以對應到你這邊任何你喜歡的帳戶鍵,但如果它不是 UUID,StoreKit 就不會儲存它,交易回傳時 appAccountToken 會被設為 nil。
appAccountToken 如何幫助處理退款?
當顧客申請退款時,Apple 會發送一個 CONSUMPTION_REQUEST,並給你十二個小時用關於那位特定買家的資料來回應。appAccountToken 是你把通知中的交易比對到那個你需要回報其使用情況的帳戶的方式,而且它本身就是消耗資訊請求中的欄位之一。沒有它,你就無法用真實的使用情況來回答,於是 Apple 會傾向於核准那些你本有證據可以爭辯的退款。
appAccountToken 應該為每一筆購買都不同嗎?
不。每個帳戶使用一個穩定的 UUID,並在該使用者的每一筆購買中重複使用它。Apple 會在訂閱鏈中的續訂和升級中攜帶該權杖,所以一個穩定的權杖會給你一條隨時間推移的清晰連結。一個每筆購買都變化的權杖會切斷這條連結,並使把交易連回同一位顧客變得更困難。

資料來源與延伸閱讀

RefundHalt

App Store 與 Google Play 的退款自動駕駛

繼續閱讀

下一筆退款申請已經在路上。

讀完另一封關於未能抗辯退款的客服郵件所需的時間,就足夠您設定好 RefundHalt。