【コード例あり】Ruby決済APIの実装|PAY.JPで決済作成・取得・返金
2026.10.07

Ruby(またはRuby on Rails)でシステムを開発している開発者の方に向けて、PAY.JPの決済APIをRubyから呼び出す具体的な実装方法を解説します。公式Rubyライブラリのインストールから、決済の作成・取得・返金、顧客へのカード保存、エラーハンドリング、Webhookの検証まで、PAY.JPの公式APIドキュメントの複数ページに分かれて書かれている情報を実装する順番で1本にまとめました。
Rubyでの決済実装がPAY.JPにとって適しているかどうかの前提知識はRuby決済実装とは?実装を始める前に知っておきたい基本で扱っているため、本記事は実装そのものにフォーカスします。
この記事でわかること
- 公式RubyライブラリのインストールとAPIキーの設定方法
- 決済の作成・取得・返金をRubyのコードで実装する手順
- 顧客へのカード保存、エラーハンドリング、Webhookの検証、実装時によくあるつまずきポイント
実装前の準備:テストキーとgemの導入
この記事のコード例で使うsk_test_...というテスト用のシークレットキーは、PAY.JPのアカウント登録とメール認証を済ませた時点で発行されます。本番申請やカード会社の審査は、実際の決済(本番環境)を行う段階で必要になるものであり、テスト環境での実装・動作確認にはこれらの審査は不要です。
なお、PAY.JPには公開鍵(pk_test_...)と秘密鍵(sk_test_...)の2種類の鍵があります。本記事のRubyライブラリ(サーバーサイド)で使うのは秘密鍵で、外部に漏れないよう厳重に管理する必要があります。一方、後述するカード情報のトークン化(ブラウザ側のpayjp.js)で使うのは公開鍵で、こちらは画面に埋め込まれる前提の鍵です。この2つを取り違えないこと(特に秘密鍵をフロントエンドのコードに書かないこと)が、実装のセキュリティ上とても重要です。
PAY.JPの公式RubyライブラリはRubyGems経由でインストールします。
gem install payjpBundler(Rubyプロジェクトの依存gemをまとめて管理するツール)を使っている場合は、Gemfileに追加してbundle installします。
source 'https://rubygems.org'
gem 'payjp'対応バージョンはRuby 2.0.0以上です。ライブラリ本体はGitHub(payjp/payjp-ruby)で公開されています。
なお、本記事のコード例はAPI v1を前提としています。v2ではCharge(支払い)に相当する処理が「Payment Flow」という別のAPIオブジェクトに置き換わっており、エラーレスポンスの形式も異なるため、本記事のコード例はv2ではそのまま使えません。PAY.JPにはv1とv2の2つのAPIバージョンがあり、恒久的な併用は想定されていません。
判断の目安として、定期課金(サブスクリプション)やモバイルアプリ向けSDK(iOS/Android/Flutter等)を使いたい場合はv1(v2も定期課金への対応を開発中)、PayPay決済への対応や自動3Dセキュアなど新しい機能を使いたい新規事業者の場合はv2が適しています。詳しくはAPI v1からv2への移行ガイド(docs.pay.jp)・API v2のご利用について(help.pay.jp)を参照してください。
Ruby向けライブラリの基本
ライブラリを読み込み、発行したテスト用シークレットキーを設定すれば、以降のAPI呼び出しの準備は完了です。
require 'payjp'
Payjp.api_key = 'sk_test_••••••••••••••••••••••••'
Payjp.open_timeout = 30 # 接続タイムアウト(秒、任意)
Payjp.read_timeout = 90 # 読み込みタイムアウト(秒、任意)Payjp.api_keyに設定したシークレットキーは、以降のすべてのAPI呼び出しで自動的に使われます。本番環境に切り替える際は、この値を本番用のシークレットキー(sk_live_...)に差し替えるだけで済みます。
決済を作成する実装手順
決済処理の基本単位はCharge(支払い)です。Chargeを1件作成するリクエストが、そのまま1回の決済(与信・売上確定)に対応します。
require 'payjp'
Payjp.api_key = 'sk_test_••••••••••••••••••••••••'
charge = Payjp::Charge.create(
amount: 3500,
card: 'tok_76e202b409f3da51a0706605ac81',
currency: 'jpy'
)cardパラメータには、カード番号そのものではなくトークンIDを渡します。カード番号を自社のRubyサーバーに直接送らせず、決済フォーム上でPAY.JPが提供するJavaScript(payjp.js)を使ってカード情報を直接PAY.JPサーバーへ送信し、返ってきたトークンIDだけを自社サーバーに送る、という流れです(詳細はカード情報のトークン化(docs.pay.jp)を参照)。仕組み全体の解説は決済APIとはでも扱っています。
テスト環境で試す際、トークンのもとになるカード番号は実在の番号ではなく、PAY.JPが指定するテストカード番号(例:4242424242424242)を使う必要があります。
戻り値のchargeオブジェクトにid(例:ch_...から始まる文字列)が含まれていれば決済成功です。パラメータ不備やカードの与信エラーなど、リクエストが失敗した場合は例外が発生します(詳細は後述の「エラーハンドリングの書き方」を参照)。
決済情報の取得・返金の実装方法
作成済みのChargeは、IDを指定して取得できます。
require 'payjp'
Payjp.api_key = 'sk_test_••••••••••••••••••••••••'
charge = Payjp::Charge.retrieve('ch_fa990a4c10672a93053a774730b0a')返金は、取得したchargeオブジェクトに対してrefundを呼び出すだけで実装できます。
charge = Payjp::Charge.retrieve('ch_fa990a4c10672a93053a774730b0a')
charge.refund金額を指定した一部返金にも対応しており、refundメソッドにamountを渡します。全額返金したい場合は引数なしで呼び出します。仕様の詳細は支払いを返金する(docs.pay.jp)を参照してください。
顧客登録とカードの保存で継続課金に備える
会員登録があるサービスや、次回以降もカード情報を使い回したい場合は、Customer(顧客)を作成し、そこにカードを紐付けます。
require 'payjp'
Payjp.api_key = 'sk_test_••••••••••••••••••••••••'
customer = Payjp::Customer.create(
description: 'test'
)作成したCustomerにカードを追加するには、Customerを取得したうえでcards.createを呼び出します。
customer = Payjp::Customer.retrieve('cus_4df4b5ed720933f4fb9e28857517')
customer.cards.create(
card: 'tok_76e202b409f3da51a0706605ac81'
)こうしてカードを紐付けたCustomerに対しては、次回以降の決済でPayjp::Charge.createを呼び出す際に、トークンID(card)の代わりにCustomerのID(customer)を渡すだけで、保存済みのカードを使って決済できます。
charge = Payjp::Charge.create(
amount: 3500,
customer: 'cus_4df4b5ed720933f4fb9e28857517',
currency: 'jpy'
)たとえば会員が2回目以降に買い物をする際、カード番号を毎回入力してもらう必要がなくなります。
ただし、この方法はあくまで「事業者側のプログラムが、必要なタイミングでChargeを1回ずつ呼び出す」方式(自前バッチ処理)です。月額課金のように毎月繰り返し課金したい場合、いつ課金するかのスケジュール管理や、課金に失敗したときのリトライ判断まで、すべて自社側で実装・運用する必要があります。
これに対し、課金のスケジュール管理・実行をPAY.JP側にまかせられる定期課金API(Subscription API)という選択肢もあります。どちらが向いているかは自社の運用体制次第で、実装手順は定期課金APIの実装方法にまとめています。
エラーハンドリングの書き方
エラーハンドリングとは、処理が失敗したときにそれを検知し、原因に応じて適切に対処する実装のことです。決済APIの呼び出しでは、通信エラーやカードの与信エラーなど「起こりうる失敗」があらかじめ分かっているため、それぞれの失敗が実際に起きたときにプログラムがどう振る舞うかをコードに組み込んでおく必要があります。
PAY.JPのRubyライブラリは、失敗の種類に応じて異なる例外クラスをraiseします(raiseは、通常の処理を中断してその例外を呼び出し元に伝える=例外を発生させることを指すRubyの用語です)。決済のように金銭を扱う処理では、この例外クラスごとの分岐が実装の要になります。
begin
charge = Payjp::Charge.create(
amount: 3500,
card: 'tok_76e202b409f3da51a0706605ac81',
currency: 'jpy'
)
rescue Payjp::CardError => e
# カードの与信エラーなど、決済結果としての失敗
body = e.json_body
err = body[:error]
puts "Status: #{e.http_status}"
puts "Type: #{err[:type]}"
puts "Code: #{err[:code]}"
puts "Message: #{err[:message]}"
rescue Payjp::InvalidRequestError => e
# パラメータ不備などのリクエストエラー
rescue Payjp::AuthenticationError => e
# APIキーの設定間違いなど認証エラー
rescue Payjp::APIConnectionError => e
# ネットワークエラー
rescue Payjp::PayjpError => e
# 上記以外のPAY.JP起因のエラー全般
endPayjp::CardErrorは「カードが使えなかった」という決済結果としての失敗を表し、Payjp::InvalidRequestErrorやPayjp::AuthenticationErrorとは性質が異なります。ユーザーに表示するエラーメッセージを出し分ける場合は、この2系統を区別して扱うことが重要です。テスト用のAPIキーそのものが正しく設定されているかを確認したい場合は、まずPayjp::AuthenticationErrorが発生していないかを確認してください。
Webhookの検証で決済結果を安全に受け取る
決済の作成をRubyから呼び出すだけでなく、後から発生するイベント(入金確定・定期課金の課金結果など)を受け取るにはWebhook(PAY.JP側でイベントが発生したときに、あらかじめ登録した自社のURLへ自動的に送られてくる通知の仕組み)を使います。仕組み自体の詳細はWebhookとはを参照してください。
PAY.JPのWebhookは、StripeのようなHMAC署名の検証ではなく、X-Payjp-Webhook-Tokenという固定トークンをヘッダーで文字列比較する方式です(詳細はWebhookの検証(docs.pay.jp)を参照)。PAY.JPの管理画面で確認できる自身のトークンと、受信したヘッダーの値が一致するかを、受信処理の最初に必ず確認してください。この検証を省略すると、第三者が自社のWebhook受信URLに偽のリクエストを送りつけるだけで「決済が完了した」というイベントを偽装できてしまいます(無料で注文を「支払い済み」にされる、といった被害につながります)。
require 'openssl'
# PAY.JPの管理画面で確認した自身のWebhookトークン
WEBHOOK_TOKEN = 'whook_a09d5c1c87be4e1590a9f16516'
def valid_payjp_webhook?(request_token)
# 単純な==比較はタイミング攻撃(比較にかかる時間の差からトークンを推測される攻撃)に弱いため、
# OpenSSL.secure_compareで一定時間の比較を行う
OpenSSL.secure_compare(request_token, WEBHOOK_TOKEN)
endRails等でエンドポイントを実装する場合、リクエストヘッダーからX-Payjp-Webhook-Tokenを取り出し、上記のメソッドで検証してから処理を進めます。検証に失敗した場合は403を返してその場で処理を打ち切ってください。
token = request.headers['X-Payjp-Webhook-Token'].to_s
head :forbidden and return unless valid_payjp_webhook?(token)なお、PAY.JPは配信に失敗した通知を3分間隔で最大3回まで自動的に再送するため、同じイベントを重複して処理しないよう、受信したイベントIDを記録しておく実装も合わせて必要です。
実装時によくあるつまずきポイント
Ruby(Ruby on Rails)でPAY.JPの決済APIを実装する際、次のような点でつまずきやすいので注意してください。
- Webhookのトークン検証を省略してしまう:
X-Payjp-Webhook-Tokenヘッダーの照合をせずに受信データをそのまま信用してしまうと、第三者に決済完了イベントを偽装される。比較には単純な==ではなくOpenSSL.secure_compareを使う - 例外クラスを
Payjp::PayjpErrorだけでまとめてrescueしてしまう:Payjp::CardError(決済結果としての失敗)と、Payjp::InvalidRequestError・Payjp::AuthenticationError(実装側の不備)を区別しないと、本来直すべき実装ミスをユーザー向けのエラーメッセージとしてそのまま表示してしまう - 通信タイムアウト時に、同じ決済リクエストを再送して二重決済を招く:API v1には冪等性(べきとうせい。同じリクエストを何度送っても結果が1回分と同じになる性質)キー(Idempotency-Key)の仕組みがなく、v2のように「同じリクエストを再送しても1回分しか処理されない」保証がない。タイムアウト時は直前の決済が成立していないか確認してから再送するなど、重複防止をアプリ側で実装する必要がある
- 決済が失敗したカードに、成功するまでリトライし続けてしまう:PAY.JPは実装についての禁止事項(help.pay.jp)で、失敗したカードへの「成功するまでのリトライ」を明確に禁止している。違反するとカード会社から指摘を受け、最悪の場合クレジットカード決済自体が停止される
- 本番反映時にAPIキーの差し替えを忘れる:
Payjp.api_keyをテスト用(sk_test_...)のまま本番環境にデプロイし、決済が実行されない、あるいは意図せずテストデータのまま運用してしまう - Webhookの受信処理を後回しにする:カード会社側の遅延や障害時の決済結果は、Chargeの作成レスポンスだけでは分からない場合がある。
PAY.JPの料金プランとはじめ方
PAY.JPのRuby向け公式ライブラリは、決済の作成・取得・返金・顧客へのカード保存までを数行のコードで実装でき、APIドキュメント(Rubyタブ)にも各処理のコード例が一通り揃っています。決済手段はクレジットカード・Apple Pay(Web/アプリ)に対応しています。
料金プランはスタンダード(月額費用0円・決済手数料3.3%)・ビジネス(月額費用20,000円・決済手数料2.78%)・エンタープライズ(月額費用50,000円・Visa/Mastercard決済手数料2.59%、その他ブランド2.7%)の3つが用意されており、事業規模に応じて事業者自身がPAY.JPの料金プランから申請することでプランを選択できます。まずはテスト用のAPIキーを発行し、本記事のコード例を実際に動かしながら実装を進めてみてください。
まとめ
- PAY.JPのRuby向け公式ライブラリは
gem install payjpで導入でき、Payjp.api_keyにシークレットキーを設定するだけで利用を開始できる - 決済の作成は
Payjp::Charge.create、取得はPayjp::Charge.retrieve、返金は取得したChargeへのrefund呼び出しで実装できる - 継続利用したいカードは
Payjp::Customerを作成しcards.createで紐付ける。毎月の自動課金には定期課金API(Subscription API)という選択肢もある - エラー処理は
Payjp::CardError(決済結果としての失敗)とPayjp::InvalidRequestError等(実装側の不備)を区別してrescueすることが重要 - Webhookは
X-Payjp-Webhook-TokenヘッダーをOpenSSL.secure_compareで検証し、イベント偽装を防ぐ - APIキーの本番差し替えを忘れない、Webhookの受信処理を後回しにしないなど、実装時に見落としやすい点に注意する