Stripe Checkout / Customer Portal を利用して、シンプルな Subscription 管理システムを構築する

Stripe Checkout / Customer Portal を利用して、シンプルな Subscription 管理システムを構築する

Subscription(定額課金)システムを利用する場合、一般的には API / SDK を利用したプログラムを独自に開発して組み込んでいくやり方をとります。しかしその場合、「クレジットカード変更・追加・削除などの管理」や「支払い履歴の表示・PDF ダウンロード」といった機能・画面の実装が毎回必要です。

Stripe を利用している場合、Checkout / Customer Portal という2つの機能を利用することで、これらの定番的な機能・画面を簡単に組み込みすることができます。

Subscription(定額課金)システムイメージ

Stripe Customer Portal とは

Stripe で Subscription(定額課金)を提供する場合に利用する Stripe Billing には、契約中の顧客がカード情報を更新したり支払い履歴を確認したりといった操作を行うことができる「カスタマーポータル」を作る機能が用意されています。これが Stripe Customer Portal です。

これは Stripe の Customer ID を利用して、一時的に有効な URL を発行することができるシステムです。発行された URL にアクセスすることで、その顧客が契約している Subscription(定額課金)を管理するカスタマーポータルを利用できるようになります。

import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_API_KEY, {
      apiVersion: '2020-08-27',
    })

/**
  * StripeのCustomer IDからカスタマーポータルへのURLを発行する
  **/
export const getCustomerPortalURL = async (customerId: string) => {
    const result = await stripe.billingPortal.sessions.create({ customer: customerId })
    return result.url
}

これだけのコードで、顧客に応じたカスタマーポータルへの URL が作成できます。そしてそのカスタマーポータルでは、プラン管理・カード管理・請求先情報管理そして支払い履歴の確認と非常に多くのタスクを実行できます。

カスタマーポータルの例

Customer Portal で設定できること

この記事を書いている時点(2020年11月末)では、以下の設定が可能です。

  • 請求履歴の表示の可否
  • 請求先情報の更新の可否
    • メールアドレスの更新可否
    • 請求先住所の更新可否
    • 配送先住所の更新可否
    • 電話番号の更新可否
  • 支払い方法の更新可否
  • クーポンコードの入力可否
  • 定期支払いのキャンセルの可否
    • キャンセルのタイミング(今すぐ or 期間の終了後)
  • 定期支払いの更新の可否
    • 数量変更の可否
    • プラン変更時の差額処理(今すぐ or 期間の終了後)
カスタマーポータルの例-2

また、プランの変更を許可する場合には選択できるプランのリストを指定することができます。

プランのリスト指定の例

Subscription(定額課金)の新規作成は Stripe Checkout で

ここまで Stripe Customer Portal を用いたプラン管理方法を紹介しました。しかしこの Customer Portal だけでは、新規の契約を作成することができません。

新しく Subscription(定額課金)を始めれるようにするもっとも簡単な方法は、Stripe Checkout を利用することです。

Stripe Checkout は、Stripe が提供する商品または Subscription 購入フォームです。数行のコードを利用するだけで、管理するサーバー上を一切介さずにクレジットカードや顧客情報などを入力してもらうことができます。

Stripe Checkout 画面サンプル

Stripe Checkout を利用するには、そのコードを埋め込むサイトのドメイン登録と、利用できる商品の指定が必要です。また、Apple / Google Pay の利用可否などの調整もできます。

Stripe Checkoutカスタマイズ例のスクリーンショット

Checkout を有効にすると、Stripe に登録した商品や料金ごとに埋め込み URL を作成できます。

埋め込み URL を作成しているスクリーンショット

以下のコードは、作成されるコードスニペットの例です。

<!-- Load Stripe.js on your website. -->
<script src="https://js.stripe.com/v3"></script>

