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

2026.10.07

Pythonでシステムを開発している開発者の方に向けて、PAY.JPの決済APIをPythonから呼び出す具体的な実装方法を解説します。公式ライブラリのインストールから、決済の作成・取得・返金、顧客へのカード保存、エラーハンドリング、Webhookの検証まで、PAY.JPの公式APIドキュメントの複数ページに分かれて書かれている情報を実装する順番で1本にまとめました。

Pythonでの決済実装がPAY.JPにとって適しているかどうかの前提知識はPython決済実装とは?AI活用時の注意点も解説で扱っているため、本記事は実装そのものにフォーカスします。

この記事でわかること

  • 公式PythonライブラリのインストールとAPIキーの設定方法
  • 決済の作成・取得・返金、顧客登録とカードの保存をPythonのコードで実装する手順
  • エラーハンドリング、Webhookの検証、実装時によくあるつまずきポイント

実装前の準備:テストキーとpipでの導入

この記事のコード例で使うsk_test_...というテスト用のシークレットキーは、PAY.JPのアカウント登録とメール認証を済ませた時点で発行されます。本番申請やカード会社の審査は、実際の決済(本番環境)を行う段階で必要になるものであり、テスト環境での実装・動作確認にはこれらの審査は不要です。

なお、PAY.JPには公開鍵(pk_test_...)と秘密鍵(sk_test_...)の2種類の鍵があります。本記事のPythonライブラリ(サーバーサイド)で使うのは秘密鍵で、外部に漏れないよう厳重に管理する必要があります。一方、後述するカード情報のトークン化(ブラウザ側のpayjp.js)で使うのは公開鍵で、こちらは画面に埋め込まれる前提の鍵です。この2つを取り違えないこと(特に秘密鍵をフロントエンドのコードに書かないこと)が、実装のセキュリティ上とても重要です。

PAY.JPの公式ライブラリはpip経由でインストールします。

1
pip install payjp

対応バージョンはPython 3.8以上です(Python 3.8は2024年10月にEOLを迎えているため、実際には保守中のバージョンでの利用を推奨します)。ライブラリ本体はGitHub(payjp/payjp-python)で公開されていますが、型ヒント(py.typed)は同梱されていないため、mypyなどの静的型チェックやIDEの引数補完は効かない点に注意してください。

なお、本記事のコード例は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)を参照してください。

Python向けライブラリの基本

ライブラリを読み込み、発行したテスト用シークレットキーを設定すれば、以降のAPI呼び出しの準備は完了です。

123
import payjp

payjp.api_key = 'sk_test_••••••••••••••••••••••••'

payjp.api_keyに設定したシークレットキーは、以降のすべてのAPI呼び出しで自動的に使われます。本番環境に切り替える際は、この値を本番用のシークレットキー(sk_live_...)に差し替えるだけで済みます。

決済を作成する実装手順

決済処理の基本単位はCharge(支払い)です。Chargeを1件作成するリクエストが、そのまま1回の決済(与信・売上確定)に対応します。

123456789
import payjp

payjp.api_key = 'sk_test_••••••••••••••••••••••••'

charge = payjp.Charge.create(
   amount=3500,
   card='tok_76e202b409f3da51a0706605ac81',
   currency='jpy',
)

cardパラメータには、カード番号そのものではなくトークンIDを渡します。カード番号を自社のPythonサーバーに直接送らせず、決済フォーム上でPAY.JPが提供するJavaScript(payjp.js)を使ってカード情報を直接PAY.JPサーバーへ送信し、返ってきたトークンIDだけを自社サーバーに送る、という流れです(詳細はカード情報のトークン化(docs.pay.jp)を参照)。仕組み全体の解説は決済APIとはでも扱っています。

テスト環境で試す際、トークンのもとになるカード番号は実在の番号ではなく、PAY.JPが指定するテストカード番号(例:4242424242424242)を使う必要があります。

