Webhookを使用すると、決済の完了、失敗、返金などのイベントをリアルタイムで受け取ることができます。
ZAFA PAYは決済ステータスが変更されると、登録されたWebhookエンドポイントにHTTP POSTリクエストを送信します。
Webhookエンドポイント
複数のWebhookエンドポイントを登録してイベント通知を受け取ることができます。各エンドポイントには個別のURL、シークレット、およびオプションのイベントフィルターがあります。
| 機能 | 説明 |
|---|
| 複数エンドポイント | 加盟店ごとに最大16個のWebhookエンドポイントを登録可能 |
| イベントフィルター | 各エンドポイントが受信するイベントタイプを選択可能。フィルター未設定の場合、すべてのイベントを受信 |
| エンドポイント別シークレット | 各エンドポイントに署名検証用の個別のWebhook Secret(whsec_xxx形式)が付与 |
| 有効/無効切り替え | エンドポイントを削除せずに個別に有効化・無効化可能 |
Webhookエンドポイントは、加盟店管理画面の**「Webhook」**タブから作成・管理できます。
イベントタイプ
| イベント | 説明 |
|---|
payment.succeeded | 決済が正常に完了 |
payment.failed | 決済が失敗 |
payment.canceled | 決済がキャンセル |
payment.refunded | 返金が完了 |
payment.chargeback | チャージバックが発生 |
ペイロード
イベントタイプ(例: payment.succeeded)
決済ステータス(succeeded, failed, canceled, refunded, chargeback)
保存されたカードのID(pmi_xxx形式)。リカーリング決済またはsave_cardを使用した場合のみ
カードが将来の決済用に保存された場合のみtrueで含まれる
返金済み金額(文字列形式。payment.refundedイベントのみ)
エラーメッセージ(payment.failedイベントのみ)
カードブランド(visa, mastercard, amex, jcbなど)
ペイロード例
payment.succeeded(決済成功)
{
"event": "payment.succeeded",
"transaction_id": "tx_abc123",
"merchant_id": "acct_12345",
"merchant_name": "サンプルストア",
"status": "succeeded",
"amount": "100.00",
"currency": "usd",
"payment_method": "card",
"payment_method_id": "pmi_abc123",
"is_recurring": false,
"save_card": true,
"external_id": "order_12345",
"product_name": "プレミアムプラン",
"customer_id": "cust_12345",
"email": "customer@example.com",
"tel": "09012345678",
"card_brand": "visa",
"card_last4": "4242",
"cardholder_name": "TARO YAMADA",
"card_exp_month": 12,
"card_exp_year": 2028,
"metadata": {},
"created_at": "2024-01-15T10:30:00Z",
"timestamp": "2024-01-15T10:31:00Z"
}
payment_method_idはsave_cardが有効な場合、またはリカーリング決済の場合のみ含まれます
save_cardは初回決済でカードを保存した場合のみtrueで含まれます
is_recurringはリカーリング決済の場合trueになります
payment.failed(決済失敗)
{
"event": "payment.failed",
"transaction_id": "tx_abc123",
"merchant_id": "acct_12345",
"merchant_name": "サンプルストア",
"status": "failed",
"amount": "100.00",
"currency": "usd",
"payment_method": "card",
"is_recurring": false,
"external_id": "order_12345",
"product_name": "プレミアムプラン",
"customer_id": "cust_12345",
"email": "customer@example.com",
"tel": "09012345678",
"error": "Your card was declined.",
"card_brand": "visa",
"card_last4": "4242",
"cardholder_name": "TARO YAMADA",
"card_exp_month": 12,
"card_exp_year": 2028,
"metadata": {},
"created_at": "2024-01-15T10:30:00Z",
"timestamp": "2024-01-15T10:31:00Z"
}
errorフィールドはpayment.failedイベントでのみ含まれます。
payment.refunded(返金完了)
{
"event": "payment.refunded",
"transaction_id": "tx_abc123",
"merchant_id": "acct_12345",
"merchant_name": "サンプルストア",
"status": "refunded",
"amount": "100.00",
"currency": "usd",
"payment_method": "card",
"is_recurring": false,
"external_id": "order_12345",
"product_name": "プレミアムプラン",
"customer_id": "cust_12345",
"email": "customer@example.com",
"tel": "09012345678",
"amount_refunded": "50.00",
"card_brand": "visa",
"card_last4": "4242",
"cardholder_name": "TARO YAMADA",
"card_exp_month": 12,
"card_exp_year": 2028,
"metadata": {},
"created_at": "2024-01-15T10:30:00Z",
"timestamp": "2024-01-20T14:00:00Z"
}
amount_refundedフィールドはpayment.refundedイベントでのみ含まれます。部分返金の場合は返金済み金額が設定されます。
payment.chargeback(チャージバック発生)
{
"event": "payment.chargeback",
"transaction_id": "tx_abc123",
"merchant_id": "acct_12345",
"merchant_name": "サンプルストア",
"status": "chargeback",
"amount": "100.00",
"currency": "usd",
"payment_method": "card",
"is_recurring": false,
"external_id": "order_12345",
"product_name": "プレミアムプラン",
"customer_id": "cust_12345",
"email": "customer@example.com",
"tel": "09012345678",
"card_brand": "visa",
"card_last4": "4242",
"cardholder_name": "TARO YAMADA",
"card_exp_month": 12,
"card_exp_year": 2028,
"metadata": {},
"created_at": "2024-01-15T10:30:00Z",
"timestamp": "2024-02-01T09:00:00Z"
}
チャージバックが発生した場合、取引金額が加盟店から引き落とされる可能性があります。速やかに対応してください。
署名検証
Webhookリクエストには署名ヘッダーが含まれます。エンドポイントのWebhook Secretを使用して署名を検証することで、リクエストがZAFA PAYから送信されたことを確認できます。
署名ヘッダー
| 環境 | ヘッダー名 |
|---|
| Sandbox | X-Zafapay-Signature-Sandbox |
| Production | X-Zafapay-Signature |
検証方法
署名はリクエストボディ(JSON文字列)を、エンドポイントのWebhook Secretをキーとして HMAC-SHA256でハッシュ化したものです。
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return signature === expectedSignature;
}
// Express.jsでの使用例
app.post('/webhooks/zafapay', express.json(), (req, res) => {
const signature = req.headers['x-zafapay-signature-sandbox'];
// このエンドポイントのWebhook Secret(whsec_xxx形式)を使用
const secret = process.env.ZAFAPAY_WEBHOOK_SECRET;
if (!verifyWebhookSignature(req.body, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// イベント処理
const { event, transaction_id, status } = req.body;
switch (event) {
case 'payment.succeeded':
// 決済成功時の処理
break;
case 'payment.failed':
// 決済失敗時の処理
break;
case 'payment.refunded':
// 返金時の処理
break;
}
res.json({ received: true });
});
レスポンス
Webhookを正常に受信した場合は、HTTPステータスコード 2xx を返してください。
リトライ
ZAFA PAYは以下の場合にWebhookを自動的にリトライします:
- HTTPステータスコード
2xx 以外が返された場合
- 接続タイムアウトが発生した場合(10秒)
リトライスケジュール
| 試行回数 | 失敗後の待機時間 |
|---|
| 1回目 | 即時送信 |
| 2回目 | 1分後 |
| 3回目 | 5分後 |
| 4回目 | 30分後 |
| 5回目 | 2時間後 |
| 6回目 | 6時間後 |
合計6回のリトライが約9時間にわたって実行されます。すべてのリトライが失敗した場合、イベントはデッドレターキュー(DLQ)に移動され、手動での対応が必要になります。
リトライによる重複通知を防ぐため、transaction_idをキーにして冪等性を確保してください。
ベストプラクティス
署名を必ず検証する
不正なリクエストを防ぐため、署名検証は必須です
冪等性を確保する
同じイベントが複数回送信される可能性があるため、transaction_idをキーにして重複処理を防いでください
すぐにレスポンスを返す
Webhook処理は非同期で行い、すぐに 200 を返してください。処理に時間がかかるとタイムアウトでリトライが発生します
エラーをログに記録する
デバッグのため、受信したペイロードと処理結果をログに記録してください