返金
Refund は Order のフラグではなく、第一級のエンティティです。 部分返金、同じ Order に対する複数の返金、または返金 + 再課金を 同じフローで行えます。
返金レコードが存在するようになる経路は 2 つあります:
| フロー | フォーム入力者 | 認証 | 着地ステータス |
|---|---|---|---|
| マーチャント開始 | あなたのダッシュボード / バックエンド | HMAC (sk_…) | 即座に APPROVED |
| 顧客開始 | バイヤーが当方のホスト型ページで | 1 回限りのトークン (資格情報なし) | PENDING — 承認するか、構成が自動承認ならショートカット |
顧客開始フローは短命の 返金リクエストトークン を使います。
あなたはトークンを発行 (B2B またはダッシュボード) し、URL を
好きな方法でバイヤーに渡し、バイヤーは
checkout.infraio.xyz/refund-request/:token で返金詳細を完了させます。
バイヤーがあなたの API に触れることはなく、あなたのマーチャント
キーを目にすることもありません。
返金ライフサイクル
| 状態 | 意味 |
|---|---|
PENDING | 返金が記録され、承認待ち。顧客開始の返金は常にここから始まります。 |
APPROVED | 実行可。マーチャント開始の返金はここに直接ジャンプします。 |
REJECTED | 返金が拒否されました。注文ステータスは変わりません。 |
EXECUTED | オンチェーン送金が確定。注文は PARTIALLY_REFUNDED / REFUNDED に遷移します。 |
マーチャント開始
あなたが返金を決定 (例: バイヤーがチャットで苦情)。マーチャント開始
エンドポイントを呼ぶと、レビューをスキップして直接 APPROVED に
着地します。
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) の組は
オンチェーン送金先です; payment-service が暗号資産サガを動かすのに使います。
法定通貨レール (プロバイダにより自動ルーティング) では無視されます。
注文はオンチェーン送金を実行するまで既存のステータスを維持します (暗号資産返金の実行 を参照)。
顧客開始 — 返金リクエストトークン
バイヤーは返金フォームを 当方のホスト型ページ で記入します。 あなたの仕事はトークンを発行し URL を届けることだけです。
トークンライフサイクル
| 状態 | 意味 | 顧客 URL の表示 |
|---|---|---|
ACTIVE | トークンは生きていて now < expires_at | 返金フォーム (refund_to_address, reason, amount, 任意のメモ → metadata.note) |
SUBMITTED | バイヤーがフォームを完了; Refund 行が存在 | /r/:linkToken をミラーしたステータスカード |
EXPIRED_UNUSED | バイヤーが提出前に TTL が経過 | プロンプト: 「このリンクは期限切れです。新しいリンクを要求」 |
RENEWAL_REQUESTED | バイヤーが新しいリンクを要求 | 待機通知: 「マーチャントに通知されました」 |
RENEWED | マーチャントが再発行を承認し置き換えを発行 | 「このリンクは置き換えられました — メールで新しいリンクをご確認ください」(新しいトークンはここで明かさず、転送リンク攻撃を回避) |
CANCELED | マーチャントがダッシュボードからトークンを取り消し | プレーンな「この返金リクエストはキャンセルされました」 |
トークンは 1 回限り使用です。一度 SUBMITTED になると、URL は
バイヤーがステータスを確認するために有効なままですが、再提出には
使えません。同じ注文に対して 2 回目の返金を発行するには、新しい
トークンを発行してください。
TTL のデフォルト
| 発行元 | デフォルト TTL | 理由 |
|---|---|---|
POST /b2b/v1/merchants/{merchant_id}/refund-requests (HMAC) | 30 分 | プログラム — 直ちにバイヤーに渡されることを想定。 |
POST /payment/v1/merchants/{merchant_id}/refund-requests (ダッシュボード JWT) | 24 時間 | 手動 — マーチャントが URL をメール / SMS に貼り付け。 |
オーバーライドしたい場合、両方のエンドポイントが ttl_seconds ボディ
フィールドを受け付けます。現状サーバー側に強制される min/max の上下限
はありません — 一般的な値は 1 分から 7 日です。バイヤーを驚かせたり、
キャンセル済みトークンに容量を保持したりしないよう、この範囲に
留めてください。
B2B API での発行
サポート会話の直後、注文キャンセルフローなどで返金リンクを プログラマティックに生成したいバックエンド向けです。
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
}B2B リクエスト署名は 小文字の生 hex で、sha256= プレフィックスは
付きません — そのプレフィックスは インバウンド の webhook 署名
(Infraio → あなたのサーバー) にのみ現れます。アウトバウンドの B2B 署名
文字列は METHOD\nPATH\nTIMESTAMP\nBODY です; 正規のアルゴリズムは
認証 を参照してください。
金額は発行ボディに 含まれ、必須 です。バイヤーがフォームで 提出できる上限をロックします — それ以下では提出できますが、それを 超えることは決してできません。(部分返金には部分額でトークンを発行し、 全額返金には注文合計で発行してください。)
レガシーの { "order_id": "..." } 形式は後方互換のため依然受け入れます —
内部では (ref_type=order_id, ref_value=...) にマップされます — が、
新しい統合では明示的な ref_type + ref_value ペアを使ってください。
レスポンス:
{
"token": "rfqt_01J7P3Q9R…",
"refund_url": "https://checkout.infraio.xyz/refund-request/rfqt_01J7P3Q9R…",
"expires_at": "2026-05-28T10:32:00Z"
}Webhook エンドポイントへ refund_request.created を発火 (これで、
注文に対して現在どのトークンが有効かをログ / 監査できます)。
ダッシュボードでの発行
マーチャントダッシュボード の Issue
Refund モーダルにはトグル: Execute now vs Send link to customer
があります。後者を選ぶと、裏で
POST /payment/v1/merchants/{merchant_id}/refund-requests (JWT 認証、
ボディ形は上記の B2B と同じ) が呼ばれ、コピーボタンと QR コード付きの
URL が表示されます。メール、サポートチャット、SMS など、適切なチャネルに
貼り付けてください。
JavaScript SDK 経由 — openRefundRequest
@lartech/infraio-checkout-js を既にスタックに持っていて、バイヤーに
外部 URL ではなく自分のページフロー内で返金を完了させたい場合は、
B2B 発行と sdk.openRefundRequest() を組み合わせてください:
// 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()
を参照してください。
顧客主導の再発行 — バイヤー駆動の再発行
トークン期限切れ後にバイヤーが URL を開くと、フォームの代わりに 新しいリンクを要求 ボタンが表示されます。クリックすると:
/pub/v1/refund-requests/:token/request-renewalに POST (資格情報なし — トークン自体が bearer-of-truth)- 任意でバイヤーがマーチャントに残せるフリーテキストノート (
customer_note) をキャプチャ - トークンを
RENEWAL_REQUESTEDに遷移し、Webhook にrefund_request.renewal_requestedを発火
ダッシュボードの再発行リクエストウィジェットにバッジが表示されます。
ワンクリックで承認 — システムは新しい ACTIVE トークンを発行し、
refund_request.renewed を発火し、新しい URL をコピーして再送できる
ようにします。古い URL はアクセス可能なままですが、「置き換え済み —
メールをご確認ください」と表示されるので、古い URL の転送コピーで
新しい URL を釣り出すことはできません。
暗号資産返金の実行
API は意図を記録します — 資金は動かしません。あなた がマーチャント ウォレットからオンチェーン送金を署名してブロードキャストし、tx ハッシュを 返金レコードに戻してスタンプします:
POST /b2b/v1/refunds/:refund_id/submit-tx
Content-Type: application/json
{
"tx_hash": "0xabcd…",
"network": "ethereum",
"token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}3 つのボディフィールドはすべて必須: 同じ tx ハッシュが異なるチェーンに 存在することがあり、また元の支払いがキャプチャされた通貨と異なる ステーブルコインで返金することもあります。
チェーンウォッチャーがその tx が設定された確認カウント
(チェーン & アセット を参照) を満たしたと
確認すると、返金は EXECUTED に遷移し、Order の返金合計が更新されます。
当方はマーチャント資金の管理を意図的に保持しません。これは
あなたの代わりに返金を実行することができないことを意味します。
オンチェーン送信を管理ツールに組み込んでください — マルチシグ
またはホットウォレットからの eth_sendRawTransaction、最後に
tx ハッシュを返金 API に投稿するワークフローで完結させましょう。
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()— ホスト型返金フォームをポップアップ / リダイレクト / 埋め込みで開く。 - API リファレンス → 返金 — エンドポイントカタログ (発行、提出、再発行、ステータス)。
- コンセプト → 注文 — Refund 状態が Order ライフサイクルにどう紐付くか。
- Webhook → 概要 — 全イベントカタログ。