<!-- Create a button that your customers click to complete their purchase. Customize the styling to suit your branding. -->
<button
  style="background-color:#6772E5;color:#FFF;padding:8px 12px;border:0;border-radius:4px;font-size:1em"
  id="checkout-button-price_1HrFReDOP6RCnWSqZCOEeNr2"
  role="link"
  type="button"
>
  Checkout
</button>

<div id="error-message"></div>

<script>
(function() {
  var stripe = Stripe('pk_test_xxxxxxxxxxx');

  var checkoutButton = document.getElementById('checkout-button-price_1HrFReDOP6RCnWSqZCOEeNr2');
  checkoutButton.addEventListener('click', function () {
    // When the customer clicks on the button, redirect
    // them to Checkout.
    stripe.redirectToCheckout({
      lineItems: [{price: 'price_xxxxxxxx', quantity: 1}],
      mode: 'subscription',
      // Do not rely on the redirect to the successUrl for fulfilling
      // purchases, customers may not always reach the success_url after
      // a successful payment.
      // Instead use one of the strategies described in
      // https://stripe.com/docs/payments/checkout/fulfill-orders
      successUrl: 'https://www.example.com/success',
      cancelUrl: 'https://www.example.com/canceled',
    })
    .then(function (result) {
      if (result.error) {
        // If `redirectToCheckout` fails due to a browser or network
        // error, display the localized error message to your customer.
        var displayError = document.getElementById('error-message');
        displayError.textContent = result.error.message;
      }
    });
  });
})();
</script>

ユーザー管理システムと Stripe Customer Portal の連携を実装する

Stripe Checkout で Subscription(定額課金)を契約し、Stripe Customer Portal でその内容の確認や管理ができることがわかりました。そして欠けている最後のピースは、「 Checkout で契約した顧客のデータをどこで管理し、それをCustomer Portal に渡すか」です。

Stripe Checkout で購入が完了した時点で、Stripe 側にそのユーザーの Customer データが登録されます。このデータをそれぞれが持つユーザー管理システムに受け渡す方法は2つあります。

方法1 : successUrl の戻り値を利用したAPIを開発する

1つ目の方法は、Stripe Checkout での購入が完了した場合にアクセスするページにて、Stripe Customer データをシステムに登録するための API を呼び出すものです。

