0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ruby on Railsでのストライプ課金システムの開発方法まとめ

0
Posted at

0. はじめに

SaaS開発において欠かせない機能の一つが「サブスクリプション(継続課金)」の実装です。

本記事では、Ruby on RailsとStripe APIを組み合わせ、堅牢な課金システムを構築する手順を解説します。

単なる決済導入だけでなく、Webhookを利用したデータベースとの同期や、Stripe CLIを用いたローカルテストなど、実務で運用するために必要な工程をステップバイステップでまとめました。

これから自社サービスに決済機能を組み込みたいと考えている方の参考になれば幸いです。

1. Stripeダッシュボードで商品登録

まずはコードを書く前に、Stripeの管理画面上で「商品(サブスクリプションプラン)」を作成します。

金額や請求サイクル(月次・年次)をStripe側で管理することで、将来的に料金改定があった場合でも、Rails側のコードを修正することなくダッシュボード上の設定変更だけで対応できるようになります。

1.1 アカウントにサインイン

まず、Stripeのダッシュボードにログインします。
スクリーンショット 2025-12-04 12.24.42.png

1.2 商品カタログへ移動

ログイン後、画面左上の 「テスト環境」 がオン(オレンジ色)になっていることを確認してください。 左サイドバーにある 「商品カタログ」 をクリックします。

次に、商品カタログ画面の右上にある 「+ 商品を作成」 ボタンをクリックします。
スクリーンショット 2025-12-04 12.28.13.png

1.3 プラン情報の入力

商品の詳細を入力します。

スクリーンショット 2025-12-04 12.29.55.png

例)

  • 名前: エントリープラン
  • 料金: 2,980
  • 支払い体系: 「継続」 を選択(ここがサブスクリプション設定です)
  • 請求期間: 「毎月」

入力が終わったら、右上の 「商品を追加」 をクリックして保存します。

1.4 【重要】Price ID の取得

  • 画面中央にある 「料金」 という見出しを探してください
  • そのすぐ下に 「¥2,980」 と書かれた行があります
  • その行の一番右端にある「…」(3点リーダー)をクリックしてください
  • メニューが表示されるので、「IDをコピー」をクリックします

スクリーンショット 2025-12-04 12.39.57.png

【なぜ必要なの?】
Rails側で「2,980円」と金額を直接コードに書くのではなく、「このIDの商品を決済画面に出して」とStripeに指示を送るために使用します。

2 Webhook設定

Stripe側で「決済が完了した」「サブスクリプションが更新された」といったイベントが発生した際、その情報をRailsアプリケーションに自動通知するための「Webhook」を設定します。

2.1 開発者ダッシュボードへ移動

ダッシュボード右上の「開発者」をクリックし、左メニューの「Webhook」タブを選択します。 続いて、「エンドポイントを追加」ボタンをクリックします。
スクリーンショット 2025-12-05 20.32.35.png

2.2 エンドポイントの追加(通知先URL)

「イベントの送信先」を作成する画面に進むと、まずイベントの発生元を選択する画面が表示されます。
ここでは 「Your account(自身のアカウント)」 を選択してください。

スクリーンショット 2025-12-05 20.32.48.png

補足: なぜ Your account なのか?

  1. Your account: 自社の商品を販売する一般的なSaaSやECサイトの場合(今回のケース)。
  2. Connected accounts: メルカリやUberのように、プラットフォームとして他者の決済を仲介する場合(Stripe Connect)に使用します。

2.3 監視するイベントの選択

「リッスンするイベントを選択」をクリックし、Rails側で処理したいイベントにチェックを入れます。 今回の実装では以下の3つが重要です。

checkout.session.completed
初回決済が完了したタイミング(ここでDBに契約情報を作成します)

invoice.payment_succeeded
毎月の支払いが成功したタイミング(有効期限の更新や、クレジットのリセットを行います)

customer.subscription.deleted
解約されたタイミング

スクリーンショット 2025-12-05 20.34.00.png

2.4 「エンドポイントURL」を入力します。

次に、イベントをどのような技術で受け取るかを選択します。 RailsアプリケーションのURLにデータを送りたいので、「Webhook endpoint」 を選択し、「Continue」をクリックします。
スクリーンショット 2025-12-05 20.34.46.png

※補足: Amazon EventBridgeとは?

AWSのサーバーレス環境(Lambdaなど)でイベントを受け取りたい場合に選択するオプションです。今回はRailsアプリで直接受け取るため選択しません。

2.5 エンドポイントURLの設定

設定画面に進んだら、「Endpoint URL」を入力します。

開発環境: 一旦仮のURLを入力するか、後述するStripe CLIを使ってテストします(CLIを使う場合はここの画面設定は不要ですが、本番デプロイ時には必須となります)。

スクリーンショット 2025-12-05 20.38.10.png
最後に「Create Destination」を選択するとwebhookの設定の完了です。

2.6 署名シークレット(Signing Secret)の取得

設定を保存すると、作成したWebhookの詳細画面が表示されます。
画面右側にある 「Signing secret」 という項目の「Reveal(表示)」ボタンをクリックしてください。

