BlogRight arrowTutorialRight ArrowiOS アプリ内購入、パート 4: サーバーサイドの購入の検証
BlogRight arrowTutorialRight ArrowiOS アプリ内購入、パート 4: サーバーサイドの購入の検証

iOS アプリ内購入、パート 4: サーバーサイドの購入の検証

iOS アプリ内購入、パート 4: サーバーサイドの購入の検証
Listen to the episode
iOS アプリ内購入、パート 4: サーバーサイドの購入の検証

サーバーの購入検証とは?

サーバー検証 (サーバーサイドのレシート検証 (receipt validation)) は、購入の信頼性を検証する方法です。意外にも、デバイスベースの検証とは異なり、サーバーの検証はサーバーサイドで行われます。検証とは、購入が実際に行われたかどうか、および購入が有効であったかどうかを確認するために、デバイスまたはサーバーがAppleのサーバーにリクエストを送信することです。

⭐️ Download our guide on in-app techniques which will make in-app purchases in your app perfect

購入を検証する理由

サーバーサイドの検証は必須ではないため、ご注意ください。検証しなくてもアプリ内購入を完了できますが、いくつかの重要なメリットがあります。

1. 高度な支払い分析:アクティベーション後に発生するすべてのことは、デバイスによって処理されないため、サブスクリプションにおいて特に重要です。サーバーサイドの購入処理がないため、現在のサブスクリプションステータスを取得したり、ユーザーがサブスクリプションを更新したりキャンセルしたりしたかどうか、請求の問題 (billing issue) があるかどうかなどを把握できません。

2. 購入の信頼性を確認できる:トランザクションが不正ではなく、ユーザーが実際にプロダクトの代金を支払ったことを確認できます.

3.クロスプラットフォームのサブスクリプション:ユーザーのサブスクリプションステータスをリアルタイムで確認できれば、他のプラットフォームと同期できます。たとえば、iOSデバイスからサブスクリプションを購入したユーザーは、Android、ウェブサイト、およびその他のプラットフォームで使用できます。

4. サーバーサイドからコンテンツへのアクセスを制御できるため、サーバーへのリクエストを実行するだけで、サブスクリプションせずにデータにアクセスしようとするユーザーから保護されます。

経験上、サーバーサイドの購入処理を設定するには、最初のメリットだけで十分です。

購入の検証

通常、iOSでのレシート検証のプロセスは次のようになります。

共有シークレット (shared secret) の生成

支払い検証のリクエストを送信するには、リクエストを承認するための共有シークレットを含める必要があります。共有シークレットは、App Store Connectで生成できます。

共有シークレットは、特定のアプリ (アプリ専用シークレット) またはアカウント内のすべてのアプリ (主要シークレット) に対して作成できます。

アプリ専用のシークレットを生成するには、App Store Connectでアプリのページを開き、[App内課金] → [管理] に移動して、[App用共有シークレット] をクリックします。開いたウィンドウで、新しいトークンを生成するか、既存のトークンをコピーできます。

アプリ専用の共有シークレットの生成

アカウント内のすべてのアプリのシークレットを受け取るには、[ユーザとアクセス] ページを開き、[共有シークレット] タブを選択します。

すべてのアプリの共有シークレットを確認する方法

支払い検証のリクエスト

共有シークレットを受け取ったら、レシートを送信してAppleサーバーで検証することができます。これは、verifyReceiptリクエストを介して行われます。 https://buy.itunes.apple.com/verifyReceiptにPOST リクエストを送信する必要があります。リクエストのJSON本文で、passwordフィールドに共有シークレットを渡し、receipt-dataフィールドにレシートを渡します。オプションでは、exclude-old-transactionsパラメータもあります。trueの値が指定されている場合、自動更新可能なサブスクリプションごとに、完全な更新履歴ではなく、最新のトランザクションのみを受け取ります.

購入検証リクエストのペイロードは次のとおりです。

{
  "password": "f4d35830e3...52aae",
  "receipt-data": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
  "exclude-old-transactions": false
}

サンドボックス (sandbox) 環境で作業している場合 (購入をテストしている場合)、検証リクエストをhttps://sandbox.itunes.apple.com/verifyReceiptに送信します。共有シークレット、ペイロード、応答形式は同じままです。

本番サーバーのサンドボックス環境で作成されたレシートを検証することはできず、その逆もできないことに注意することが重要です。そのため、実際のシステムでは、ステータス キーが21007エラーコードを返した場合に備えて、最初のリクエストを運用サーバーに送信し、それをサンドボックスサーバーにリダイレクトすることをお勧めします。アプリのレビュー中に必要不可欠な対応です。これにより、Appleの従業員が購入をテストできると同時に、アプリの実際のユーザーが購入できるようになるためです。

