<!-- Source: https://docs.infraio.xyz/ja/concepts/refunds -->
<!-- Last updated: 2026-10-04 -->

# 返金

**Refund** は Order のフラグではなく、第一級のエンティティです。
部分返金、同じ Order に対する複数の返金、または返金 + 再課金を
同じフローで行えます。

> **Note:**
>
> 返金は[マーチャントアプリ](https://docs.infraio.xyz/ja/get-started/merchant-app)からも発行できます。

返金レコードが存在するようになる経路は 2 つあります:

| フロー | フォーム入力者 | 認証 | 着地ステータス |
| --- | --- | --- | --- |
| マーチャント開始 | あなたのダッシュボード / バックエンド | HMAC (sk_…) | 即座に `APPROVED` |
| 顧客開始 | バイヤーが当方のホスト型ページで | 1 回限りのトークン (資格情報なし) | `PENDING` — 承認するか、構成が自動承認ならショートカット |

顧客開始フローは短命の **返金リクエストトークン** を使います。
あなたはトークンを発行 (B2B またはダッシュボード) し、URL を
好きな方法でバイヤーに渡し、バイヤーは
`checkout.infraio.xyz/refund-request/:token` で返金詳細を完了させます。
バイヤーがあなたの API に触れることはなく、あなたのマーチャント
キーを目にすることもありません。

## 返金ライフサイクル

```mermaid
stateDiagram-v2
    [*] --> PENDING:  refund created (customer submit or B2B customer-flow)
    PENDING --> APPROVED: passes review (auto for merchant-initiated)
    PENDING --> REJECTED: review denies
    APPROVED --> EXECUTED: on-chain tx confirmed
    APPROVED --> REJECTED: canceled before execution
    REJECTED --> [*]
    EXECUTED --> [*]
```

| 状態 | 意味 |
| --- | --- |
| `PENDING` | 返金が記録され、承認待ち。顧客開始の返金は常にここから始まります。 |
| `APPROVED` | 実行可。マーチャント開始の返金はここに直接ジャンプします。 |
| `REJECTED` | 返金が拒否されました。注文ステータスは変わりません。 |
| `EXECUTED` | オンチェーン送金が確定。注文は `PARTIALLY_REFUNDED` / `REFUNDED` に遷移します。 |

---

## マーチャント開始

あなたが返金を決定 (例: バイヤーがチャットで苦情)。マーチャント開始
エンドポイントを呼ぶと、レビューをスキップして直接 `APPROVED` に
着地します。

```http
POST /b2b/v1/merchants/{merchant_id}/refunds
{
  "order_id": "ord_01J5K…",
  "amount": "49.00",                       // partial or full, in the order's display currency
  "reason": "customer complaint #4521",
  "refund_to_address":   "0xBUYER…",       // required for crypto rails
  "refund_network":      "polygon",        // network slug; see Concepts → Chains
  "refund_token_address":"0xUSDC_CONTRACT" // ERC-20 contract paid back; usually the original token
}
```

返金リクエストには `currency` フィールドはありません — 返金は常に
注文の表示通貨を継承します (今日は USD)。
`(refund_to_address, refund_network, refund_token_address)` の組は
オンチェーン送金先です。
法定通貨レール (プロバイダにより自動ルーティング) では無視されます。

注文はオンチェーン送金を実行するまで既存のステータスを維持します
([暗号資産返金の実行](#executing-a-crypto-refund) を参照)。

---

## 顧客開始 — 返金リクエストトークン

バイヤーは返金フォームを **当方のホスト型ページ** で記入します。
あなたの仕事はトークンを発行し URL を届けることだけです。

### トークンライフサイクル

```mermaid
stateDiagram-v2
    [*] --> ACTIVE:           mint (B2B or dashboard)
    ACTIVE --> SUBMITTED:     buyer submits the form
    ACTIVE --> EXPIRED_UNUSED: now > expires_at
    ACTIVE --> CANCELED:      merchant cancels (dashboard)
    EXPIRED_UNUSED --> RENEWAL_REQUESTED: buyer clicks "Request new link"
    RENEWAL_REQUESTED --> RENEWED: merchant approves, new ACTIVE token issued
    SUBMITTED --> [*]
    CANCELED --> [*]
    RENEWED --> [*]
```

| 状態 | 意味 | 顧客 URL の表示 |
| --- | --- | --- |
| `ACTIVE` | トークンは生きていて `now < expires_at` | 返金フォーム (`refund_to_address`, `reason`, `amount`, 任意のメモ → `metadata.note`) |
| `SUBMITTED` | バイヤーがフォームを完了; 返金レコードが存在 | `/r/:linkToken` をミラーしたステータスカード |
| `EXPIRED_UNUSED` | バイヤーが提出前に TTL が経過 | プロンプト: 「このリンクは期限切れです。新しいリンクを要求」 |
| `RENEWAL_REQUESTED` | バイヤーが新しいリンクを要求 | 待機通知: 「マーチャントに通知されました」 |
| `RENEWED` | マーチャントが再発行を承認し置き換えを発行 | 「このリンクは置き換えられました — メールで新しいリンクをご確認ください」(新しいトークンはここで明かさず、転送リンク攻撃を回避) |
| `CANCELED` | マーチャントがダッシュボードからトークンを取り消し | プレーンな「この返金リクエストはキャンセルされました」 |

> **Note:**
>
> トークンは 1 回限り使用です。一度 `SUBMITTED` になると、URL は
> バイヤーがステータスを確認するために有効なままですが、再提出には
> 使えません。同じ注文に対して 2 回目の返金を発行するには、新しい
> トークンを発行してください。

### TTL のデフォルト

| 発行元 | デフォルト TTL | 理由 |
| --- | --- | --- |
| `POST /b2b/v1/merchants/{merchant_id}/refund-requests` (HMAC) | **30 分** | プログラム — 直ちにバイヤーに渡されることを想定。 |
| マーチャントダッシュボード | **24 時間** | 手動 — マーチャントが URL をメール / SMS に貼り付け。 |

デフォルトは `ttl_seconds` ボディフィールドで上書きできます。最小値も
最大値も強制されません。一般的な値は 1 分から 7 日です。

### B2B API での発行

サポート会話の直後、注文キャンセルフローなどで返金リンクを
プログラマティックに生成したいバックエンド向けです。

```http
POST /b2b/v1/merchants/{merchant_id}/refund-requests
Content-Type: application/json
X-Client-ID: pk_live_…
X-Timestamp: 1729536000
X-Signature: 4f2a1b9c8d3e2f1a0b9c8d7e6f5a4b3c…

{
  "ref_type":    "order_id",                          // required: order_id | order_number | session_id | session_key
  "ref_value":   "ord_01J5K…",                        // required: matches ref_type
  "amount":      "49.00",                             // required — locks the maximum the buyer can submit
  "ttl_seconds": 1800,                                // optional — defaults to 1800 (30 min)
  "metadata":    { "support_ticket": "4521" },        // optional — Stripe-style key/value
  "hide_summary": false,                              // optional UI flags for the hosted form
  "hide_header":  false
}
```

> **Warning:**
>
> B2B リクエスト署名は **小文字の生 hex** で、**`sha256=` プレフィックスは
> 付きません** — そのプレフィックスは *インバウンド* の webhook 署名
> (Infraio → あなたのサーバー) にのみ現れます。アウトバウンドの B2B 署名
> 文字列は `METHOD\nPATH\nTIMESTAMP\nBODY` です; 正規のアルゴリズムは
> [認証](https://docs.infraio.xyz/ja/api-reference/authentication) を参照してください。

金額は発行ボディに **含まれ**、**必須** です。バイヤーがフォームで
提出できる上限をロックします — それ以下では提出できますが、それを
超えることは決してできません。(部分返金には部分額でトークンを発行し、
全額返金には注文合計で発行してください。)

以前の `{ "order_id": "..." }` 形式も引き続き受け入れられ、
`ref_type=order_id` として扱われますが、新しい統合では明示的な
`ref_type` + `ref_value` ペアを使ってください。

レスポンス:

```json
{
  "token":      "rfqt_01J7P3Q9R…",
  "refund_url": "https://checkout.infraio.xyz/refund-request/rfqt_01J7P3Q9R…",
  "expires_at": "2026-05-28T10:32:00Z"
}
```

Webhook エンドポイントへ `refund_request.created` を発火 (これで、
注文に対して現在どのトークンが有効かをログ / 監査できます)。

### ダッシュボードでの発行

[マーチャントダッシュボード](https://app.infraio.xyz) の Issue
Refund モーダルにはトグル: **Execute now** vs **Send link to customer**
があります。後者を選ぶと、返金リクエストトークンが作成され (上記の B2B
呼び出しと同じ)、コピーボタンと QR コード付きの
URL が表示されます。メール、サポートチャット、SMS など、適切なチャネルに
貼り付けてください。

### JavaScript SDK 経由 — `openRefundRequest`

`@lartech/infraio-checkout-js` を既にスタックに持っていて、バイヤーに
外部 URL ではなく自分のページフロー内で返金を完了させたい場合は、
B2B 発行と `sdk.openRefundRequest()` を組み合わせてください:

```ts
// Server-side: mint the token
const { token } = await fetch("/api/mint-refund-token", { method: "POST" }).then(r => r.json());

// Client-side: open the hosted form
const sdk = await loadInfraIo("pk_live_yourkeyhere");
const close = sdk.openRefundRequest({
  token,
  mode: "popup",                       // or "redirect" | "embed"
  onSuccess: ({ linkToken, refundId }) => {
    // linkToken → /r/:linkToken buyer status page.
    // refundId  → B2B API reference for approve / reject.
    window.location.href = `/r/${linkToken}`;
  },
  onCancel: () => { /* buyer closed the popup */ },
  onError:  (err) => { /* see SDK reference */ },
});
```

オプションの全表は [SDK リファレンス → `sdk.openRefundRequest()`](https://docs.infraio.xyz/ja/sdks/javascript#sdkopenrefundrequest-)
を参照してください。

### 顧客主導の再発行 — バイヤー駆動の再発行

トークン期限切れ後にバイヤーが URL を開くと、フォームの代わりに
**新しいリンクを要求** ボタンが表示されます。クリックすると:

1. 再発行リクエストを送信 (資格情報は不要; リンク自体が
   認可の役割を果たします)
2. 任意でバイヤーがあなたに残せるフリーテキストノート (`customer_note`)
   をキャプチャ
3. トークンを `RENEWAL_REQUESTED` に遷移し、Webhook に
   `refund_request.renewal_requested` を発火

ダッシュボードの再発行リクエストウィジェットにバッジが表示されます。
ワンクリックで承認すると、新しい `ACTIVE` トークンが発行され、
`refund_request.renewed` を発火し、新しい URL をコピーして再送できる
ようにします。古い URL はアクセス可能なままですが、「置き換え済み —
メールをご確認ください」と表示されるので、古い URL の転送コピーで
新しい URL を釣り出すことはできません。

---

## 暗号資産返金の実行

API は意図を記録します — 資金は動かしません。**あなた** がマーチャント
ウォレットからオンチェーン送金を署名してブロードキャストし、tx ハッシュを
返金レコードに戻してスタンプします:

```http
POST /b2b/v1/refunds/:refund_id/submit-tx
Content-Type: application/json

{
  "tx_hash": "0xabcd…",
  "network": "ethereum",
  "token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}
```

3 つのボディフィールドはすべて必須: 同じ tx ハッシュが異なるチェーンに
存在することがあり、また元の支払いがキャプチャされた通貨と異なる
ステーブルコインで返金することもあります。

InfraIO Pay がそのトランザクションが必要な確認カウント
([チェーン & アセット](https://docs.infraio.xyz/ja/concepts/chains) を参照) に達したことを
確認すると、返金は `EXECUTED` に遷移し、Order の返金合計が更新されます。

> **Warning:**
>
> 当方はマーチャント資金の管理を意図的に保持しません。これは
> あなたの代わりに返金を実行することができないことを意味します。
> オンチェーン送信を管理ツールに組み込んでください — マルチシグ
> またはホットウォレットからの `eth_sendRawTransaction`、最後に
> tx ハッシュを返金 API に投稿するワークフローで完結させましょう。

### TRON、Solana、TON での返金

流れは同じです。ご自身のウォレットから返金を送金し、トランザクションハッシュを送信します。詳細はネットワークによって異なります。

- ダッシュボードの返金画面には、送金先、金額、ネットワーク、トークンが表示され、対応するネットワークでは QR コードも表示されます（Solana では Solana Pay の QR、TON では TON 送金リンク）。TRON では、コピー用の送金先アドレスが表示されます（金額を含むウォレットリンクがないため、金額はご自身で入力してください）。
- `token_address` は、そのネットワーク上のトークンのアドレスです。TRC-20 コントラクト、SPL ミント、または Jetton マスターアドレスを指定します。
- トランザクションハッシュの形式はネットワークごとに異なります。TRON は 0x なしの hex、Solana は base58 署名、TON は hex または base64 のハッシュです。
- プラットフォームはその正確なトランザクションをオンチェーンで検証し、[対応チェーン・資産](https://docs.infraio.xyz/ja/concepts/chains) の確認回数に基づいて返金を `EXECUTED` に更新します。

---

## Webhook イベント

返金サブシステムは 2 つのイベントファミリを発火します:

### トークンライフサイクル (`refund_request.*`)

| イベント | 発火条件 |
| --- | --- |
| `refund_request.created` | トークンが発行された — `data.source` は `b2b` / `dashboard` / `renewal` |
| `refund_request.renewal_requested` | トークン期限切れ後にバイヤーが「新しいリンクを要求」をクリック。**サブスクライブ推奨 — マーチャントが対応する合図です。** |
| `refund_request.renewed` | あなたが再発行を承認し、新しいトークンが古いものを置き換えた。`data.old_token` / `data.new_token` が監査チェーンを形成。 |
| `refund_request.canceled` | あなたがダッシュボードからトークンを `CANCELED` に遷移させた。冪等 — 最初の遷移のみ発火。`data.reason` は任意のマーチャントノート。 |

### 返金ライフサイクル (`payment.refund.*`)

| イベント | 発火条件 |
| --- | --- |
| `payment.refund.requested` | 新しい Refund 行が存在 — 提出元 (フォーム提出、マーチャント開始 API、ダッシュボード) を問わず。 |
| `payment.refund.approved` | 返金が承認された — 自動承認 (マーチャント開始) または保留中に `/approve` を呼んだ後。 |
| `payment.refund.rejected` | 保留中の返金に `/reject` を呼んだ。 |
| `payment.refund.executed` | 資金が動いた (あなたの暗号資産 tx ハッシュが必要な確認に到達)。 |

返金に対しては `payment.failed` は **発火しません** — 返金には
`payment.refund.*` プレフィックス配下に独自のイベント系列があります。

## 次に

- [SDK リファレンス → `sdk.openRefundRequest()`](https://docs.infraio.xyz/ja/sdks/javascript#sdkopenrefundrequest-) — ホスト型返金フォームをポップアップ / リダイレクト / 埋め込みで開く。
- [API リファレンス → 返金](https://docs.infraio.xyz/ja/api-reference#refunds) — エンドポイントカタログ (発行、提出、再発行、ステータス)。
- [コンセプト → 注文](https://docs.infraio.xyz/ja/concepts/orders) — Refund 状態が Order ライフサイクルにどう紐付くか。
- [Webhook → 概要](https://docs.infraio.xyz/ja/webhooks/overview) — 全イベントカタログ。