whsec_ から始まる文字列が表示されます。これが「署名シークレット」です。

スクリーンショット 2025-12-05 20.40.47.png

3.実装手順

3.1 gemのインストール

gem 'stripe'

bundle installを実行。

3.2 環境変数の設定 (.env または credentials.yml.enc)

STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_SIGNING_SECRET=whsec_... # Step 6で取得しますが、枠だけ用意

【各キーの役割と注意点】

▪️ STRIPE_PUBLISHABLE_KEY (公開可能キー)

フロントエンドで使用します。ブラウザからStripeへ安全にカード情報を送信するために必要です。このキーは公開されても問題ありません。

▪️ STRIPE_SECRET_KEY (シークレットキー)

Railsサーバー側で使用します。API操作の全権限を持つため、絶対に外部に流出させないでください。

▪️ STRIPE_SIGNING_SECRET (署名シークレット)

Webhookの検証に使用します。外部からの「なりすましリクエスト」を防ぎ、通知が正当なStripeからのものであることを保証するための重要なキーです。

3.3 subscriptions テーブル作成

subscriptions
class CreateSubscriptions < ActiveRecord::Migration[8.0]
  def change
    create_table :subscriptions do |t|
      t.references :user, null: false, foreign_key: true
      t.string :stripe_subscription_id
      t.string :plan # "basic", "pro"
      t.string :status # "active", "canceled" 等
      t.datetime :current_period_start
      t.datetime :current_period_end
      t.integer :generations_count, default: 0
      t.timestamps
    end
    
    # Userに顧客IDを追加
    add_column :users, :stripe_customer_id, :string
    add_index :users, :stripe_customer_id
  end
end

stripe_subscription_idはStripe側で発行される「サブスクリプション契約ごとの管理番号(ID)」になります。

このstripe_subscription_idはRailsのデータベースと、Stripeのデータベースを紐付けるタイミングで利用します。

次に、subscriptions テーブルを作成します。

Subscription
class Subscription < ApplicationRecord
  belongs_to :user

  # ステータスの管理(便利なのでenumを使うのがおすすめです)
  # Stripeのstatusと合わせると管理しやすいです
  enum :status, {
    incomplete: 'incomplete',
    incomplete_expired: 'incomplete_expired',
    trialing: 'trialing',
    active: 'active',
    past_due: 'past_due',
    canceled: 'canceled',
    unpaid: 'unpaid'
  }, prefix: true

  # バリデーション(必須項目のチェック)
  validates :stripe_subscription_id, presence: true, uniqueness: true
end

3.4 Userモデルへの関連付け

ユーザーからサブスクリプションを参照できるように、app/models/user.rb にも追記が必要です。

user
class User < ApplicationRecord
  # ... (deviseなどの既存コード) ...

  # 1人のユーザーは1つのサブスクリプションを持つ想定
  has_one :subscription, dependent: :destroy
  
  # ...
end

4. 決済画面(Checkout)の実装

ユーザーが「契約する」ボタンを押した時の処理です。

Rails側でStripeの決済URLを発行し、そこへリダイレクトさせます。

SubscriptionsController
class SubscriptionsController < ApplicationController
  before_action :authenticate_user!

  def new
    # プラン選択画面の表示
  end

  def create
    # フロントから送られてきたプランID (例: 'price_xxxxx')
    price_id = params[:price_id] 
    
    # すでに顧客IDがあれば使い回す、なければStripe上に作成
    customer_id = current_user.stripe_customer_id
    unless customer_id
      customer = Stripe::Customer.create(email: current_user.email)
      current_user.update(stripe_customer_id: customer.id)
      customer_id = customer.id
    end

    # Checkoutセッション作成
    session = Stripe::Checkout::Session.create({
      customer: customer_id,
      payment_method_types: ['card'],
      line_items: [{ price: price_id, quantity: 1 }],
      mode: 'subscription',
      success_url: root_url + '?session_id={CHECKOUT_SESSION_ID}', # 決済成功時の戻り先
      cancel_url: new_subscription_url, # キャンセル時の戻り先
      # Webhookで誰の決済か判別するためにIDを埋め込む(メタデータ利用も可)
      client_reference_id: current_user.id 
    })

    # Turboを使っている場合、外部リダイレクトさせる
    redirect_to session.url, allow_other_host: true
  end
end

5.Webhookの実装(最重要)

Stripe上で発生した「決済完了」や「翌月の自動更新」などのイベント通知を、Rails側でリアルタイムに受け取る仕組みです。

この通知をトリガーに自社アプリのデータベース(契約ステータスや有効期限)を更新することで、Stripe側の状態と常に同期させることができます。