注意すべきその他のエラーの中には、誤ったシークレットを使用していることを示す21004エラーコードがあります。ユーザーエクスペリエンスと分析の精度の両方に影響を与えるため、追跡することが重要です。最悪のシナリオでは、有料機能へのアクセス権がユーザーに付与されない場合、アプリはApp Storeから削除される可能性があります。

検証が正常に完了した場合 (status=0)、応答にはユーザーのトランザクションの詳細が含まれます。

支払い検証リクエストの応答は次のとおりです。

{
  "environment": "Production",
  "receipt": {
    "receipt_type": "Production",
    "adam_id": 123,
    "app_item_id": 123,
    "bundle_id": "com.adapty.sample_app",
    "application_version": "1",
    "download_id": 123,
    "version_external_identifier": 123,
    "receipt_creation_date": "2021-04-28 19:42:01 Etc/GMT",
    "receipt_creation_date_ms": "1619638921000",
    "receipt_creation_date_pst": "2021-04-28 12:42:01 America/Los_Angeles",
    "request_date": "2021-08-09 18:26:02 Etc/GMT",
    "request_date_ms": "1628533562696",
    "request_date_pst": "2021-08-09 11:26:02 America/Los_Angeles",
    "original_purchase_date": "2017-04-09 21:18:41 Etc/GMT",
    "original_purchase_date_ms": "1491772721000",
    "original_purchase_date_pst": "2017-04-09 14:18:41 America/Los_Angeles",
    "original_application_version": "1",
    "in_app": [
      {
        "quantity": "1",
        "product_id": "basic_subscription_1_month",
        "transaction_id": "1000000831360853",
        "original_transaction_id": "1000000831360853",
        "purchase_date": "2021-04-28 19:41:58 Etc/GMT",
        "purchase_date_ms": "1619638918000",
        "purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
        "original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
        "original_purchase_date_ms": "1619638918000",
        "original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
        "expires_date": "2021-05-05 19:41:58 Etc/GMT",
        "expires_date_ms": "1620243718000",
        "expires_date_pst": "2021-05-05 12:41:58 America/Los_Angeles",
        "web_order_line_item_id": "230000397200750",
        "is_trial_period": "true",
        "is_in_intro_offer_period": "false",
        "in_app_ownership_type": "PURCHASED"
      }
    ]
  },
  "latest_receipt_info": [
    {
      "quantity": "1",
      "product_id": "basic_subscription_1_month",
      "transaction_id": "230001020690335",
      "original_transaction_id": "1000000831360853",
      "purchase_date": "2021-08-04 19:41:58 Etc/GMT",
      "purchase_date_ms": "1628106118000",
      "purchase_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
      "original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
      "original_purchase_date_ms": "1619638918000",
      "original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
      "expires_date": "2021-08-11 19:41:58 Etc/GMT",
      "expires_date_ms": "1628710918000",
      "expires_date_pst": "2021-08-11 12:41:58 America/Los_Angeles",
      "web_order_line_item_id": "230000438372383",
      "is_trial_period": "false",
      "is_in_intro_offer_period": "false",
      "in_app_ownership_type": "PURCHASED",
      "subscription_group_identifier": "272394410"
    },
    {
      "quantity": "1",
      "product_id": "basic_subscription_1_month",
      "transaction_id": "230001017218955",
      "original_transaction_id": "1000000831360853",
      "purchase_date": "2021-07-28 19:41:58 Etc/GMT",
      "purchase_date_ms": "1627501318000",
      "purchase_date_pst": "2021-07-28 12:41:58 America/Los_Angeles",
      "original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
      "original_purchase_date_ms": "1619638918000",
      "original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
      "expires_date": "2021-08-04 19:41:58 Etc/GMT",
      "expires_date_ms": "1628106118000",
      "expires_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
      "web_order_line_item_id": "230000849023623",
      "is_trial_period": "false",
      "is_in_intro_offer_period": "false",
      "in_app_ownership_type": "PURCHASED",
      "subscription_group_identifier": "272394410"
    }
  ],
  "latest_receipt": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
  "pending_renewal_info": [
    {
      "auto_renew_product_id": "basic_subscription_1_month",
      "product_id": "basic_subscription_1_month",
      "original_transaction_id": "1000000831360853",
      "auto_renew_status": "1"
    }
  ],
  "status": 0
}