戻り値のchargeオブジェクトにid(例:ch_...から始まる文字列)が含まれていれば決済成功です。パラメータ不備やカードの与信エラーなど、リクエストが失敗した場合は例外が発生します(詳細は後述の「エラーハンドリングの書き方」を参照)。

決済情報の取得・返金の実装方法

作成済みのChargeは、IDを指定して取得できます。

1
charge = payjp.Charge.retrieve('ch_fa990a4c10672a93053a774730b0a')

返金は、取得したchargeオブジェクトに対してrefund()を呼び出すだけで実装できます。

12
charge = payjp.Charge.retrieve('ch_fa990a4c10672a93053a774730b0a')
charge.refund()

金額を指定した一部返金にも対応しており、refund()メソッドに引数として金額を渡します。全額返金したい場合は引数なしで呼び出します。

顧客登録とカードの保存で継続課金に備える

会員登録があるサービスや、次回以降もカード情報を使い回したい場合は、Customer(顧客)を作成し、そこにカードを紐付けます。

123
customer = payjp.Customer.create(
   description='test'
)

作成したCustomerにカードを追加するには、Customerを取得したうえでcards.createを呼び出します。

1234
customer = payjp.Customer.retrieve('cus_4df4b5ed720933f4fb9e28857517')
customer.cards.create(
   card='tok_76e202b409f3da51a0706605ac81'
)

こうしてカードを紐付けたCustomerに対しては、次回以降の決済でpayjp.Charge.createを呼び出す際に、トークンID(card)の代わりにCustomerのID(customer)を渡すだけで、保存済みのカードを使って決済できます。

12345
charge = payjp.Charge.create(
   amount=3500,
   customer='cus_4df4b5ed720933f4fb9e28857517',
   currency='jpy',
)

たとえば会員が2回目以降に買い物をする際、カード番号を毎回入力してもらう必要がなくなります。

ただし、この方法はあくまで「事業者側のプログラムが、必要なタイミングでChargeを1回ずつ呼び出す」方式(自前バッチ処理)です。月額課金のように毎月繰り返し課金したい場合、いつ課金するかのスケジュール管理や、課金に失敗したときのリトライ判断まで、すべて自社側で実装・運用する必要があります。

これに対し、課金のスケジュール管理・実行をPAY.JP側にまかせられる定期課金API(Subscription API)という選択肢もあります。どちらが向いているかは自社の運用体制次第で、実装手順は定期課金APIの実装方法にまとめています。

エラーハンドリングの書き方

エラーハンドリングとは、処理が失敗したときにそれを検知し、原因に応じて適切に対処する実装のことです。決済APIの呼び出しでは、通信エラーやカードの与信エラーなど「起こりうる失敗」があらかじめ分かっているため、それぞれの失敗が実際に起きたときにプログラムがどう振る舞うかをコードに組み込んでおく必要があります。

PAY.JPのPythonライブラリは、失敗の種類に応じて異なる例外クラスをraiseします(raiseは、通常の処理を中断してその例外を呼び出し元に伝える=例外を発生させることを指すPythonの用語です)。決済のように金銭を扱う処理では、この例外クラスごとの分岐が実装の要になります。

1234567891011121314151617181920212223242526
try:
   charge = payjp.Charge.create(
       amount=3500,
       card='tok_76e202b409f3da51a0706605ac81',
       currency='jpy',
   )
except payjp.error.CardError as e:
   # カードの与信エラーなど、決済結果としての失敗
   body = e.json_body
   err = body['error']
   print('Status: %s' % e.http_status)
   print('Type: %s' % err['type'])
   print('Code: %s' % err['code'])
   print('Message: %s' % err['message'])
except payjp.error.InvalidRequestError as e:
   # パラメータ不備やリソース未検出などのリクエストエラー
   pass
except payjp.error.AuthenticationError as e:
   # APIキーの設定間違いなど認証エラー
   pass
