こんにちは、わいけいです。
今回の記事ではStripeを使ってクレジットカード決済を実装します。
前置き
開発規模の大小を問わず、実装したプロダクトで収益を得るにはユーザーからお金を払ってもらう必要があります。
しかし、決済の仕組みを自分たちで実装するのは色々な困難が伴います。
キャッシュレス決済といえばクレジットカードですが、クレカ決済の実装には例えば下記のような課題があります。
- 実装ミスなどで、ユーザーのクレカ情報が流出すると大事故になる
- PCI DSSというセキュリティ要件を守り、決済ブランドの審査に通過するには莫大なコストがかかる
結論として、個人や小規模な企業がクレカ決済を独自実装するのはあまり現実的ではないと私は思っています。
こういった問題のソリューションとなるのがStripeです。
私のSNSアカウント等です。
今後もPython・LLM・Go・Web開発などのトピックについて発信していくのでフォローしていただけると喜びます。
X: わいけい
LinkedIn: ykimura517
Github: ykimura517
そもそもStripeとは
まずは そもそもStripeとはなんぞや? というところから見ていきましょう(既に知っている方は読み飛ばしてください)。
Stripeは、
- ユーザーのカード情報をStripe上の安全な環境に保存しておける
- カード決済時に必要なセキュリティ要件がクリアされているので、Stripeを通じて安全にユーザーに対して請求を行える
- StripeのAPIと連携して、自分たちのサービスに柔軟に決済機能を組み込める
- 手数料が完全従量課金(初期費用0)で、手数料も3.6%(2024年5月時点)とリーズナブル
といったメリットを備えたグローバルなサービスです。
(特に手数料率3.6%は魅力的ですね。)
さて、以上のようにStripeはとても便利です。
しかし、出来ることが多い分その全容を理解するのはかなり大変です。
まるっと提供されている機能だけでも、例えば以下のようなものがあります。
製品・機能 | 説明 |
---|---|
Stripe Checkout | カスタマイズ可能な支払いページを提供し、ウェブサイトに簡単に組み込める。 |
Stripe Elements | カスタマイズ可能なUIコンポーネントで、フォームを通じて決済情報を安全に収集。 |
Stripe Billing | サブスクリプションや繰り返し課金を管理するためのツール。 |
Stripe Connect | マーケットプレイスやプラットフォームの支払いを容易にする。 |
Stripe Radar | 金融不正取引防止ツールで、異常な取引を検出しリスクを管理する。 |
Stripe Atlas | スタートアップがアメリカで会社を設立し、銀行口座を開設するサポートを提供。 |
Stripe Issuing | 企業が自社のクレジットカードやデビットカードを発行できるサービス。 |
Stripe Treasury | 企業が顧客に銀行のような金融サービスを提供するためのAPI。 |
Stripe Terminal | 実店舗での支払いを処理するためのPOSデバイスとSDK。 |
ぱっと見、よく分からないものも多いと思います。
実際の現場では、これらのサービス全てを理解するのはコスパが良いとは言えません。
それよりは、ユースケースに合わせて動くものを作りながら学んでいった方がいいでしょう。
今回実装する機能の説明
そこで、今回は一つのサンプルとして、下記のような機能を備えたアプリを実装してみます。
- ユーザーがクレジットカードを登録する
- そのクレジットカードに対して課金する
- ユーザーのクレジットカードを必要に応じて削除する
クレカ決済を導入する際の最も基本的なフロー(というか最小単位)ですね。
純粋にクレカ決済をするだけならば、今回紹介する内容よりも簡単なやり方はあります。
しかし、その場合決済のたびにユーザーがクレカ情報を入力する必要があったりと、何かと不便な点があったりします。
サービスを運用していく上ではユーザーに何度もクレカ情報を入力させるのはCVRの低下を招くことが強く懸念されます。
そのため、今回は一度ユーザーにクレカを登録させ、以降の支払いは既に登録済みのクレカに対して行う形式でいきます。
注意事項として、今回は
- stripeには既に登録しており、APIの公開キー&シークレットキーを発行済み
- 簡素化のため、webhookを利用しない
- SPA形式のサービスへの組み込みを想定する(他の形式の場合でも大元のコードは流用可です)
- バックエンドのコードはstripeを扱う主要部分のみを実装する(Web APIの体裁にするのは各自の実装に委ねる)
という前提のもとやっていきたいと思います。
なお、フロントエンドはjavascript,バックエンドはPythonとGoの2種類の実装を提示します。
※StripeのAPIキーについてはこちら を参照してください。APIキーは、アカウント作成後ダッシュボードの開発者用画面をポチポチすれば作れます。
また、単純化のためにエラーハンドリングをやっていなかったり、秘匿情報をコードにそのまま書いていたりしますが実運用時はこの辺もしっかりやる必要があるので注意してください。
Stripeへのユーザー登録とカードの紐付け
まず、Stripe上にユーザーデータを登録し、そのユーザーにカードを紐付けていきましょう。
まずStripe上のユーザーを作成する部分です。
stripeにユーザーが作成されると同時に、stripe上でのuser_id
(stripe上でユーザーはcustomerと呼ばれるので正確にはcustomer_id
という値)が返却されます。
ほとんどの場合、これを取得した時にバックエンドのDBでuser_id
とstripe上でのcustomer_id
を紐付ける必要があると思われます。
また、Stripeへユーザー登録するタイミングはアプリ要件によって色々ありますが、多くの場合は
- アプリにユーザーが登録した時に必ずStripeにも登録する
- ユーザーが課金することが確定した段階でStripeに登録する
の2パターンになりそうです。
要件に合わせて適切な実装を行ってください。
package main
import (
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/customer"
)
func main() {
// Stripeのシークレットキーを設定
stripe.Key = "your secret key"
stripe_cus_id, err := createStripeCustomer()
if err != nil {
panic(err)
}
println(*stripe_cus_id)
}
func createStripeCustomer() (*string, error) {
params := &stripe.CustomerParams{
Name: stripe.String("Jenny Rosen from Go code"),
Email: stripe.String("jennyrosen@example.com"),
}
result, err := customer.New(params)
if err != nil {
return nil, err
}
return &result.ID, nil
}
from stripe import StripeClient
from stripe import Customer
api_key = "your secret key"
client = StripeClient(api_key=api_key)
customer: Customer = client.customers.create(
params={
"email": "hoge@example.com",
"name": "John Doe from python code",
},
)
print(customer.id)
これらのコードを実行後、Stripeのダッシュボードを見てみると顧客データが増えていることが確認できると思います。
繰り返しになりますが、実際にはここで取得したstripeのcustomer_id
とアプリ上でのuser_id
をDB上で紐付けて保存することになるでしょう。
登録したユーザーへクレジットカードを紐付け
次にこのユーザーにクレカ情報を紐付ける部分です。
概要としては
・フロントエンドからstripeに直接カード登録リクエストを投げる
・バックエンドでstripe上のカードとユーザーを紐付ける
というフローを踏みます。
図にすると以下のようになります。
まず、カード自体を登録するフロント側のサンプルです。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payment</title>
<script src="https://js.stripe.com/v3/"></script>
<style>
/* Stripe Elementsのスタイル設定 */
.StripeElement {
box-sizing: border-box;
height: 40px;
padding: 10px 12px;
border: 1px solid transparent;
border-radius: 4px;
background-color: white;
box-shadow: 0 1px 3px 0 #e6ebf1;
-webkit-transition: box-shadow 150ms ease;
transition: box-shadow 150ms ease;
}
.StripeElement--focus {
box-shadow: 0 1px 3px 0 #cfd7df;
}
.StripeElement--invalid {
border-color: #fa755a;
}
.StripeElement--webkit-autofill {
background-color: #fefde5 !important;
}
</style>
</head>
<body>
<form id="payment-form">
<div id="card-element"></div>
<button type="submit">Submit Payment</button>
</form>
<script>
//API公開キーを設定
var stripe = Stripe('your publishable key');
var elements = stripe.elements();
var card = elements.create('card');
card.mount('#card-element');
var form = document.getElementById('payment-form');
form.addEventListener('submit', function(event) {
event.preventDefault();
stripe.createPaymentMethod('card', card).then(function(result) {
if (result.error) {
console.log(result.error.message);
} else {
// stripeから渡されたpayment methodのid
console.log(result.paymentMethod.id)
// このidをバックエンドに送り、バックエンドでstripeのcustomerと紐付ける
// 実際にはほとんどの場合でpaymentmethodのidとともにサーバーのDBのuser idを特定できる情報(ログインしているユーザーの認証トークンなど)をサーバーに送る必要があると思われる
fetch('/save-payment-method', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({paymentMethodId: result.paymentMethod.id}),
}).then(function(response) {
response.json().then(function(data) {
console.log(data);
});
});
}
});
});
</script>
</body>
</html>
※このままテストをする場合は、このhtmlファイルが置いてあるディレクトリで
python3 -m http.server 8000
のコマンド等でこのファイルをサーバーから配信する必要があるかと思います。(React
とかを走らせている場合は不要です)
ここで、クレジットカードの登録フローはフロントエンドとStripeのやり取りで完結していることに注意しましょう。
(後で提示するように、登録したカードとユーザーの紐付け部分にはバックエンドが絡みます。が、あくまで紐付け部分だけです。)
これは、セキュリティ面を考慮してこの仕様が推奨されているものと思われます。
バックエンドにユーザーのカード情報が一度でも流されると、情報が思わぬところで保存されてしまうリスクがあります。
例えば、意図せざるログが出てしまってそれが残留してしまう等ですね。恐ろしや。
バックエンドが介在せずに処理を行っているので、必然的にクレカ登録工程(PaymentMethod
作成の工程)では、stripeの公開APIキーなどの公開情報しか使っていません。
つまり理屈の上では、その気になれば誰でも勝手に他人のサービスにクレカを登録できてしまいます。
しかし、シークレットキーが無ければ登録されたクレカに対して決済等を行うことは出来ません。
なので問題なし(というかカード情報がバックエンドを経由しないメリットの方が大きい)、という設計思想なのだと思います。
さて、次はバックエンドでフロントからpaymentmethod_id
を受取り、それをstripe上のcustomerと紐付けましょう。
package main
import (
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/paymentmethod"
)
func main() {
// Stripeのシークレットキーを設定
stripe.Key = "your secret key"
//実際には多くの場合フロントから送られた認証トークンなどを基にuser_idを特定し、
//それを使ってDBから検索して取得することになるはず
stripe_customer_id := "cus_xxxxxxxx"
stripe_paymentmethod_id := "pm_xxxxxxxxxxxxxxxx" //実際にはフロントから受け取る想定
err := attachCardToCustomer(stripe_customer_id, stripe_paymentmethod_id)
if err != nil {
panic(err)
}
println("Success")
}
func attachCardToCustomer(stripe_customer_id string, stripe_paymentmethod_id string) error {
// PaymentMethodをCustomerに紐付ける
_, err := paymentmethod.Attach(
stripe_paymentmethod_id,
&stripe.PaymentMethodAttachParams{
Customer: stripe.String(stripe_customer_id),
},
)
if err != nil {
return err
}
return nil
}
from stripe import StripeClient
from stripe import PaymentMethod
api_key = "your secret key"
# 実際には多くの場合フロントから送られた認証トークンなどを基にuser_idを特定し、それを使ってDBから検索して取得することになるはず
stripe_customer_id = "cus_xxxxxxxxx"
# 実際にはフロントから受け取る想定
stripe_payment_method_id = "pm_xxxxxxxx"
client = StripeClient(api_key=api_key)
pm: PaymentMethod = client.payment_methods.attach(
payment_method=stripe_payment_method_id,
params={
"customer": stripe_customer_id,
},
)
print("success!")
このコードが実行されると、ダッシュボード上で顧客データにpaymentMethod
作成時に入力したクレカが紐付けられているのが確認できると思います。
クレジットカードに対して請求
次にユーザーと紐付けられたクレジットカードに対して請求する部分を作っていきましょう。
全体の流れとしては以下のようなフローになります。
- フロントエンドからバックエンドに
user_id
を送る - バックエンドで
user_id
から そのユーザーに紐付いた、stripe上のPaymentMethod ID
を取得する - バックエンドからstripe上で指定した
PaymentMethod
(今回の場合はクレジットカード)からPaymentIntent
というオブジェクトを作成し、これを介してチャージする
いくつか補足します。
ステップ2では、Stripe上で1つのcustomer
に複数のPaymentMethod
( 今回の場合だとクレジットカード)が 紐付いている可能性があることに注意しましょう。
どういった条件を満たすクレジットカードを使うかは アプリケーションの要件によって異なります。
今回は単純化のために、そもそも1枚しかクレジットカードを登録できない仕様のアプリケーションを作っていると想定しましょう。
この場合は機械的にリストの一番始めのクレジットカードに対して課金すればOKです。
次にステップ3についてです。
PaymentIntent
とはざっくり言うと ユーザーの支払い意向を表すオブジェクトです。
あくまで支払いたいという意向に過ぎないなのでPaymentIntent
を作成したからと言って必ずしもその場で課金されるわけではありません。
PaymentIntent
がカバーする「支払い意向」はクレジットカードによる即時払いだけではありません。
例えば銀行振込のように必然的に支払いがその処理で完結しないアクションも含まれます。
このような支払い方法を使いたい場合、後々支払いが完了したことをバックエンドが認識するためにwebhookの受け口を設置する必要があります。
ただ、ここではクレジットカードによる即時払いのみに対応したアプリケーションを作っていると想定しているので、一旦この辺の事情は忘れてしまっても問題ありません。
クレカで即時支払いをする場合、PaymentIntent
を作成する時に confirm=True
という パラメータを設定することでその場でクレジットカードに課金を行うことができます。
問題はクレジットカードによっては3D認証などのさらなる認証ステップを挟まなければいけないケースがあることです。
例えば(最近ではもう非推奨になっている規格ですが)3Dセキュア1の場合はカードごとに指定された URL にユーザーをリダイレクトしなければなりません。
そのケースの場合は、作成されたPaymentIntentオブジェクトの中にnext_action
というフィールドが含まれます。
ここにリダイレクトURLがredirect_to_url.url
として提供されます。
このURLは、ユーザーを認証ページにリダイレクトするために使用されます。
これをバックエンドからフロントエンドに渡してあげましょう。
そして フロントエンドでこのURLにユーザーをリダイレクトして、ユーザーはリダイレクト先で追加の認証を完了させます。
追加の認証が完了するとPaymentIntent
のステータスが変更され支払いが完了となります。
しかし 最終的に支払いが完了したことをバックエンドで把握するには これまたWebhook等の手段が必要になります。
諸々対応しているとやることが無限に増えるため、今回は3D認証を要求するカード対応は諦めるということにしておきましょう。
全体として、stripeでの課金処理はカードの登録処理とはまた違った複雑さがあります。
カード登録とは違い、バックエンドとStripeで大体の処理が完結しているのは嬉しい点です。
一方でPaymentIntent
という新たなオブジェクトが出てきたことでややこしさを感じている人もいるかもしれません。
(あと3D認証とかに真面目に対応しだすとどんどん話がややこしくなっていくことでしょう。)
フロントからはユーザーが識別できる情報を渡すだけなので、今回はバックエンドのサンプルコードのみを示します。
package main
import (
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/paymentintent"
"github.com/stripe/stripe-go/paymentmethod"
)
func main() {
// Stripeのシークレットキーを設定
stripe.Key = "your secret key"
//実際には多くの場合フロントから送られた認証トークンなどを基にuser_idを特定し、
//それを使ってDBから検索して取得することになるはず
stripe_customer_id := "cus_xxxxxxxxx"
amount := 1000 //課金額
// 顧客のPaymentMethodリストを取得
paymentMethodListParams := &stripe.PaymentMethodListParams{
Customer: stripe.String(stripe_customer_id),
Type: stripe.String("card"),
}
paymentMethods := paymentmethod.List(paymentMethodListParams)
// 最初のカードを取得
var firstPaymentMethod *stripe.PaymentMethod
for paymentMethods.Next() {
pm := paymentMethods.PaymentMethod()
firstPaymentMethod = pm
break
}
if firstPaymentMethod == nil {
panic("No payment method found")
}
// PaymentIntentを作成して課金
params := &stripe.PaymentIntentParams{
Amount: stripe.Int64(int64(amount)),
Currency: stripe.String(string(stripe.CurrencyJPY)),
PaymentMethod: stripe.String(firstPaymentMethod.ID),
Confirm: stripe.Bool(true),
Customer: stripe.String(stripe_customer_id),
}
pi, err := paymentintent.New(params)
if err != nil {
panic(err)
}
//paymentIntentの主要な情報を表示
println("PaymentIntent ID: " + pi.ID)
println("PaymentIntent Status: " + string(pi.Status))
println("PaymentIntent Amount: " + string(pi.Amount))
println("PaymentIntent Currency: " + string(pi.Currency))
println("PaymentIntent PaymentMethod: " + string(pi.PaymentMethod.ID))
println("PaymentIntent ConfirmationMethod: " + string(pi.ConfirmationMethod))
//フロントに必要な情報を返す
}
from stripe import StripeClient
from stripe import PaymentMethod, ListObject
api_key = "your secret key"
# 実際には多くの場合フロントから送られた認証トークンなどを基にuser_idを特定し、それを使ってDBから検索して取得することになるはず
stripe_customer_id = "cus_xxxxxxx"
# 実際にはフロントから受け取る想定
stripe_payment_method_id = "pm_xxxxxxxx"
# 課金額
amount = 20000
client = StripeClient(api_key=api_key)
payment_methods: ListObject[PaymentMethod] = client.customers.payment_methods.list(
customer=stripe_customer_id, params={"type": "card"}
)
pm_id = payment_methods.data[0]["id"]
# paymentIntentを作成して即時支払い
payment_intent = client.payment_intents.create(
params={
"amount": amount,
"currency": "jpy",
"payment_method": pm_id,
"customer": stripe_customer_id,
"confirm": True,
# 3Dセキュアなどのリダイレクト設定を省略するために下記を設定
"automatic_payment_methods": {"enabled": True, "allow_redirects": "never"},
},
)
print("success!")
このコードの実行が成功すると、Stripeのダッシュボードで売上が増えているのが確認できるはずです!
本番環境でもこれだけ簡単に売上が増えればいいのに、、、、、、。
クレジットカードを削除
最後に、クレジットカードを削除していきましょう。
全体のフローとしては、下記のような流れになります。
- フロントから該当ユーザーを特定できる情報をバックエンドに送信
- バックエンドからStripeにリクエストし、そのユーザーに紐付いている
PaymentMethod
の一覧を取得 - リストの最初の
PaymentMethod
を削除
今回は決済手段として各ユーザー毎に一枚のクレカのみを登録可能な仕様を想定しています。
そのため、削除するクレカは単純にPaymentMethod
のリストの最初のものを指定すればOKです。
フローを図にすると下記のようになります。
さて、削除についてもサンプルコードを書いてみます。
例によってバックエンドとstripeのやりとりのみを掲載します。
package main
import (
"github.com/stripe/stripe-go"
"github.com/stripe/stripe-go/paymentmethod"
)
func main() {
// Stripeのシークレットキーを設定
stripe.Key = "your secret key"
//実際には多くの場合フロントから送られた認証トークンなどを基にuser_idを特定し、
//それを使ってDBから検索して取得することになるはず
stripe_customer_id := "cus_xxxxxxxx"
// 顧客のPaymentMethodをリスト取得
paymentMethodListParams := &stripe.PaymentMethodListParams{
Customer: stripe.String(stripe_customer_id),
Type: stripe.String("card"),
}
paymentMethods := paymentmethod.List(paymentMethodListParams)
// 最初のPaymentMethodを削除
var pmIDToDelete string
if paymentMethods.Next() {
pm := paymentMethods.PaymentMethod()
pmIDToDelete = pm.ID
}
if pmIDToDelete == "" {
panic("No payment methods found for the customer")
}
// PaymentMethodを削除
_, err := paymentmethod.Detach(pmIDToDelete, nil)
if err != nil {
panic(err)
}
println("PaymentMethod deleted")
}
from stripe import StripeClient
from stripe import PaymentMethod, ListObject
api_key = "your secret key"
# 実際には多くの場合フロントから送られた認証トークンなどを基にuser_idを特定し、それを使ってDBから検索して取得することになるはず
stripe_customer_id = "cus_xxxxxxxx"
client = StripeClient(api_key=api_key)
payment_methods: ListObject[PaymentMethod] = client.customers.payment_methods.list(
customer=stripe_customer_id, params={"type": "card"}
)
pm_id = payment_methods.data[0]["id"]
# paymentMethodを削除
payment_intent = client.payment_methods.detach(
payment_method=pm_id,
)
print("success!")
このコードの実行に成功すれば、ダッシュボードの顧客情報にて顧客と紐付いていたクレカが消えているのを確認できるはずです。
まとめ
今回の記事で示したように、Stripeを使えば自分たちのサービスに決済を手軽に組み込むことが出来ます。
これを機に開発したプロダクトから収益を得られる方が一人でも増えることを願っております!
なお、薄々感じた方もいると思いますが、クレカ決済の仕組み自体がそれなりに複雑化していることもあり特に認証周りを真面目に実装するとなると色々な困難が発生します。
また今回の記事ではエラーハンドリングを全然真面目にやらなかったのですが、実運用ではむしろそこがキモになるかもしれません。
ここは注意してください。
最後に、今回の記事が役にたったと感じていただけた方は、私のSNSをフォローしていただけると喜びます。
私のSNSアカウント等です。
今後もPython・LLM・Go・Web開発などのトピックについて発信していくのでフォローしていただけると喜びます。
X: わいけい
LinkedIn: ykimura517
Github: ykimura517
質問やお仕事の依頼などがありましたら、DMでどうぞ(返信出来ない場合も多々あるので申し訳ないですがその場合はご容赦ください)。
Zennに投稿したバージョン:
https://zenn.dev/spiralai/articles/e9704cd8879c0f