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

Node.jsでシステムを開発している開発者の方に向けて、PAY.JPの決済APIをNode.jsから呼び出す具体的な実装方法を解説します。公式ライブラリのインストールから、決済の作成・取得・返金、顧客へのカード保存、エラーハンドリング、Webhookの検証まで、PAY.JPの公式APIドキュメントの複数ページに分かれて書かれている情報を実装する順番で1本にまとめました。
Node.jsでの決済実装がPAY.JPにとって適しているかどうかの前提知識はNode.js決済実装とは?実装を始める前に知っておきたい基本で扱っているため、本記事は実装そのものにフォーカスします。
この記事でわかること
- 公式Node.jsライブラリのインストールとAPIキーの設定方法
- 決済の作成・取得・返金をNode.jsのコードで実装する手順
- 顧客へのカード保存、エラーハンドリング、Webhookの検証、実装時によくあるつまずきポイント
実装前の準備:テストキーとnpmでの導入
この記事のコード例で使うsk_test_...というテスト用のシークレットキーは、PAY.JPのアカウント登録とメール認証を済ませた時点で発行されます。本番申請やカード会社の審査は、実際の決済(本番環境)を行う段階で必要になるものであり、テスト環境での実装・動作確認にはこれらの審査は不要です。
なお、PAY.JPには公開鍵(pk_test_...)と秘密鍵(sk_test_...)の2種類の鍵があります。本記事のNode.jsライブラリ(サーバーサイド)で使うのは秘密鍵で、外部に漏れないよう厳重に管理する必要があります。一方、後述するカード情報のトークン化(ブラウザ側のpayjp.js)で使うのは公開鍵で、こちらは画面に埋め込まれる前提の鍵です。この2つを取り違えないこと(特に秘密鍵をフロントエンドのコードに書かないこと)が、実装のセキュリティ上とても重要です。
PAY.JPの公式ライブラリはnpm経由でインストールします。
npm install --save payjp対応Node.jsバージョンは22以上です。ライブラリはJavaScriptだけでなくTypeScriptにも対応しており、本体はGitHub(payjp/payjp-node)で公開されています。
なお、本記事のコード例は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)を参照してください。
なお、この記事はNode.js(サーバーサイド)での実装が対象です。PythonやRubyなど他言語での実装はPython 決済実装・Ruby決済APIの実装方法を、ブラウザ側でカード情報をトークン化するクライアントサイドJavaScriptの実装はJavaScript 決済実装を参照してください。
Node.js向けライブラリの基本
ライブラリを読み込み、発行したテスト用シークレットキーを渡せば、以降のAPI呼び出しの準備は完了です。
const payjp = require('payjp')('sk_test_••••••••••••••••••••••••');TypeScriptの場合は次のように記述します。
import * as Payjp from 'payjp';
const payjp = Payjp('sk_test_••••••••••••••••••••••••');require(またはimport)時に渡したシークレットキーは、以降のすべてのAPI呼び出しで自動的に使われます。本番環境に切り替える際は、この値を本番用のシークレットキー(sk_live_...)に差し替えるだけで済みます。各APIメソッドはPromiseを返すため、then/catchまたはasync/awaitで処理を書けます。以降のコード例では紙面をシンプルにするためcatchを省略している箇所がありますが、実装時は後述の「エラーハンドリングの書き方」のとおり、必ずcatch(またはtry/catchでの例外処理)を追加してください。
決済を作成する実装手順
決済処理の基本単位はCharge(支払い)です。Chargeを1件作成するリクエストが、そのまま1回の決済(与信・売上確定)に対応します。
payjp.charges.create({
card: 'tok_76e202b409f3da51a0706605ac81',
amount: 3500,
currency: 'jpy',
}).then(function(charge) {
console.log(charge);
}).catch(function(err) {
console.error(err);
});cardパラメータには、カード番号そのものではなくトークンIDを渡します。カード番号を自社のNode.jsサーバーに直接送らせず、決済フォーム上でPAY.JPが提供するJavaScript(payjp.js)を使ってカード情報を直接PAY.JPサーバーへ送信し、返ってきたトークンIDだけを自社サーバーに送る、という流れです(詳細はカード情報のトークン化(docs.pay.jp)を参照)。このブラウザ側のトークン発行で使うのが、前述した公開鍵(pk_test_...)です。仕組み全体の解説は決済APIとはでも扱っています。
テスト環境で試す際、トークンのもとになるカード番号は実在の番号ではなく、PAY.JPが指定するテストカード番号(例:4242424242424242)を使う必要があります。
Promiseが解決(resolve)され、戻り値のchargeオブジェクトにid(例:ch_...から始まる文字列)が含まれていれば決済成功です。パラメータ不備やカードの与信エラーなど、リクエストが失敗した場合はPromiseがreject(拒否)されます(詳細は後述の「エラーハンドリングの書き方」を参照)。
決済情報の取得・返金の実装方法
作成済みのChargeは、IDを指定して取得できます。
payjp.charges.retrieve('ch_fa990a4c10672a93053a774730b0a');返金はcharges.refundにChargeのIDを渡すことで実装できます。
payjp.charges.refund('ch_fa990a4c10672a93053a774730b0a', {amount: 1});金額を指定した一部返金にも対応しており、上記のようにamountを渡します。全額返金したい場合はamountを省略して呼び出します。
顧客登録とカードの保存で継続課金に備える
会員登録があるサービスや、次回以降もカード情報を使い回したい場合は、Customer(顧客)を作成し、そこにカードを紐付けます。
payjp.customers.create({
description: 'test'
}); 作成したCustomerにカードを追加するには、CustomerのIDを指定してcustomers.cards.createを呼び出します。
payjp.customers.cards.create('cus_121673955bd7aa144de5a8f6c262', {
card: 'tok_76e202b409f3da51a0706605ac81',
default: true,
}); default: trueを指定すると、そのカードが顧客のデフォルトカードとして設定されます。こうしてカードを紐付けたCustomerに対しては、次回以降の決済でpayjp.charges.createを呼び出す際に、トークンID(card)の代わりにCustomerのID(customer)を渡すだけで、保存済みのカードを使って決済できます。
payjp.charges.create({
amount: 3500,
customer: 'cus_121673955bd7aa144de5a8f6c262',
currency: 'jpy',
});たとえば会員が2回目以降に買い物をする際、カード番号を毎回入力してもらう必要がなくなります。
ただし、この方法はあくまで「事業者側のプログラムが、必要なタイミングでChargeを1回ずつ呼び出す」方式(自前バッチ処理)です。月額課金のように毎月繰り返し課金したい場合、いつ課金するかのスケジュール管理や、課金に失敗したときのリトライ判断まで、すべて自社側で実装・運用する必要があります。
これに対し、課金のスケジュール管理・実行をPAY.JP側にまかせられる定期課金API(Subscription API)という選択肢もあります。どちらが向いているかは自社の運用体制次第で、実装手順は定期課金APIの実装方法にまとめています。
エラーハンドリングの書き方
エラーハンドリングとは、処理が失敗したときにそれを検知し、原因に応じて適切に対処する実装のことです。決済APIの呼び出しでは、通信エラーやカードの与信エラーなど「起こりうる失敗」があらかじめ分かっているため、それぞれの失敗が実際に起きたときにプログラムがどう振る舞うかをコードに組み込んでおく必要があります。
PAY.JPのNode.jsライブラリは、Ruby決済APIの実装方法で解説しているpayjp-rubyのような例外クラス(Payjp::CardError等)の区別を持たず、Promiseのreject(拒否)でエラーを表現します。失敗の種類ごとにrescueする型を変えられるRubyとは異なり、Node.jsでは常に同じ形のエラーオブジェクトが返ってくるため、catch側で中身を見て種類を判定する必要があります。決済のように金銭を扱う処理では、このcatch側でのエラー情報の分岐が実装の要になります。
payjp.customers.create().catch(function(err) {
console.log(err.status); // HTTPステータスコード(例: 401)
console.log(err.response.body); // エラーの詳細(例: { error: { message, status, type } })
console.log(err.message); // エラーメッセージ
});err.responseが存在する場合は、err.response.body.errorにmessage(メッセージ)・status(ステータスコード)・type(エラー種別、例:認証エラーはauth_error)が入っています。一方、ネットワーク自体が失敗した場合はerr.responseが存在せず、err.messageだけで接続エラーの内容を判断します。ユーザーに表示するエラーメッセージを出し分ける場合は、err.status(4xxか5xxか、402かどうか等)で決済結果としての失敗かどうかをまず判定し、err.response.body.error.typeでさらに細かく分岐する、という流れが基本です。テスト用のAPIキーそのものが正しく設定されているかを確認したい場合は、まずerr.statusが401(認証エラー)になっていないかを確認してください。
Webhookの検証で決済結果を安全に受け取る
決済の作成をNode.jsから呼び出すだけでなく、後から発生するイベント(入金確定・定期課金の課金結果など)を受け取るにはWebhook(PAY.JP側でイベントが発生したときに、あらかじめ登録した自社のURLへ自動的に送られてくる通知の仕組み)を使います。仕組み自体の詳細はWebhookとはを参照してください。
PAY.JPのWebhookは、StripeのようなHMAC署名の検証ではなく、X-Payjp-Webhook-Tokenという固定トークンをヘッダーで文字列比較する方式です(詳細はWebhookの検証(docs.pay.jp)を参照)。PAY.JPの管理画面で確認できる自身のトークンと、受信したヘッダーの値が一致するかを、受信処理の最初に必ず確認してください。この検証を省略すると、第三者が自社のWebhook受信URLに偽のリクエストを送りつけるだけで「決済が完了した」というイベントを偽装できてしまいます(無料で注文を「支払い済み」にされる、といった被害につながります)。
const crypto = require('crypto');
// PAY.JPの管理画面で確認した自身のWebhookトークン
const WEBHOOK_TOKEN = 'whook_a09d5c1c87be4e1590a9f16516';
function isValidPayjpWebhook(requestToken) {
const received = Buffer.from(requestToken || '');
const expected = Buffer.from(WEBHOOK_TOKEN);
// 長さが異なるとtimingSafeEqualが例外を投げるため、先に長さを確認する
if (received.length !== expected.length) return false;
// 単純な===比較はタイミング攻撃(比較にかかる時間の差からトークンを推測される攻撃)に弱いため、
// crypto.timingSafeEqualで一定時間の比較を行う
return crypto.timingSafeEqual(received, expected);
}Expressなどでエンドポイントを実装する場合、リクエストヘッダーからx-payjp-webhook-tokenを取り出し、上記の関数で検証してから処理を進めます。検証に失敗した場合は403を返してその場で処理を打ち切ってください。
app.post('/webhook', (req, res) => {
const token = req.headers['x-payjp-webhook-token'] || '';
if (!isValidPayjpWebhook(token)) {
return res.sendStatus(403);
}
// ここから決済結果の処理
});なお、PAY.JPは配信に失敗した通知を3分間隔で最大3回まで自動的に再送するため、同じイベントを重複して処理しないよう、受信したイベントIDを記録しておく実装も合わせて必要です。
実装時によくあるつまずきポイント
Node.jsでPAY.JPの決済APIを実装する際、次のような点でつまずきやすいので注意してください。
- Webhookのトークン検証を省略してしまう:
X-Payjp-Webhook-Tokenヘッダーの照合をせずに受信データをそのまま信用してしまうと、第三者に決済完了イベントを偽装される。比較には単純な===ではなくcrypto.timingSafeEqualを使う(長さの違うバッファを渡すと例外になるため、事前の長さチェックも必要) - Promiseのcatchを書き忘れる:
payjp.charges.create(...)の結果をthenだけで受け取りcatchを実装しないと、決済失敗時に未処理のPromise rejectionとしてエラーが握りつぶされ、ユーザーには決済が成功したように見えてしまう場合がある - 通信タイムアウト時に、同じ決済リクエストを再送して二重決済を招く:API v1には冪等性(べきとうせい。同じリクエストを何度送っても結果が1回分と同じになる性質)キー(Idempotency-Key)の仕組みがなく、v2のように「同じリクエストを再送しても1回分しか処理されない」保証がない。タイムアウト時は直前の決済が成立していないか確認してから再送するなど、重複防止をアプリ側で実装する必要がある
- 決済が失敗したカードに、成功するまでリトライし続けてしまう:PAY.JPは実装についての禁止事項(help.pay.jp)で、失敗したカードへの「成功するまでのリトライ」を明確に禁止している。違反するとカード会社から指摘を受け、最悪の場合クレジットカード決済自体が停止される
- 本番反映時にAPIキーの差し替えを忘れる:初期化時に渡すシークレットキーをテスト用(
sk_test_...)のまま本番環境にデプロイし、決済が実行されない、あるいは意図せずテストデータのまま運用してしまう - Webhookの受信処理を後回しにする:カード会社側の遅延や障害時の決済結果は、Chargeの作成レスポンスだけでは分からない場合がある。
PAY.JPの料金プランとはじめ方
PAY.JPのNode.js向け公式ライブラリは、決済の作成・取得・返金・顧客へのカード保存までを数行のコードで実装でき、APIドキュメント(Node.jsタブ)にも各処理のコード例が一通り揃っています。JavaScriptだけでなくTypeScriptの型定義も提供されているため、型安全にエラーハンドリングを書きたいプロジェクトにも向いています。決済手段はクレジットカード・Apple Pay(Web/アプリ)に対応しています。
料金プランはスタンダード(月額費用0円・決済手数料3.3%)・ビジネス(月額費用20,000円・決済手数料2.78%)・エンタープライズ(月額費用50,000円・Visa/Mastercard決済手数料2.59%、その他ブランド2.7%)の3つが用意されており、事業規模に応じて事業者自身がPAY.JPの料金プランから申請することでプランを選択できます。まずはテスト用のAPIキーを発行し、本記事のコード例を実際に動かしながら実装を進めてみてください。
まとめ
- PAY.JPのNode.js向け公式ライブラリは
npm install --save payjpで導入でき、require('payjp')(秘密鍵)で初期化する。公開鍵と秘密鍵、API v1とv2を混同しないよう注意する - 決済の作成は
charges.create、取得はcharges.retrieve、返金はcharges.refundで実装できる。すべてPromiseを返すためthen/catchまたはasync/awaitで扱う - 継続利用したいカードは
customers.createでCustomerを作成しcustomers.cards.createで紐付ける。毎月の自動課金には定期課金API(Subscription API)という選択肢もある - エラーはPromiseのrejectで表現され、
err.statusとerr.response.body.error.typeを組み合わせて、決済結果としての失敗か実装側の不備かを判定する - Webhookは
X-Payjp-Webhook-Tokenヘッダーをcrypto.timingSafeEqualで検証し、イベント偽装を防ぐ - v1に冪等性キーがなく再送で二重決済を招きやすい点、失敗カードへの成功するまでのリトライが禁止されている点など、見落としやすい注意点を踏まえて実装する