Stripe Checkout にアクセスするコード redirectToCheckout には、成功時にアクセスする URL を指定することができます。

    stripe.redirectToCheckout({
      // Do not rely on the redirect to the successUrl for fulfilling
      // purchases, customers may not always reach the success_url after
      // a successful payment.
      // Instead use one of the strategies described in
      // https://stripe.com/docs/payments/checkout/fulfill-orders
      successUrl: 'https://www.example.com/success',
...

この URL に ?session_id={CHECKOUT_SESSION_ID} を追加すると、購入時のセッションIDがクエリストリングに含まれる形でリダイレクトされるようになります。

    stripe.redirectToCheckout({
      // Do not rely on the redirect to the successUrl for fulfilling
      // purchases, customers may not always reach the success_url after
      // a successful payment.
      // Instead use one of the strategies described in
      // https://stripe.com/docs/payments/checkout/fulfill-orders
      successUrl: 'https://www.example.com/success?session_id={CHECKOUT_SESSION_ID}',
...

この ID を JavaScript で取得しましょう。

const urlParams = new URLSearchParams(window.location.search);
  const sessionId = urlParams.get('session_id');

そして API またはサーバー側の処理を呼び出し、session id から Stripe のCustomer を取得して、各システムに登録します。

/**
  * Session IDからStripe Customerのデータを引き出す
  **/
export const getStripeCustomerBySessionId = async (sessionId: string) => {
    const session = await stripe.checkout.sessions.retrieve(sessionId);
    if (!session || !session.customer) throw new Error('No such customer');
    const customerId =
      typeof session.customer === 'string'
        ? session.customer
        : session.customer.id;
    const customer = await this.stripe.customers.retrieve(customerId);
    return customer
}

あとはユーザー管理システムの metadata などから、この customer id を取得して Customer Portal の URL を作成する処理・API を実装すれば OK です。

方法2 : Webhook を利用してバックエンドで紐付けする

もう一つの方法は、Stripe の Webhook を利用するものです。

Stripe Checkout での購入が成功すると、checkout.session.completed イベントの Webhook が呼び出されます。このイベントを受け取ることで、先ほどと同様に Session ID を入手することができます。

ただしこちらの場合、Webhook を処理するプログラムが「どのユーザーのイベントであるか」を識別できる必要があります。そのため、redirectToCheckout の引数に clientReferenceId を追加し、ユーザー名をリクエストに含めるようにしましょう。

    stripe.redirectToCheckout({
        clientReferenceId: YourApplicationUsername,
...

その後、Webhook を処理するプログラムで session id を取得します。

/**
  * StripeのWebhookイベントオブジェクトからSession IDを取得する
  **/
const getSessionIdFromRequest = (event) => [
  if ("type" !== "checkout.session.completed") return null;
  return event.data.object.id
}

あとは方法1と同様に Customer データを取得して紐づける処理を実装しましょう。

こちらの方がシンプルに作ることができますが、Webhook を外部から利用されないように「署名シークレット」を発行する必要があります。

署名シークレットの発行イメージ

署名シークレットの検証には、「リクエスト body / リクエストHeader / Dashboard で発行した署名シークレットの値」の3つが必要です。

// リクエストbody
const payload = request.body

// リクエストHeaderから、stripe-signatureを取得する
const sig = request.headers['stripe-signature']

// Dashboardで発行した署名シークレット
const endpointSecret = 'whsec_...'

  try {
    // 正しいリクエストかを検証する
    const event = stripe.webhooks.constructEvent(payload, sig, endpointSecret);
   ...
  } catch (err) {
    throw new Error(`Webhook Error: ${err.message}`);
  }

さらに踏み込んだ開発 : Stripe SDKを利用して商品一覧を表示する

ここまででプラン数の少ないサービスであれば、Checkout のコードスニペットの埋め込みと、Customer Portal URL 発行処理の実装だけである程度の機能が提供できるようになります。

もし商品数が多い・または入れ替わりが激しいなどの場合には、ここにもう1つ API を追加することをお勧めします。それは商品一覧取得の API です。

Stripe Checkout のコードスニペットを読むと、「どの商品・価格を購入しようとしているか」の判定は price id を用いていることが伺えます。つまり、このprice id をまとめて取得し、それを表示できるように実装することで商品一覧・購入ボタンの表示を自動化することができます。

まず API またはサーバー側のプログラムにて、price id を含むリストを取得する処理を実装します。

export const listAllPrices = async () => {
    const { data: products } = await stripe.products.list()
    const productsWithPrices = await Promise.all(products.map(async product => {
        const { data: prices } = await StripeJSProvider.prices.list({
            product: typeof product === 'string' ? product : product.id,
        })
        return {
            name: product.name,
            description: product.description,
            images: product.images,
            prices: prices.map(price => ({
                id: price.id,
                nickname: price.nickname,
                unit_amount: price.unit_amount,
            }))
        }
    }))
    return productsWithPrices
}

そしてフロントエンドでは、jQuery や React などを用いてこの商品一覧を表示させましょう。

import { loadStripe, Stripe } from '@stripe/stripe-js';
const Products: FC<{products: any[]}> = ({products}) => {
    return (
     <>
       {products.map(product => {
           return (
            <Fragment key={product.id}>
              {product.prices.map((price) => (
                <Price key={price.id} price={price} product={product} />
              ))}
            </Fragment>
            )
       })}
     </>
    )
}

const Price: FC<{product: any; price: any}> = ({product, price}) => {
    const handleBuy = useCallback(() => {
        loadStripe('pk_test_xxxx')
            .then(stripe => {
                return stripe.redirectToCheckout({
                    lineItems: [{
                        price: price.id,
                        quantity: 1
                    }],
                    mode: 'subscription',
                    successUrl: 'http://localhost:4200/success/?session_id={CHECKOUT_SESSION_ID}',
                    cancelUrl: 'http://localhost:4200/'
                })
            })
    }, [])
    return (
        <div>
            <p>{price.nickname} - {product.name}</p>
            <button onClick={handleBuy}>Buy</button>
        </div>
    )
}

このように実装することで、Stripe の商品・料金を更新するだけでサイトの商品リストを更新できるようになります。lineItems の値をカスタマイズできるように作り込むことで、より本格的なカートシステムにすることも可能でしょう。

なお、実際の実装では Context を用いたり、ユーザー管理システムとの連携などの処理も入ります。あくまでこのコードは「このようなコードを書きます」というイメージをお伝えするものだとご理解いただければと思います。

Stripe Checkout + Customer Portal + APIで実装したデモサイト

今回紹介した手法を使ったStripe Billingのデモサイトを用意しました。

Stripe Billingのデモサイトイメージ
https://main.d1j1xi457tj0us.amplifyapp.com/

商品一覧はNestjsで構築したHTTP APIから、StripeのAPIで商品・料金のリストを取得し、それをReactにて表示しています。

Buyボタンをクリックすると、CheckoutのURLが作成されて購読画面に遷移します。デモサイトですので、各種情報は「4242 4242 4242 4242」などのダミーデータで問題ありません。

購読が完了した画面イメージ

購読が完了すると、デモサイトに戻ってCustomer PortalのURLが表示されます。(表示されない場合は一度リロードしてください)

デモサイトでは、SessionStorageを利用してStripeから送られたIDを記録しています。そのため、実際の運用に組み込む場合はここからAuth0やAmazon Cognito User Poolsなどと連携させる実装が必要となります。

自前で実装が必要なケースも

このようにシンプルにカスタマーポータルや商品一覧を実装することができますが、サービスによっては従来通り自前で開発した方が良いケースももちろん存在します。

特に Stripe Checkout / Customer Portal はどちらも Stripe が用意する画面で、Stripe が許可している範囲のカスタマイズのみ可能な仕組みです。

そのため、ブランドイメージにあわせた UI や領収書を作成したい場合には、自身のアプリケーション内に実装することをおすすめします。

また、Stripe Checkout には、Subscription のパッケージ料金や段階的な料金など、対応していない商品がいくつか存在します。そのため、そのような商品を取り扱っている場合には、こちらも自前で実装する必要があります。

Stripe でコアビジネス外の要件をよりシンプルに

Stripe を用いることで、Subscription(定額課金)に欠かせない「支払い履歴表示」「カード情報管理」「プラン変更・解約」といった機能を最小限のコストでサービスに組み込むことができます。

特に Checkout / Customer Portal が利用できる案件であれば、これらの実装を自前で行う工数・コストを削減して自身のコアビジネスに集中できることは、とても大きなメリットになるでしょう。

よりよいサービス開発・運営のためにも、Stripe をより便利に使いこなすことが重要になるかもしれません。

横浜市立大学附属病院 次世代臨床研究センターリクルートダイレクトスカウト
JOLLY GOOD!エピックベース株式会社
有限会社ワグ株式会社デジタルガレージ
LegalOn Technologies日本協創投資
SmartHRSHARP
mikihouseInternet Society
INFOBAHN GROUPfreee
CanCam旭化成
横浜市立大学附属病院 次世代臨床研究センターリクルートダイレクトスカウト
JOLLY GOOD!エピックベース株式会社
有限会社ワグ株式会社デジタルガレージ
LegalOn Technologies日本協創投資
SmartHRSHARP
mikihouseInternet Society
INFOBAHN GROUPfreee
CanCam旭化成
Contact

当社へご興味をお持ちいただきありがとうございます。
「こんなことやってみたい!」と、ぜひ気軽にご相談ください。
担当者よりご連絡差し上げます。