except payjp.error.APIConnectionError as e:
   # ネットワークエラー
   pass
except payjp.error.PayjpException as e:
   # 上記以外のPAY.JP起因のエラー全般
   pass

payjp.error.CardErrorは「カードが使えなかった」という決済結果としての失敗を表し、payjp.error.InvalidRequestErrorやpayjp.error.AuthenticationErrorとは性質が異なります。ユーザーに表示するエラーメッセージを出し分ける場合は、この2系統を区別して扱うことが重要です。テスト用のAPIキーそのものが正しく設定されているかを確認したい場合は、まずpayjp.error.AuthenticationErrorが発生していないかを確認してください。

Webhookの検証で決済結果を安全に受け取る

決済の作成をPythonから呼び出すだけでなく、後から発生するイベント(入金確定・定期課金の課金結果など)を受け取るにはWebhook(PAY.JP側でイベントが発生したときに、あらかじめ登録した自社のURLへ自動的に送られてくる通知の仕組み)を使います。仕組み自体の詳細はWebhookとはを参照してください。

PAY.JPのWebhookは、StripeのようなHMAC署名の検証ではなく、X-Payjp-Webhook-Tokenという固定トークンをヘッダーで文字列比較する方式です(詳細はWebhookの検証(docs.pay.jp)を参照)。PAY.JPの管理画面で確認できる自身のトークンと、受信したヘッダーの値が一致するかを、受信処理の最初に必ず確認してください。この検証を省略すると、第三者が自社のWebhook受信URLに偽のリクエストを送りつけるだけで「決済が完了した」というイベントを偽装できてしまいます(無料で注文を「支払い済み」にされる、といった被害につながります)。

123456789
import hmac

# PAY.JPの管理画面で確認した自身のWebhookトークン
WEBHOOK_TOKEN = 'whook_a09d5c1c87be4e1590a9f16516'

def is_valid_payjp_webhook(request_token: str) -> bool:
   # 単純な==比較はタイミング攻撃(比較にかかる時間の差からトークンを推測される攻撃)に弱いため、
   # hmac.compare_digestで一定時間の比較を行う
   return hmac.compare_digest(request_token, WEBHOOK_TOKEN)

エンドポイントを実装する場合、リクエストヘッダーからX-Payjp-Webhook-Tokenを取り出し、上記の関数で検証してから処理を進めます。検証に失敗した場合は403を返してその場で処理を打ち切ってください。Flaskの場合は次のようになります。

123456789
from flask import request, abort

@app.route('/webhook', methods=['POST'])
def webhook():
   token = request.headers.get('X-Payjp-Webhook-Token', '')
   if not is_valid_payjp_webhook(token):
       abort(403)
   # ここから決済結果の処理
   return '', 200

FastAPIの場合、request・abortはFlask固有のものなのでそのままでは使えません。RequestとHTTPExceptionを使って書き換えます。

123456789
from fastapi import Request, HTTPException

@app.post('/webhook')
async def webhook(request: Request):
   token = request.headers.get('X-Payjp-Webhook-Token', '')
   if not is_valid_payjp_webhook(token):
       raise HTTPException(status_code=403)
   # ここから決済結果の処理
   return {}

is_valid_payjp_webhook自体はhmacモジュールだけで完結する軽い処理なので、async defのエンドポイント内でそのまま呼び出しても問題ありません(前述の「非同期フレームワークでイベントループをブロックする」問題が起きるのはpayjp.Charge.createのようなAPI通信を伴う呼び出しに限られます)。

なお、PAY.JPは配信に失敗した通知を3分間隔で最大3回まで自動的に再送するため、同じイベントを重複して処理しないよう、受信したイベントIDを記録しておく実装も合わせて必要です。

複数アカウントを扱う場合のAPIキー管理

