オンライン決済を使ってサブスクリプションの仕組みを取り入れたい場合にどのようにすればよいか調べてみました。 実際に試してみた方がわかりやすいと考え、使いやすそうな決済APIサービスを使ってオンライン決済の流れを確認してみることにしました。 オンライン決済APIを提供しているサービスは多々ありますが、今回はテスト環境構築の簡易さと機能の多さからStripeを選定しました。
Stripe
Stripeは以下のようなサービスです。 まずはStripeアカウントを作成してログインし、ダッシュボードを開きます。
グローバルに支払いを受け付け、入金を行うウェブサイトとアプリを構築するために必要なあらゆる機能を盛り込みました。Stripe の製品は、オンライン販売および対面販売の小売業者、サブスクビジネス、ソフトウェアプラットフォームおよびマーケットプレイスをはじめ、ありとあらゆるすべてのビジネスの決済体験を向上させます。
決済フローについて
日本カード情報セキュリティ協議会のサイトを確認すると、PCI DSSにカード情報を扱う(保存、処理、または伝送する)企業はPCI DSSに準拠する必要があるとのこと。 つまり、カード情報を自社のサーバに送信するだけでこのPCI DSSに対応しなければならなくなります。 そこで、Stripeなどの決済サービス提供事業者はサービス利用者の代わりにカード情報を管理し、カード情報からトークンを発行することでサービス利用者のサーバをカード情報が通らないようにするフローを用意してくれています。 またこのトークンを利用することでサービス利用者側のサーバは決済処理を実行することができるようです。
カード情報を「保存、処理、または伝送する※1」企業であるカード加盟店、銀行、決済代行など行うサービス・プロバイダーが、年間のカード取引量に応じて、PCI DSS 準拠する必要があります。
決済フローイメージの参考
決済(サブスクリプション)機能の動作を確認
今回はサブスクリプションの決済がどのような仕組みで行われるのか知りたかったため、Stripeのドキュメントに沿ってサブスクリプション機能のクイックスタートを試してみました。
※実行環境はWindows11
サンプルコードのダウンロード
Stripe Checkout を使用した構築済みのサブスクリプションページ | Stripe のドキュメント
まずドキュメントからサンプルコードをダウンロードします。 ドキュメント上部に「フロントエンド」と「バックエンド」を選択できるドロップボックスがありますが、今回は「フロントエンド:HTML」「バックエンド:Ruby」とします。 ダウンロードしたファイルを解凍すると「stripe-sample-code」フォルダができるので、以後このフォルダを作業ディレクトリとします。
商品を作成する
今回は仮想の¥1,000のサブスクリプションプランを作成します。 Stripeのダッシュボードから「商品」タブを開き「商品を追加」をクリックします。
以下のように設定して仮想の商品「Basicプラン」を作成します。 月額プランで¥1,000を継続して請求するような内容になっています。 今回は使用しませんが、この画像ではよくある年間請求プランもお試しで追加しています。 その他、Stripeでは「料金体系モデル」を変更することで請求方式を色々とカスタマイズできたりもするようです。
項目 | 設定値 | 説明 |
---|---|---|
名前 | Basic | 商品名 |
説明 | Basicプラン | 商品説明 |
料金体系モデル | 標準の料金体系 | 料金体系 |
価格 | ¥1,000/JPY | 商品の価格/通貨 |
継続 | 支払い方法 | |
請求期間 | 月次 | 月額・年額などの支払い期間設定 |
商品の作成が完了したら商品画面に戻り、先ほど作成した商品が追加されていることを確認します。
サーバ環境構築
サンプルコードでは以下のgemをインストールする必要があります。 Gemfileが梱包されているのでこれを使用してインストールします。
stripe-sample-code> bundle install Using bundler 2.2.22 Using ruby2_keywords 0.0.5 Using mustermann 3.0.0 Using rack 2.2.6.4 Using rack-protection 3.0.6 Using tilt 2.1.0 Using sinatra 3.0.6 Using stripe 8.5.0 Bundle complete! 2 Gemfile dependencies, 8 gems now installed. Use `bundle info [gemname]` to see where a bundled gem is installed.
サーバのコードを確認すると、APIキーを設定する必要があるようなのでダッシュボードから確認して設定しておきます。 ダッシュボード画面右上の「開発者」をクリックして、「APIキー」タブを開き「シークレットキー」を確認します。
# This is your test secret API key. ★ Stripe.api_key = 'sk_test_xxx'
gemをインストールするとサーバを起動できるので起動しておきます。
stripe-sample-code> ruby server.rb
クライアントを確認
「public/checkout.html」を確認すると、Checkoutボタンをクリックすることで「/create-checkout-session」へ「lookup_key={{PRICE_LOOKUP_KEY}}」がPOSTされるようになっています。
ドキュメントを確認すると「lookup_key」とは商品(料金)の検索ワードのようなもののようです。
先ほど追加した商品のlookup_keyを調べると設定されていなかったので設定しておきます。 ダッシュボード画面右上の「開発者」をクリックして「APIキー」タブを開きます。 「シークレットキー」を確認して以下のコマンドで使用します。
lookup_keyが設定されていないことを確認
※lookup_keyを設定する価格のIDを調べておく
stripe-sample-code> curl -G https://api.stripe.com/v1/prices -u sk_test_xxxx { "id": "price_xxxxxxxxxxxxxxxxxxxxxxxxxx", ※ "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1681806594, "currency": "jpy", "custom_unit_amount": null, "livemode": false, "lookup_key": null, ★ . . .
lookup_keyを設定
確認した価格のURLに「lookup_key=basic_plan_monthly」をPOSTする
stripe-sample-code> curl -X POST -G https://api.stripe.com/v1/prices/price_xxxxxxxxxxxxxxxxxxxxxxxxxx -u sk_test_xxxx -d "lookup_key"="basic_plan_monthly" { "id": "price_xxxxxxxxxxxxxxxxxxxxxxxxxx", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1681806594, "currency": "jpy", "custom_unit_amount": null, "livemode": false, "lookup_key": "basic_plan_monthly", ★ "metadata": {}, . . .
「public/checkout.html」の「{{PRICE_LOOKUP_KEY}}」を先ほど設定した「basic_plan_monthly」で置き換えます。
<form action="/create-checkout-session" method="POST"> <!-- Add a hidden field with the lookup_key of your Price --> <input type="hidden" name="lookup_key" value="basic_plan_monthly" /> ★ <button id="checkout-and-portal-button" type="submit">Checkout</button> </form>
カスタマーポータル画面の設定
Stripeにはカスタマーポータル画面をユーザに表示する機能があります。 ユーザ操作できる内容をカスタマイズすることができ、ここからユーザはサブスクリプションのキャンセル等を行うことが可能となっています。
ここではテスト環境のリンクを有効にして、決済の動作確認後にカスタマーポータル画面を表示させることができるよう設定しておきます。
まずはダッシュボードの画面右上の「⚙マーク」から設定画面を開き、「カスタマーポータル」をクリックします。
カスタマーポータル画面左の「テスト環境のリンクを有効化」をクリックして有効にしておきます。
動作確認
サーバがポート4242で起動しているので、「http://localhost:4242/checkout.html」をブラウザに入力してアクセスすると「public/checkout.html」が表示されます。
Checkoutボタンをクリックすると、先ほど作成した商品の決済画面が表示されます。 決済情報を入力する際は、テスト用なのでカード番号以外は適当に入力して問題なさそうです。(※ただし有効期限は未来の年月を指定すること) カード番号は処理成功となる「4242 4242 4242 4242」を使用します。(※その他、処理失敗となるカード番号等も用意してくれています。)
決済を完了すると、「public/success.html」が表示されます。
「Manage your billing information」をクリックすると、カスタマーポータル画面が表示されサブスクリプションの契約状況の確認や、サブスクリプションのキャンセルをユーザが行うことができます。
動作確認を踏まえてここまでのサーバのコードを確認する
- 決済画面へのリダイレクト 「public/checkout.html」からリクエストを受けて「lookup_key」で対象の商品を検索し、「Stripe::Checkout::Session.create」でサブスクリプションの決済画面を作成しています。 この時、決済成功時の画面遷移先とキャンセル時の画面遷移先を指定しています。 指定されている画面はそれぞれ「public/success.html」と「public/cancel.html」が事前に用意されています。 その後、作成した決済画面のURLへリダイレクトさせています。
YOUR_DOMAIN = 'http://localhost:4242' post '/create-checkout-session' do prices = Stripe::Price.list( lookup_keys: [params['lookup_key']], expand: ['data.product'] ) begin session = Stripe::Checkout::Session.create({ mode: 'subscription', line_items: [{ quantity: 1, price: prices.data[0].id }], success_url: YOUR_DOMAIN + '/success.html?session_id={CHECKOUT_SESSION_ID}', cancel_url: YOUR_DOMAIN + '/cancel.html', }) rescue StandardError => e halt 400, { 'Content-Type' => 'application/json' }, { 'error': { message: e.error.message } }.to_json end redirect session.url, 303 end
- カスタマーポータル画面へのリダイレクト
決済が成功した場合、「public/success.html」へ遷移します。 「public/success.html」からカスタマーポータル画面を開くために、決済時のカスタマーIDをURLから取得してカスタマー情報を取得しています。 カスタマー情報を元にカスタマーポータル画面を作成後、作成したカスタマーポータル画面のURLへリダイレクトさせています。
post '/create-portal-session' do content_type 'application/json' # For demonstration purposes, we're using the Checkout session to retrieve the customer ID. # Typically this is stored alongside the authenticated user in your database. checkout_session_id = params['session_id'] checkout_session = Stripe::Checkout::Session.retrieve(checkout_session_id) # This is the URL to which users will be redirected after they are done # managing their billing. return_url = YOUR_DOMAIN session = Stripe::BillingPortal::Session.create({ customer: checkout_session.customer, return_url: return_url }) redirect session.url, 303 end
コードと実際の動作を照らし合わせて想定通りであることが確認できました。
まとめ
今回はサンプルコード通りに動作させてみたのでカスタマーが決済毎に都度作成される動作になっていますが、「Stripe::Checkout::Session.create」時にカスタマーを指定することで同一のカスタマーで決済処理できるなど、API仕様をよく確認して利用する必要があるなと感じました。
また、Webhook機能については記事が長くなるので省略しています。