PAY.JP
PAY.JP

イベントと Webhook

イベントと Webhook の検証方法についての説明

イベントとは

イベント(Event)は、PAY.JP のリソース(決済、顧客、返金など)の状態が変化した際に、システムが自動的に作成します。

たとえば、以下のような場面でイベントが作成されます

場面イベント
顧客が決済を完了したpayment_flow.succeeded
新しいクレジットカードが登録されたpayment_method.attached
返金が処理されたrefund.created
Checkout Session が完了したcheckout.session.completed

イベントは以下の3つの目的で使用されます

  1. Webhook を通じて、加盟店のシステムに状態変化を即座に通知するリアルタイム通知
  2. すべての取引の完全な履歴を保持するログ
  3. PAY.JP と外部システムを連携させる基盤としての統合連携

いつイベントを使うか

1. 決済完了の通知

シナリオ: 顧客が商品を購入し、決済が完了したときに注文を確定したい

app.post('/webhook', (req, res) => {
  const event = req.body;

  if (event.type === 'payment_flow.succeeded') {
    const paymentFlow = event.data;

    // 注文を確定する
    fulfillOrder(paymentFlow.metadata.order_id);

    // 顧客にメールを送信
    sendConfirmationEmail(paymentFlow.customer);
  }

  res.json({ received: true });
});

受信するイベント

  • payment_flow.succeeded - 決済成功
  • payment_flow.payment_failed - 決済失敗
  • payment_flow.requires_action - 3Dセキュア認証が必要

2. 決済失敗時のリカバリー

シナリオ: 決済が失敗したときに顧客に通知し、再試行を促したい

if (event.type === 'payment_flow.payment_failed') {
  const paymentFlow = event.data;

  // 顧客に決済失敗を通知
  notifyPaymentFailure(
    paymentFlow.customer,
    paymentFlow.last_payment_error.message
  );

  // 管理画面で失敗理由を記録
  logPaymentFailure(paymentFlow);
}

3. 顧客情報の同期

シナリオ: PAY.JP の顧客情報と自社データベースを同期したい

switch (event.type) {
  case 'customer.created':
    createCustomerInDatabase(event.data);
    break;
  case 'customer.updated':
    updateCustomerInDatabase(event.data);
    break;
  case 'customer.deleted':
    deleteCustomerInDatabase(event.data.id);
    break;
}

4. 返金処理の追跡

シナリオ: 返金が処理されたときに在庫を戻したい

if (event.type === 'refund.created') {
  const refund = event.data;

  // 在庫を戻す
  restoreInventory(refund.payment_flow);

  // 顧客に返金通知メールを送信
  sendRefundNotification(refund);
}

Webhook の検証

PAY.JP からのすべての Webhook には X-Payjp-Webhook-Token がヘッダーに含まれています。この値はアカウント固有のトークン ID となっています。

X-Payjp-Webhook-Token: whook_xxxxxxxxxxxxx

Webhook を受け取るアプリケーション側で、この値が正しいかどうかを判別することで、PAY.JP からの Webhook であるかを確認できます。 自分のアカウントの X-Payjp-Webhook-Token を確認するには、PAY.JP ダッシュボードのアカウント設定から確認できます。

実装例

import express from 'express';
const app = express();

app.use(express.json());

const WEBHOOK_TOKEN = process.env.PAYJP_WEBHOOK_TOKEN;

app.post('/webhook', (req, res) => {
  // Webhook Tokenの検証
  const receivedToken = req.headers['x-payjp-webhook-token'];

  if (receivedToken !== WEBHOOK_TOKEN) {
    return res.status(401).json({ error: 'Invalid webhook token' });
  }

  const event = req.body;

  // イベントタイプに応じた処理
  switch (event.type) {
    case 'checkout.session.completed':
      handleCheckoutSessionCompleted(event.data);
      break;
    case 'payment_flow.succeeded':
      handlePaymentFlowSucceeded(event.data);
      break;
    // 他のイベントタイプ...
  }

  // 200ステータスコードを返す
  res.json({ received: true });
});

app.listen(3000);

Webhook が届かない場合の確認事項

Webhook が正常に届かない場合は、以下を確認してください

  • Webhook URL が正しく設定されているか
  • エンドポイントが公開アクセス可能か(ローカルホストなどの開発環境の場合は PAY.JP CLI を使用してください)
  • ダッシュボードの「テストイベント送信」機能でテストする

