> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zafapay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook

> 決済イベントの通知を受け取る

## 概要

Webhookを使用すると、決済の完了、失敗、返金などのイベントをリアルタイムで受け取ることができます。
ZAFA PAYは決済ステータスが変更されると、登録されたWebhookエンドポイントにHTTP POSTリクエストを送信します。

<Note>
  Webhookエンドポイントは、加盟店管理画面（[https://app.zafapay.com](https://app.zafapay.com)）の\*\*「Webhook」\*\*タブから管理できます。各エンドポイントには署名検証用の個別のWebhook Secretがあります。
</Note>

## Webhookエンドポイント

複数のWebhookエンドポイントを登録してイベント通知を受け取ることができます。各エンドポイントには個別のURL、シークレット、およびオプションのイベントフィルターがあります。

| 機能             | 説明                                                 |
| -------------- | -------------------------------------------------- |
| 複数エンドポイント      | 加盟店ごとに最大16個のWebhookエンドポイントを登録可能                    |
| イベントフィルター      | 各エンドポイントが受信するイベントタイプを選択可能。フィルター未設定の場合、すべてのイベントを受信  |
| エンドポイント別シークレット | 各エンドポイントに署名検証用の個別のWebhook Secret（`whsec_xxx`形式）が付与 |
| 有効/無効切り替え      | エンドポイントを削除せずに個別に有効化・無効化可能                          |

Webhookエンドポイントは、加盟店管理画面の\*\*「Webhook」\*\*タブから作成・管理できます。

## イベントタイプ

| イベント                 | 説明                                                                                          |
| -------------------- | ------------------------------------------------------------------------------------------- |
| `payment.succeeded`  | 決済が正常に完了                                                                                    |
| `payment.failed`     | 決済が失敗                                                                                       |
| `payment.canceled`   | 決済がキャンセル — マーチャントによる void(`POST /v1/payments/{id}/void`)、またはホスト型決済ページ上で顧客が 3D セキュア認証中にキャンセル |
| `payment.refunded`   | 返金が完了                                                                                       |
| `payment.chargeback` | チャージバックが発生                                                                                  |

## ペイロード

<ResponseField name="event" type="string">
  イベントタイプ（例: `payment.succeeded`）
</ResponseField>

<ResponseField name="transaction_id" type="string">
  トランザクションID
</ResponseField>

<ResponseField name="merchant_id" type="string">
  加盟店ID
</ResponseField>

<ResponseField name="merchant_name" type="string">
  加盟店名
</ResponseField>

<ResponseField name="status" type="string">
  決済ステータス（`succeeded`, `failed`, `canceled`, `refunded`, `chargeback`）
</ResponseField>

<ResponseField name="amount" type="string">
  決済金額（文字列形式。例: `"100.00"`）
</ResponseField>

<ResponseField name="currency" type="string">
  通貨コード
</ResponseField>

<ResponseField name="payment_method" type="string">
  決済方法（`card`, `depot`など）
</ResponseField>

<ResponseField name="payment_method_id" type="string">
  保存されたカードのID（`pmi_xxx`形式）。リカーリング決済または`save_card`を使用した場合のみ
</ResponseField>

<ResponseField name="is_recurring" type="boolean">
  リカーリング（継続課金）決済の場合は`true`
</ResponseField>

<ResponseField name="save_card" type="boolean">
  カードが将来の決済用に保存された場合のみ`true`で含まれる
</ResponseField>

<ResponseField name="external_id" type="string">
  加盟店側の注文ID（決済作成時に指定した値）
</ResponseField>

<ResponseField name="product_name" type="string">
  商品名（決済作成時に指定した値）
</ResponseField>

<ResponseField name="customer_id" type="string">
  顧客ID（決済作成時に指定した値）
</ResponseField>

<ResponseField name="email" type="string">
  顧客のメールアドレス（決済作成時に指定した値）
</ResponseField>

<ResponseField name="tel" type="string">
  顧客の電話番号（決済作成時に指定した値）
</ResponseField>

<ResponseField name="amount_refunded" type="string">
  返金済み金額（文字列形式。`payment.refunded`イベントのみ）
</ResponseField>

<ResponseField name="error" type="object">
  エラー詳細（`payment.failed`イベントのみ）

  <Expandable title="error のプロパティ">
    <ResponseField name="code" type="string">
      統一エラーコード（例: `card_declined`, `expired_card`, `insufficient_funds`）
    </ResponseField>

    <ResponseField name="category" type="string">
      エラーカテゴリ: `authentication`, `soft_decline`, `hard_decline`, `gateway_error`
    </ResponseField>

    <ResponseField name="message" type="string">
      人間が読めるエラーメッセージ
    </ResponseField>

    <ResponseField name="recommended_action" type="string">
      推奨アクション: `retry`, `contact_customer`, `use_different_card`, `none`
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="card_brand" type="string">
  カードブランド（`visa`, `mastercard`, `amex`, `jcb`など）
</ResponseField>

<ResponseField name="card_last4" type="string">
  カード番号の下4桁
</ResponseField>

<ResponseField name="cardholder_name" type="string">
  カード名義人
</ResponseField>

<ResponseField name="card_exp_month" type="number">
  カード有効期限（月）
</ResponseField>

<ResponseField name="card_exp_year" type="number">
  カード有効期限（年）
</ResponseField>

<ResponseField name="card_country" type="string">
  カード発行国（ISO 3166-1 alpha-2コード、例: `US`, `JP`, `SG`）。BINルックアップデータが利用可能な場合のみ含まれます。
</ResponseField>

<ResponseField name="card_funding" type="string">
  カードの資金源タイプ（`credit`, `debit`, `prepaid`）。BINルックアップデータが利用可能な場合のみ含まれます。
</ResponseField>

<ResponseField name="metadata" type="object">
  決済作成時に指定したメタデータ
</ResponseField>

<ResponseField name="created_at" type="string">
  トランザクション作成日時（ISO 8601形式）
</ResponseField>

<ResponseField name="timestamp" type="string">
  Webhook送信日時（ISO 8601形式）
</ResponseField>

## ペイロード例

### payment.succeeded（決済成功）

```json theme={null}
{
  "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,
  "card_country": "US",
  "card_funding": "credit",
  "metadata": {},
  "created_at": "2024-01-15T10:30:00Z",
  "timestamp": "2024-01-15T10:31:00Z"
}
```

<Note>
  * `payment_method_id`は`save_card`が有効な場合、またはリカーリング決済の場合のみ含まれます
  * `save_card`は初回決済でカードを保存した場合のみ`true`で含まれます
  * `is_recurring`はリカーリング決済の場合`true`になります
</Note>

### payment.failed（決済失敗）

```json theme={null}
{
  "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": {
    "code": "card_declined",
    "category": "soft_decline",
    "message": "Your card was declined.",
    "recommended_action": "use_different_card"
  },
  "card_brand": "visa",
  "card_last4": "4242",
  "cardholder_name": "TARO YAMADA",
  "card_exp_month": 12,
  "card_exp_year": 2028,
  "card_country": "US",
  "card_funding": "credit",
  "metadata": {},
  "created_at": "2024-01-15T10:30:00Z",
  "timestamp": "2024-01-15T10:31:00Z"
}
```

<Note>
  `error`オブジェクトは`payment.failed`イベントでのみ含まれます。`recommended_action`の値に応じて適切な対応を行ってください（`retry`, `contact_customer`, `use_different_card`, `none`）。
</Note>

### payment.canceled（決済キャンセル）

```json theme={null}
{
  "event": "payment.canceled",
  "transaction_id": "tx_abc123",
  "merchant_id": "acct_12345",
  "merchant_name": "サンプルストア",
  "status": "canceled",
  "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,
  "card_country": "US",
  "card_funding": "credit",
  "metadata": {},
  "created_at": "2024-01-15T10:30:00Z",
  "timestamp": "2024-01-15T10:32:00Z"
}
```

<Note>
  `payment.canceled` は以下の2つのケースで送信されます:

  1. **マーチャントによる void** — 決済確定前のオーソリ/キャプチャ済み決済を `POST /v1/payments/{id}/void` で取り消した場合。
  2. **顧客による 3DS 中のキャンセル** — ホスト型 3DS チャレンジ画面で顧客が「お支払いをキャンセル」をクリックした場合。同じ決済リンクで即座に再決済可能になります(別のカードでリトライ等)。

  両ケースとも同じイベント形式です。区別が必要な場合は `GET /v1/payments/{id}` でトランザクションの直前状態を確認するか、独自の状態管理を併用してください。
</Note>

### payment.refunded（返金完了）

```json theme={null}
{
  "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,
  "card_country": "US",
  "card_funding": "credit",
  "metadata": {},
  "created_at": "2024-01-15T10:30:00Z",
  "timestamp": "2024-01-20T14:00:00Z"
}
```

<Note>
  `amount_refunded`フィールドは`payment.refunded`イベントでのみ含まれます。部分返金の場合は返金済み金額が設定されます。
</Note>

### payment.chargeback（チャージバック発生）

```json theme={null}
{
  "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,
  "card_country": "US",
  "card_funding": "credit",
  "metadata": {},
  "created_at": "2024-01-15T10:30:00Z",
  "timestamp": "2024-02-01T09:00:00Z"
}
```

<Warning>
  チャージバックが発生した場合、取引金額が加盟店から引き落とされる可能性があります。速やかに対応してください。
</Warning>

## 署名検証

Webhookリクエストには署名ヘッダーが含まれます。エンドポイントのWebhook Secretを使用して署名を検証することで、リクエストがZAFA PAYから送信されたことを確認できます。

### 署名ヘッダー

| 環境         | ヘッダー名                         |
| ---------- | ----------------------------- |
| Sandbox    | `X-Zafapay-Signature-Sandbox` |
| Production | `X-Zafapay-Signature`         |

### 検証方法

署名はリクエストボディ（JSON文字列）を、エンドポイントのWebhook Secretをキーとして HMAC-SHA256でハッシュ化したものです。

```javascript Node.js theme={null}
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` を返してください。

```json theme={null}
{
  "received": true
}
```

## リトライ

ZAFA PAYは以下の場合にWebhookを自動的にリトライします：

* HTTPステータスコード `2xx` 以外が返された場合
* 接続タイムアウトが発生した場合（10秒）

### リトライスケジュール

| 試行回数 | 失敗後の待機時間 |
| ---- | -------- |
| 1回目  | 即時送信     |
| 2回目  | 1分後      |
| 3回目  | 5分後      |
| 4回目  | 30分後     |
| 5回目  | 2時間後     |
| 6回目  | 6時間後     |

合計6回のリトライが約9時間にわたって実行されます。すべてのリトライが失敗した場合、イベントはデッドレターキュー（DLQ）に移動され、手動での対応が必要になります。

<Tip>
  リトライによる重複通知を防ぐため、`transaction_id`をキーにして冪等性を確保してください。
</Tip>

## ベストプラクティス

<Steps>
  <Step title="署名を必ず検証する">
    不正なリクエストを防ぐため、署名検証は必須です
  </Step>

  <Step title="冪等性を確保する">
    同じイベントが複数回送信される可能性があるため、`transaction_id`をキーにして重複処理を防いでください
  </Step>

  <Step title="すぐにレスポンスを返す">
    Webhook処理は非同期で行い、すぐに `200` を返してください。処理に時間がかかるとタイムアウトでリトライが発生します
  </Step>

  <Step title="エラーをログに記録する">
    デバッグのため、受信したペイロードと処理結果をログに記録してください
  </Step>
</Steps>
