Skip to Content
概念退款
View as Markdown

退款

Refund 是一級實體,而非 Order 上的旗標。你可以發起部分退款、 針對同一個訂單發起多筆退款,或在同一流程中退款 + 重新收款。

也可以從商家 App 發起退款。

退款紀錄有兩種產生方式:

流程由誰填表單驗證落點
商家發起你的儀表板 / 你的後端HMAC(sk_…)立即落在 APPROVED
客戶發起買家,在我們的託管頁一次性 token(無憑證)PENDING — 你核准,或若你的設定為自動核准則直接放行

客戶發起的流程使用短效的退款申請 token。你鑄造 token(B2B 或 儀表板),用任何你喜歡的方式把 URL 交給買家,買家就在 checkout.infraio.xyz/refund-request/:token 完成退款細節。買家 永遠不會接觸你的 API,也看不到你的商家金鑰。

退款生命週期

狀態意義
PENDING退款已記錄,等待核准。客戶發起的退款一律從這裡開始。
APPROVED已可執行。商家發起的退款直接跳到這裡。
REJECTED退款被駁回。訂單狀態不變。
EXECUTED鏈上轉帳已確認。訂單轉為 PARTIALLY_REFUNDED / REFUNDED。

商家發起

你決定退款(例如買家在 chat 上抱怨)。呼叫商家發起的 endpoint — 它會跳過審查並立即落在 APPROVED。