応答は非常に複雑で、App Store Server APIの新しいバージョンで簡略化されましたが、現在の実装はそれほど難しくありません。

Start for free

You don't need to write server code yourself,

because we did it for you. Try Adapty SDK!

Start for free

サブスクリプションステータスとトランザクション履歴の取得

ユーザーがアプリの有料機能にアクセスできるかどうかを確認するには、ユーザーのサブスクリプションステータスを確認する方法が必要です。現在のバージョンのAPIには、サブスクリプションステータスを取得するための専用リクエストがないため、いずれにしてもトランザクション履歴を操作する必要があります。

latest_receipt_info配列には、デフォルトで、アプリ側で完了した消費可能なプロダクトを除き、特定のユーザーのすべてのアプリ内購入トランザクションが含まれます。このようにして、ユーザーの購入履歴全体を取得できます。これは、分析と現在のサブスクリプションステータスの判断の両方に非常に役立ちます。

トランザクションは常に最新のものが一番上に表示されるようです。とはいえ、独自にトランザクションを日付順で並び替えることをお勧めします。

トランザクションのペイロード:

{
  "quantity": "1",
  "product_id": "basic_subscription_1_month",
  "transaction_id": "1000000831360853",
  "original_transaction_id": "1000000831360853",
  "purchase_date": "2021-04-28 19:41:58 Etc/GMT",
  "purchase_date_ms": "1619638918000",
  "purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
  "original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
  "original_purchase_date_ms": "1619638918000",
  "original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
  "expires_date": "2021-05-05 19:41:58 Etc/GMT",
  "expires_date_ms": "1620243718000",
  "expires_date_pst": "2021-05-05 12:41:58 America/Los_Angeles",
  "web_order_line_item_id": "230000397200750",
  "is_trial_period": "true",
  "is_in_intro_offer_period": "false",
  "in_app_ownership_type": "PURCHASED",
  "subscription_group_identifier": "272394410"
}

現在のサブスクリプションステータスを確認するには、チェーンの最新のトランザクションを取得し、expires_dateプロパティを調べるだけで十分です。その例外は猶予期間です。これについては後で説明します。

分析目的で、次のプロパティを保存することをお勧めします。

product_id:購入したプロダクトのテキスト識別子。

transaction_id:トランザクションの一意の数値識別子。各購入または更新には、トランザクションが以前に処理されたかどうかを把握するために使用できる独自の識別子があります。

original_transaction_id:トランザクションチェーンの一意の数値識別子。これは、サブスクリプションまたは試用版 (trial) のアクティベーション時のtransaction_idと一致します。ただし、サブスクリプションがさらに更新されると、transaction_idは変更されますが、original_transaction_idは同じままです。サブスクリプションが更新された回数を追跡するのに便利です。

purchase_dateおよびoriginal_purchase_date:トランザクション日と元のトランザクション日。これは、前のプロパティと同じロジックで機能します。

expires_date:サブスクリプションの有効期限。

cancellation_date:名前が示すようにサブスクリプションのキャンセルではなく、返金日を指します。応答にこのフィールドが含まれている場合は、サブスクリプションへのユーザーのアクセスを終了できること、および分析でキャンセルを説明できることを意味します。このトランザクションで支払いを受け取ることはありません。

is_in_intro_offer_period:サブスクリプションのアクティベーション時に紹介オファーが使用されたかどうかを示すフラグ。

is_trial_period:サブスクリプションのアクティベーション時に試用期間があったかどうかを示すフラグ。

offer_code_ref_name:サブスクリプションのアクティベーション時に使用されたオファーコード。

promotional_offer_id:現在の請求期間が開始した際に使用されたプロモーションオファーのテキスト識別子。

in_app_ownership_type:サブスクリプションの所有権タイプ。ユーザーがプロダクトを自分で購入したか、ファミリーサブスクリプションの一部として受け取ったかが示されます。想定される値には次のとおりです。

PURCHASED:ユーザーが自分でプロダクトを購入しました。

FAMILY_SHARED:ユーザーはファミリーサブスクリプションの一部としてプロダクトを受け取りました。

AppleはWWDC 2021で、トランザクション情報にappAccountTokenフィールドを追加する予定であることを発表しました。これには、システム内のユーザーの識別子が含まれます。この識別子はUUID形式である必要があり、購入が初期化されるときにアプリ側で定義されます。定義されている場合、このチェーンのすべてのトランザクション (サブスクリプションの更新、請求の問題など) で返されます。つまり、どのユーザーが購入したかを把握しやすくなります。