リクエスト形式

HTTP ヘッダー

Content-Type: application/json; charset=utf-8
X-Payjp-Webhook-Token: whook_xxxxxxxxxxxxx

リクエストボディ

{
  "id": "evt_xxxxxxxxxxxxxxxxxxxxxxxx",
  "object": "event",
  "created_at": "2025-01-15T10:30:00.000Z",
  "updated_at": "2025-01-15T10:30:00.000Z",
  "livemode": true,
  "type": "payment_flow.succeeded",
  "pending_webhooks": 1,
  "data": {
    "id": "pfw_xxxxxxxxxxxxxxxxxxxxxxxx",
    "object": "payment_flow",
    "status": "succeeded",
    "amount": 1000,
    "currency": "jpy",
    "payment_method_types": ["card"],
    "capture_method": "automatic",
    ...
  }
}

フィールド

  • id: イベントID
  • object: 常に event
  • created_at: イベント作成日時 (UTC, ISO 8601)
  • updated_at: イベント更新日時 (UTC, ISO 8601)
  • livemode: 本番環境かどうか (true: 本番, false: テスト)
  • type: イベントの種類
  • pending_webhooks: 配信完了していない Webhook の数(2xx ステータスコードが得られていない)
  • data: イベントに関連するリソースオブジェクト

イベントタイプ

Customer

  • customer.created - 顧客作成
  • customer.updated - 顧客更新
  • customer.deleted - 顧客削除

PaymentFlow

  • payment_flow.created - 作成
  • payment_flow.succeeded - 成功
  • payment_flow.canceled - キャンセル
  • payment_flow.amount_capturable_updated - キャプチャ可能金額更新
  • payment_flow.payment_failed - 支払い失敗
  • payment_flow.requires_action - 追加アクション必要(3Dセキュアなど)
  • payment_flow.processing - 処理中

SetupFlow

  • setup_flow.created - 作成
  • setup_flow.succeeded - 成功
  • setup_flow.canceled - キャンセル
  • setup_flow.requires_action - 追加アクション必要
  • setup_flow.setup_failed - セットアップ失敗

PaymentMethod

  • payment_method.attached - 顧客に紐付け
  • payment_method.detached - 紐付け解除
  • payment_method.updated - 更新

Checkout Session

  • checkout.session.completed - 完了
  • checkout.session.expired - 期限切れ

Refund

  • refund.created - 返金作成
  • refund.updated - 返金更新
  • refund.failed - 返金失敗

Product / Price / TaxRate

  • product.created / product.updated / product.deleted
  • price.created / price.updated / price.deleted
  • tax_rate.created / tax_rate.updated

その他

  • dispute.created - 不正懸念取引
  • term.created / term.closed - 集計区間
  • statement.created - 取引明細
  • balance.created / balance.fixed / balance.closed / balance.merged - 残高

Webhook エンドポイントの実装要件

1. 2xx ステータスコードを返す

PAY.JP は配信成功の判定に HTTP ステータスコードを使用します。2xx 以外を返すと、PAY.JPは自動的にリトライします。

2. 10秒以内に応答する

タイムアウトした場合、配信失敗とみなされリトライされます。

リトライポリシー

配信に失敗した場合(2xx以外のHTTPステータスコード、タイムアウト10秒、接続エラー)、PAY.JP は3分間隔で、最大3回まで自動的にリトライします。 すべてのリトライが失敗した場合、配信は停止します。

すべてのリトライが失敗した場合、pending_webhooks の値は減少しません。

Events API で配信状況を確認

イベント一覧を取得

curl https://api.pay.jp/v2/events \
  -H "Authorization: Bearer YOUR_API_KEY"

特定のイベントを取得

curl https://api.pay.jp/v2/events/evt_xxxxx \
  -H "Authorization: Bearer YOUR_API_KEY"

pending_webhooks フィールド

pending_webhooks フィールドは、設定した Webhook URL への通知が完了していない(2xxのレスポンスが得られていない)Webhook の数を表します。

pending_webhooks の値説明
0すべての Webhook が正常に配信された
1以上まだ配信完了していない Webhook がある

pending_webhooks が減らない場合

値が減らない原因として以下が考えられます

  • 2xx以外のステータスコードを返している
  • タイムアウト(10秒以上かかっている)
  • エラーが発生している