POST /b2b/v1/merchants/{merchant_id}/refunds { "order_id": "ord_01J5K…", "amount": "49.00", // 部分或全額,以訂單的顯示幣別表達 "reason": "customer complaint #4521", "refund_to_address": "0xBUYER…", // 加密貨幣管道必填 "refund_network": "polygon", // 網路 slug;參見 概念 → 鏈 "refund_token_address":"0xUSDC_CONTRACT" // 退回的 ERC-20 合約;通常是原始 token }

退款請求上沒有 currency 欄位 — 退款一律繼承訂單的顯示幣別(目前 為 USD)。三元組 (refund_to_address, refund_network, refund_token_address) 是鏈上目的地。法幣管道 會忽略它們(由 provider 自動路由)。

訂單會維持既有狀態,直到你執行鏈上轉帳(見執行加密貨幣退款)。


客戶發起 — 退款申請 token

買家在我們的託管頁面填寫退款表單,而非你的頁面。你的工作只是 鑄造 token 並交付 URL。

Token 生命週期

狀態意義客戶 URL 呈現
ACTIVEToken 有效,now < expires_at退款表單(refund_to_address、reason、amount、選填備註 → metadata.note)
SUBMITTED買家完成表單;退款紀錄已存在狀態卡,鏡像 /r/:linkToken
EXPIRED_UNUSEDTTL 在買家送出前過期提示「此連結已過期。請申請新的連結」
RENEWAL_REQUESTED買家申請了新連結等待通知:「已通知你的商家」
RENEWED商家核准續期並鑄造替代 token「此連結已被替換 — 請查看 email 中的新連結」(新 token 不會在此處顯示,以防止連結轉發攻擊)
CANCELED商家從儀表板撤銷 token純訊息「此退款申請已被取消」

Token 為單次使用。一旦 SUBMITTED,URL 仍對買家有效以便查看狀態, 但無法再次用來送出。如要對同一訂單發第二筆退款,請鑄造新 token。

TTL 預設

鑄造來源預設 TTL原因
POST /b2b/v1/merchants/{merchant_id}/refund-requests(HMAC)30 分鐘程式化 — 預期會立即交給買家。
商家儀表板24 小時人工 — 商家把 URL 貼到 email / SMS。

你可以用 body 中的 ttl_seconds 欄位覆寫預設值。系統不強制 最小或最大上下限;常見值為 1 分鐘到 7 天。

透過 B2B API 鑄造

對於想在 support 對話、訂單取消流程等之後以程式產生退款連結的後端。

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", // 必填:order_id | order_number | session_id | session_key "ref_value": "ord_01J5K…", // 必填:對應 ref_type "amount": "49.00", // 必填 — 鎖定買家可提交的上限 "ttl_seconds": 1800, // 選填 — 預設 1800(30 分鐘) "metadata": { "support_ticket": "4521" }, // 選填 — Stripe 風格 key/value "hide_summary": false, // 託管表單的選填 UI 旗標 "hide_header": false }

B2B 請求簽章是原始小寫 hex,沒有 sha256= 前綴 — 那個前綴 只出現在入站的 webhook 簽章(Infraio → 你的伺服器)上。出站的 B2B 簽章字串是 METHOD\nPATH\nTIMESTAMP\nBODY;規範演算法見 身分驗證。

金額在鑄造 body 內,而且必填。它鎖定買家在表單上可以提交 的上限 — 買家可以提交更少但不能更多。(部分退款請以該金額鑄造 token;全額退款請以訂單總額鑄造。)

較舊的 { "order_id": "..." } 結構仍可接受,並視為 ref_type=order_id,但新整合請使用明確的 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(讓你可以記錄 / 稽核某訂單目前哪個 token 有效)。

透過儀表板鑄造

商家儀表板 的 Issue Refund modal 提供切換:Execute now vs Send link to customer。選擇後者 會建立一個 refund-request token(與上面的 B2B 呼叫相同),然後顯示 URL 加上複製 按鈕與 QR code。把它貼到任何適合的通道 — email、support chat、SMS。

透過 JavaScript SDK — openRefundRequest

如果你的技術棧中已經有 @lartech/infraio-checkout-js,且希望買家 在你自己的頁面流程內完成退款(而非透過外部 URL),請把 B2B 鑄造 與 sdk.openRefundRequest() 搭配使用:

// 伺服端:鑄造 token const { token } = await fetch("/api/mint-refund-token", { method: "POST" }).then(r => r.json()); // Client 端:開啟託管表單 const sdk = await loadInfraIo("pk_live_yourkeyhere"); const close = sdk.openRefundRequest({ token, mode: "popup", // 或 "redirect" | "embed" onSuccess: ({ linkToken, refundId }) => { // linkToken → /r/:linkToken 買家狀態頁。 // refundId → 用於 B2B API 核准 / 駁回。 window.location.href = `/r/${linkToken}`; }, onCancel: () => { /* 買家關閉了 popup */ }, onError: (err) => { /* 詳見 SDK 參考 */ }, });

完整的選項表請見 SDK 參考 → sdk.openRefundRequest()。

客戶續期 — 買家驅動的重發

若買家在 token 過期後打開 URL,頁面會提供 Request new link 按鈕 取代表單。點擊後:

  1. 送出續期申請(無需憑證;連結本身即為授權)
  2. 選擇性擷取買家想留給你的自由文字備註(customer_note)
  3. 把 token 移到 RENEWAL_REQUESTED 並對你的 webhook 發出 refund_request.renewal_requested

你的儀表板會在 renewal-requests widget 上顯示徽章。一鍵核准後, 會發出新的 ACTIVE token、發出 refund_request.renewed, 讓你複製新 URL 再寄一次。舊 URL 仍可存取,但會顯示 「Replaced — check your email」,讓被轉發的舊 URL 無法用來釣出 新的。


執行加密貨幣退款

API 只記錄意圖 — 它不移動資金。你從資金庫錢包簽章並廣播鏈上 轉帳,然後把 tx hash 蓋回退款紀錄:

POST /b2b/v1/refunds/:refund_id/submit-tx Content-Type: application/json { "tx_hash": "0xabcd…", "network": "ethereum", "token_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }

Body 內三個欄位都必填:同一筆 tx hash 可能存在於不同鏈上,而且 你可能以與原始付款不同的穩定幣退款。

當 InfraIO Pay 看到那筆交易達到所需的確認數(見支援的鏈與資產) 時,退款會轉為 EXECUTED,訂單的退款總額也會更新。

我們刻意不託管商家資金,因此無法代你執行退款。請把鏈上送出整合到 你的 admin 工具中 — 從 multisig 或 hot wallet 做 eth_sendRawTransaction,並讓流程結束於把 tx hash 提交到退款 API。

TRON、Solana 與 TON 上的退款

流程相同:你從自己的錢包送出退款,然後提交交易雜湊。細節依網路而異:

  • 後台的退款畫面會顯示收款位址、金額、網路與代幣,並在網路支援時附上 QR code:Solana 上是 Solana Pay QR code,TON 上是 TON 轉帳連結。TRON 上會顯示可複製的收款位址(沒有可帶入金額的錢包連結),因此金額需由你自行輸入。
  • token_address 是該網路上代幣的位址:TRC-20 合約、SPL mint,或 Jetton master 位址。
  • 交易雜湊格式各不相同:TRON 是不帶前綴的十六進位,Solana 是 base58 簽章,TON 是十六進位或 base64 雜湊。
  • 平台會在鏈上驗證這筆確切的交易,然後依據支援的鏈與資產中的確認數把退款轉為 EXECUTED。

Webhook 事件

退款子系統會發出兩個事件家族:

Token 生命週期(refund_request.*)

事件觸發時機
refund_request.createdToken 被鑄造 — data.source 為 b2b / dashboard / renewal
refund_request.renewal_requested買家在 token 過期後點「Request new link」。請訂閱此事件 — 它是商家行動的觸發點。
refund_request.renewed你核准續期,新 token 取代舊的。data.old_token / data.new_token 形成稽核鏈。
refund_request.canceled你從儀表板把 token 翻為 CANCELED。冪等 — 只有第一次轉換會發出。data.reason 是商家選填備註。

退款生命週期(payment.refund.*)

事件觸發時機
payment.refund.requested一筆新的 Refund 列存在 — 任一來源(表單送出、商家發起的 API、儀表板)。
payment.refund.approved退款被核准 — 可能是自動核准(商家發起),或在待審筆上呼叫 /approve 之後。
payment.refund.rejected你對待審退款呼叫了 /reject。
payment.refund.executed資金已轉移(你的加密貨幣 tx hash 達到要求的確認數)。

payment.failed 不會對退款觸發 — 退款有自己的事件序列,前綴為 payment.refund.*。

下一步