ここまでのコード例はpayjp.api_keyにシークレットキーを1回だけ設定する前提で書いていますが、Charge.create・Charge.retrieveなどの各メソッドは、api_keyをキーワード引数として呼び出しごとに個別に渡すこともできます。複数のPAY.JPアカウントを切り替えて使うプラットフォーム型のサービスなど、リクエストごとに異なるシークレットキーを使いたい場合は、モジュール全体のpayjp.api_keyを書き換えるのではなく、呼び出しごとにapi_keyを渡す方法のほうが、他のリクエストへの影響がなく安全です。

123456
charge = payjp.Charge.create(
   amount=3500,
   card='tok_76e202b409f3da51a0706605ac81',
   currency='jpy',
   api_key='sk_test_••••••••••••••••••••••••',
)

単一アカウントで運用する一般的なWebサービスであれば、この記事の他のコード例のとおりpayjp.api_keyを最初に1回設定するだけで十分です。

実装時によくあるつまずきポイント

PythonでPAY.JPの決済APIを実装する際、次のような点でつまずきやすいので注意してください。

  • Webhookのトークン検証を省略してしまう:X-Payjp-Webhook-Tokenヘッダーの照合をせずに受信データをそのまま信用してしまうと、第三者に決済完了イベントを偽装される。比較には単純な==ではなくhmac.compare_digestを使う
  • FastAPI等の非同期フレームワークでイベントループをブロックする:payjp-pythonはrequestsをベースにした同期専用のライブラリで、async/awaitには対応していません。async defのエンドポイント内でそのままpayjp.Charge.createを呼び出すと、レスポンスが返るまでの間サーバー全体の処理が止まってしまいます。非同期フレームワークで使う場合は、run_in_executorなどでスレッドプールに処理を逃がす必要があります
  • 例外クラスをpayjp.error.PayjpExceptionだけでまとめてexceptしてしまう:payjp.error.CardError(決済結果としての失敗)と、payjp.error.InvalidRequestError・payjp.error.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のPython向け公式ライブラリは、決済の作成・取得・返金・顧客へのカード保存までを数行のコードで実装でき、APIドキュメント(Pythonタブ)にも各処理のコード例が一通り揃っています。決済手段はクレジットカード・Apple Pay(Web/アプリ)に対応しています。

料金プランはスタンダード(月額費用0円・決済手数料3.3%)・ビジネス(月額費用20,000円・決済手数料2.78%)・エンタープライズ(月額費用50,000円・Visa/Mastercard決済手数料2.59%、その他ブランド2.7%)の3つが用意されており、事業規模に応じて事業者自身がPAY.JPの料金プランから申請することでプランを選択できます。まずはテスト用のAPIキーを発行し、本記事のコード例を実際に動かしながら実装を進めてみてください。

まとめ

  • PAY.JPのPython向け公式ライブラリはpip install payjpで導入でき、payjp.api_keyにシークレットキーを設定するだけで利用を開始できる
  • 決済の作成はpayjp.Charge.create、取得はpayjp.Charge.retrieve、返金は取得したChargeへのrefund()呼び出しで実装できる
  • 継続利用したいカードはpayjp.Customerを作成しcards.createで紐付ける。毎月の自動課金には定期課金API(Subscription API)という選択肢もある
  • エラー処理はpayjp.error.CardError(決済結果としての失敗)とpayjp.error.InvalidRequestError等(実装側の不備)を区別してexceptすることが重要
  • payjp-pythonは同期専用ライブラリのため、FastAPI等の非同期フレームワークではスレッドプールに逃がす必要がある
  • WebhookはX-Payjp-Webhook-Tokenヘッダーをhmac.compare_digestで検証し、イベント偽装を防ぐ
  • APIキーの本番差し替えを忘れない、Webhookの受信処理を後回しにしないなど、実装時に見落としやすい点に注意する

執筆者

PAY.JP編集部

PAY.JPを運営するPAY株式会社の編集部。決済・Fintech・SaaSビジネスに関する情報を、事業者・開発者向けにわかりやすく発信しています。