また、subscription_group_identifierパラメーターも追跡する必要があります。ユーザーが以前にアクティブなトライアルまたは紹介オファーを利用していた場合、同じグループのサブスクリプションでは利用できません。これは、サーバーサイドで追跡する必要があります。

サブスクリプションの更新情報、猶予期間 (grace period)、請求の問題 (billing issue)

pending_renewal_info配列には、サブスクリプションの更新データが格納されます。これにより、次の請求期間にサブスクリプションがどうなるかを確認できます。たとえば、ユーザーが自動更新をキャンセルしたことが判明した場合は、別のプランに切り替えることを提案したり、プロモーションオファー (promo offer) を提示したりできます。これらのイベントは、サーバー通知を使用して簡単に追跡できます。これについては、追って説明します。

サブスクリプション更新データ:

{
  "auto_renew_product_id": "basic_subscription_1_month",
  "product_id": "basic_subscription_1_month",
  "original_transaction_id": "1000000831360853",
  "auto_renew_status": "1"
}

product_id:購入したプロダクトのテキスト識別子。

auto_renew_product_id:次の請求期間にアクティベーションされるプロダクトのテキスト識別子。現在の識別子 (product_id) と異なる場合は、ユーザーがサブスクリプションの種類を変更したことを意味します。

auto_renew_status:サブスクリプションが次の請求期間に継続されるかどうかを示すフラグ。

expire_intent:サブスクリプションの有効期限が切れた理由。想定される値は次のとおりです。

1 — ユーザー自身でサブスクリプションをキャンセルしました。

2 — 請求の問題によりサブスクリプションがキャンセルされました。

3 — ユーザーは値上げに同意しませんでした。

4 — サブスクリプションプロダクトを更新できませんでした。App Store Connectから削除された場合など。

5 — 理由が不明。

grace_period_expires_date:アプリに猶予期間が存在する場合の有効期限。その場合、ユーザーは、トランザクション自体ではなく、ここで指定された日まで有料機能を利用できるものとします。このキーを持っている場合は、カード情報を更新するか、残高にチャージする必要があることをユーザーに通知できます。

is_in_billing_retry_period:サブスクリプションのステータスが「Billing Retry」であるかどうかを示すフラグ。サブスクリプションはキャンセルされていないものの、Appleは更新の支払いを請求できず、60日間にわたって請求を試みます。

offer_code_ref_name:次の請求期間で使用されるオファーコード。

promotional_offer_id:次の請求期間に使用されるプロモーションオファーのテキスト識別子。

price_consent_status:ユーザーが今後のサブスクリプションの値上げに同意したかどうかを示すフラグ。値が「0」の場合は、サブスクリプションをキャンセルしないように、ユーザーに別のプロダクトまたはプロモーションオファーを提示する必要があります。

消費可能、消費不可、更新不可のサブスクリプション

ユーザーが自動更新可能なプロダクト (auto-renewable product) を購入していない場合、latest_receipt_infoキーとpending_renewal_infoキーは返されません。この場合、トランザクションは [receipt] → [in_app] で確認できます。トランザクション形式は自動更新トランザクションに似ていますが、有効期限、更新、オファー、および自動更新トランザクション専用のその他のプロパティのフィールドはありません。

自動更新トランザクションの場合も [receipt] → [in_app] が届きます。ただし、含まれるサブスクリプションデータが最新であるため、latest_receipt_infoを使用することをお勧めします。

サーバートランザクション通知

少し前までは、サブスクリプションステータスの変更を追跡するために、複雑なシステムを考案する必要がありました。たとえば、サブスクリプションが更新されたかどうかを把握するには、サブスクリプションの有効期限が切れる24時間前から1時間ごとに、サブスクリプションステータスのリクエストをAppleサーバーに送信しなければなりません。Appleは時間の経過とともにサーバー通知 (server notifications) を追加してきましたが、これらはサブスクリプションに関係する重要なイベントのほぼすべてに対応できるようになりました。Apple側で変更があれば、自社のサーバーで通知されるため、非常に便利です。つまり、新しい購入、サブスクリプションの更新 (subscription renewal)、請求の問題などについて通知を受け取ります。これにより、正確な分析を収集できるだけでなく、サブスクリプションステータスの管理がはるかに簡単になります。

App Store Connectでサーバー通知を有効にできます。アプリのページを開き、[一般] → [App 情報] に移動します。次に、リンクをApp Storeサーバ通知フィールドのURLに入力し、変更を保存します。

サーバー通知:

{
  "notification_type": "DID_RENEW",
  "password": "f4d35830e3...52aae",
  "environment": "PROD",
  "auto_renew_product_id": "basic_subscription_1_month",
  "auto_renew_status": "true",
  "unified_receipt": {
    "status": 0,
    "environment": "Production",
    "latest_receipt_info": [
      {
        "quantity": "1",
        "product_id": "basic_subscription_1_month",
        "transaction_id": "230001020690335",
        "original_transaction_id": "1000000831360853",
        "purchase_date": "2021-08-04 19:41:58 Etc/GMT",
        "purchase_date_ms": "1628106118000",
        "purchase_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
        "original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
        "original_purchase_date_ms": "1619638918000",
        "original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
        "expires_date": "2021-08-11 19:41:58 Etc/GMT",
        "expires_date_ms": "1628710918000",
        "expires_date_pst": "2021-08-11 12:41:58 America/Los_Angeles",
        "web_order_line_item_id": "230000438372383",
        "is_trial_period": "false",
        "is_in_intro_offer_period": "false",
        "in_app_ownership_type": "PURCHASED",
        "subscription_group_identifier": "272394410"
      },
      {
        "quantity": "1",
        "product_id": "basic_subscription_1_month",
        "transaction_id": "230001017218955",
        "original_transaction_id": "1000000831360853",
        "purchase_date": "2021-07-28 19:41:58 Etc/GMT",
        "purchase_date_ms": "1627501318000",
        "purchase_date_pst": "2021-07-28 12:41:58 America/Los_Angeles",
        "original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
        "original_purchase_date_ms": "1619638918000",
        "original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
        "expires_date": "2021-08-04 19:41:58 Etc/GMT",
        "expires_date_ms": "1628106118000",
        "expires_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
        "web_order_line_item_id": "230000849023623",
        "is_trial_period": "false",
        "is_in_intro_offer_period": "false",
        "in_app_ownership_type": "PURCHASED",
        "subscription_group_identifier": "272394410"
      }
    ],
    "latest_receipt": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
    "pending_renewal_info": [
      {
        "auto_renew_status": "1",
        "auto_renew_product_id": "basic_subscription_1_month",
        "product_id": "basic_subscription_1_month",
        "original_transaction_id": "1000000831360853"
      }
    ]
  },
  "bid": "com.adapty.sample_app",
  "bvrs": "0"
}


サーバー通知の形式は、支払い検証応答に似ています。トランザクションの詳細は、unified_receipt latest_receipt_infoに格納されます。パスワードキーにはアプリの共有シークレットが保存され、リクエストの信頼性を検証できます。notification_typeキーには、イベントタイプが格納されます。これらの中で最も有用なものは次のとおりです。

DID_CHANGE_RENEWAL_STATUS:ユーザーがサブスクリプションの自動更新をオフまたはオンにしたことを意味します (後者は非常にまれです)。ユーザーが自動更新をオプトアウトした場合は、アクティブなサブスクリプションユーザーに再購入を促してもよいでしょう。

DID_FAIL_TO_RENEW:請求の問題により、サブスクリプションを更新できませんでした。サブスクリプションが自動的にキャンセルされないように、ユーザーに通知する必要があります。

DID_RENEW:サブスクリプションは正常に更新されました。

REFUND:サブスクリプションは返金されました。今回の購入によって付与された有料機能へのユーザーのアクセス権を制限し、分析で返金 (売上の喪失) を説明する必要があります。

まとめ

サーバーの検証により、アプリについて収集できる分析が強化されます。これにより、不正ユーザーが有料コンテンツにアクセスすることが難しくなり、クロスプラットフォームの定期購入の実装に使用できます。ただし、特に高精度のデータが必要な場合は、サーバーサイドの検証の実装にかなりの時間がかかることがあります。高品質のデータを提供するには、サブスクリプションのアップグレード、サブスクリプションのクロスグレード、トライアル期間、プロモーションまたは紹介オファー、猶予期間、返金、ファミリーサブスクリプションなど、多数のサイドケースを考慮する必要があります。また、Appleでは、一年以上定期的に更新されたサブスクリプションの手数料を30%から15%に引き下げるポリシーを制定しています。

Further reading

Adapty July Update: New Segmentations, New Analytical Chart, Podcast!
Adapty July Update: New Segmentations, New Analytical Chart, Podcast!
August 2, 2021
2 min read
Paywall A/B testing guide, part 2: what to test on the paywall
Paywall A/B testing guide, part 2: what to test on the paywall
July 5, 2022
15 min read
Adapty API outage and what we've learned from it
Adapty API outage and what we've learned from it
April 8, 2021
5 min read