StripeWebhooksController
class StripeWebhooksController < ApplicationController
  # CSRF保護を無効化(StripeからのPOSTを受け取るため)
  skip_before_action :verify_authenticity_token

  def create
    payload = request.body.read
    sig_header = request.env['HTTP_STRIPE_SIGNATURE']
    endpoint_secret = ENV['STRIPE_SIGNING_SECRET'] # Step 6で取得

    begin
      event = Stripe::Webhook.construct_event(payload, sig_header, endpoint_secret)
    rescue JSON::ParserError, Stripe::SignatureVerificationError => e
      head :bad_request
      return
    end

    # イベントごとの処理
    case event.type
    when 'checkout.session.completed'
      # 初回契約完了時
      session = event.data.object
      handle_checkout_session_completed(session)
      
    when 'invoice.payment_succeeded'
      # 毎月の支払成功時(更新時)
      invoice = event.data.object
      handle_payment_succeeded(invoice)
      
    when 'customer.subscription.deleted'
      # 解約時
      subscription = event.data.object
      handle_subscription_deleted(subscription)
    end

    head :ok
  end

  private

  def handle_checkout_session_completed(session)
    user = User.find(session.client_reference_id)
    # ここでSubscriptionsテーブルにレコードを作成
    # session.subscription から詳細情報を取得して保存
  end

  def handle_payment_succeeded(invoice)
    # StripeのサブスクIDからDBを検索
    subscription = Subscription.find_by(stripe_subscription_id: invoice.subscription)
    
    # 期間更新と、クレジット(生成枚数)のリセット
    subscription.update!(
      current_period_end: Time.at(invoice.lines.data[0].period.end),
      generations_count: 0 # ★ここで月間生成数をリセット
    )
  end
  
  # ... 他のハンドラ
end
routes.rb
post 'stripe_webhooks', to: 'stripe_webhooks#create'

6. カスタマーポータル(解約・プラン変更)

マイページから「プラン変更」や「解約」を行う場合、自前で画面を作らずStripeのCustomer Portalへ飛ばすのが定石です。

PortalController
def create
  session = Stripe::BillingPortal::Session.create({
    customer: current_user.stripe_customer_id,
    return_url: root_url # ポータルから戻るURL
  })
  redirect_to session.url, allow_other_host: true
end

7. ローカルでの動作確認

開発環境(localhost)でWebhookが正しく動くか確認するためには、Stripe CLI というツールを使用します。

Stripe CLIとは?
Stripeのリソース作成やイベント監視などを、ターミナル(コマンドライン)から直接操作できる公式の開発者用ツールです。

Stripe CLIが必要な理由は?
通常、Stripeのサーバー(インターネット上)から、あなたの開発中のPC(ローカル環境/localhost)へ直接アクセスしてデータを送ることはできません。セキュリティやネットワークの仕組み上、外部から見えない状態になっているためです。

Stripe CLIを使うと、Stripeとローカル環境の間にトンネル(直通の通信経路)を作り、Stripe上で発生したイベントをローカルのRailsサーバーへ転送(フォワード)してくれます。これにより、本番環境にデプロイしなくてもWebhookの実装テストが可能になります。

7.1 Stripe CLIのインストール

Homebrewを使用している場合(Macなど)は以下のコマンドでインストールします。

brew install stripe/stripe-cli/stripe

7.2 認証(ログイン)

ターミナルで以下のコマンドを実行し、ブラウザ経由でStripeアカウントとの紐付けを行います。

stripe login

7.3 イベントの転送(Listen)

以下のコマンドを実行すると、Stripeからの通知をローカルのRailsアプ(localhost:3000/stripe_webhooks)へ転送する待機状態になります。

stripe listen --forward-to localhost:3000/stripe_webhooks

7.4 署名シークレットの設定

コマンドを実行すると、ターミナル上に以下のような表示が出ます。

Ready! Your webhook signing secret is whsec_...

この whsec_ から始まる文字列が、ローカルテスト専用の署名シークレットです。 この値を .env ファイルの STRIPE_SIGNING_SECRET に設定してください。

STRIPE_SIGNING_SECRET=whsec_xxxxxxxxxxxxxxxxxxxx

stripe listen コマンドを実行している間だけ、ローカルへの転送が有効になります。開発中はターミナルのタブを開いたままにしておきましょう

8. まとめ

長くなりましたが、これでRailsアプリにStripeのサブスクリプション決済を導入する一連の流れは完了です。

今回の実装における重要なポイントを振り返ります。

8.1 商品はStripe側で管理する

プラン名や金額はStripeダッシュボードで設定し、Rails側は Price ID を渡すだけで決済画面を作ることができます。

8.2 データの同期はWebhookに任せる

決済完了、更新、解約といったステータスの変化は、すべてWebhook経由で受け取り、データベースに反映させるのが鉄則です。これにより、自動更新などの処理も確実に行えます。

8.3 ローカルテストはCLIで

Stripe CLIを活用することで、本番環境にデプロイすることなく、安全かつ効率的にWebhookの挙動を確認できます。

8.4 終わりに

特にWebhookの実装は最初は複雑に感じるかもしれませんが、ここさえ堅実に作っておけば、「ユーザーが解約したのにサービスが使えてしまう」「更新時に課金されていない」といった致命的なトラブルを防ぐことができます。

次のステップとして、決済失敗時(invoice.payment_failed)にユーザーへメールを送る処理や、プランのアップグレード・ダウングレード機能の実装などにもぜひ挑戦してみてください。

この記事が、あなたのSaaS開発の一助になれば幸いです!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?