Skip to Content
Khái niệmHoàn tiền
View as Markdown

Hoàn tiền

Refund là một entity hạng nhất, không phải một cờ trên Order. Bạn có thể phát hành refund từng phần, nhiều refund cùng cho một Order, hoặc refund + tính phí lại trong cùng một luồng.

Refund cũng có thể được tạo từ merchant app.

Có hai cách bản ghi refund có thể được tạo ra:

LuồngAi điền formAuthTrạng thái khi tạo
Merchant khởi tạoDashboard / backend của bạnHMAC (sk_…)APPROVED ngay lập tức
Khách hàng khởi tạoNgười mua, trên trang do hệ thống hostToken dùng một lần (không cần creds)PENDING — bạn approve, hoặc tự động ngắn mạch nếu cấu hình auto-approve

Luồng do khách hàng khởi tạo dùng một token refund-request ngắn hạn. Bạn phát hành token (qua B2B hoặc dashboard), giao URL cho người mua theo cách bạn muốn, và người mua hoàn tất chi tiết refund trên checkout.infraio.xyz/refund-request/:token. Người mua không bao giờ chạm vào API của bạn và không bao giờ thấy merchant key của bạn.

Vòng đời refund

Trạng tháiÝ nghĩa
PENDINGRefund đã ghi nhận, đang chờ approve. Refund do khách hàng khởi tạo luôn bắt đầu ở đây.
APPROVEDĐã được duyệt để thực thi. Refund do merchant khởi tạo nhảy thẳng vào đây.
REJECTEDRefund bị từ chối. Trạng thái Order không đổi.
EXECUTEDGiao dịch chuyển on-chain đã được xác nhận. Order chuyển sang PARTIALLY_REFUNDED / REFUNDED.

Merchant khởi tạo

Bạn quyết định refund (ví dụ người mua khiếu nại qua chat). Gọi endpoint do merchant khởi tạo — nó bỏ qua review và lập tức rơi vào APPROVED.

POST /b2b/v1/merchants/{merchant_id}/refunds { "order_id": "ord_01J5K…", "amount": "49.00", // partial hoặc full, theo display currency của order "reason": "customer complaint #4521", "refund_to_address": "0xBUYER…", // bắt buộc cho rail crypto "refund_network": "polygon", // network slug; xem Khái niệm → Chains "refund_token_address":"0xUSDC_CONTRACT" // contract ERC-20 được trả lại; thường là token gốc }

Không có trường currency trong refund request — refund luôn kế thừa display currency của order (USD hiện tại). Bộ ba (refund_to_address, refund_network, refund_token_address) là đích on-chain. Chúng bị bỏ qua với rail fiat (provider tự định tuyến).

Order giữ nguyên trạng thái hiện tại cho đến khi bạn thực thi chuyển on-chain (xem Thực thi refund crypto).


Khách hàng khởi tạo — token refund-request

Người mua điền form refund trên trang do hệ thống host, không phải trang của bạn. Việc duy nhất của bạn là phát hành token và chuyển URL.

Vòng đời token

Trạng tháiÝ nghĩaURL khách hàng hiển thị
ACTIVEToken đang chạy, now < expires_atForm refund (refund_to_address, reason, amount, ghi chú tùy chọn → metadata.note)
SUBMITTEDNgười mua đã hoàn tất form; một bản ghi refund đã tồn tạiThẻ trạng thái mirror /r/:linkToken
EXPIRED_UNUSEDTTL đã hết trước khi người mua submitThông báo: “This link has expired. Request a new one”
RENEWAL_REQUESTEDNgười mua đã yêu cầu link mớiThông báo chờ: “Your merchant has been notified”
RENEWEDMerchant đã approve renewal và phát hành thay thế“This link has been replaced — check your email for the new link” (token mới không bị tiết lộ ở đây để vô hiệu hóa kiểu tấn công forward link)
CANCELEDMerchant đã thu hồi token từ dashboard“This refund request was canceled” đơn giản

Token là single-use. Khi đã SUBMITTED, URL vẫn còn hiệu lực để người mua kiểm tra trạng thái nhưng không thể submit lại. Để phát hành refund thứ hai cho cùng order, hãy phát hành token mới.

TTL mặc định

Nguồn phát hànhTTL mặc địnhVì sao
POST /b2b/v1/merchants/{merchant_id}/refund-requests (HMAC)30 phútProgrammatic — giả định được giao cho người mua ngay lập tức.
Merchant dashboard24 giờThủ công — merchant paste URL vào email / SMS.

Bạn có thể override mặc định bằng trường body ttl_seconds. Không có ngưỡng tối thiểu hay tối đa nào được enforce; giá trị thường gặp từ 1 phút đến 7 ngày.

Phát hành qua B2B API

Dành cho backend muốn tự động tạo link refund ngay sau một cuộc hội thoại support, một luồng hủy order, v.v.

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 }

Chữ ký request B2B là hex lowercase thuần với không có prefix sha256= — prefix đó chỉ xuất hiện trên chữ ký webhook inbound (Infraio → server của bạn). Chuỗi ký B2B outbound là METHOD\nPATH\nTIMESTAMP\nBODY; xem Xác thực để biết thuật toán canonical.

Trường amount nằm trong body phát hành và bắt buộc. Nó khóa trần mà người mua có thể submit trên form — họ có thể submit ít hơn nhưng không bao giờ vượt quá. (Với refund partial, phát hành token với số partial; với refund đầy đủ, phát hành với tổng order.)

Cấu trúc cũ { "order_id": "..." } vẫn được chấp nhận và được coi là ref_type=order_id, nhưng các tích hợp mới nên dùng cặp ref_type + ref_value tường minh.

Phát hành qua dashboard

Modal Issue Refund trong merchant dashboard  bộc lộ một toggle: Execute now vs Send link to customer. Chọn mục thứ hai sẽ tạo một token refund-request (giống lệnh gọi B2B ở trên) và hiển thị URL cho bạn cùng nút copy và QR code. Paste vào kênh phù hợp — email, support chat, SMS.

Qua JavaScript SDK — openRefundRequest

Nếu bạn đã có @lartech/infraio-checkout-js trong stack và muốn người mua hoàn tất refund bên trong luồng trang của bạn (không qua URL bên ngoài), kết hợp lệnh phát hành B2B với sdk.openRefundRequest():

// Server-side: phát hành token const { token } = await fetch("/api/mint-refund-token", { method: "POST" }).then(r => r.json()); // Client-side: mở form do hệ thống host const sdk = await loadInfraIo("pk_live_yourkeyhere"); const close = sdk.openRefundRequest({ token, mode: "popup", // hoặc "redirect" | "embed" onSuccess: ({ linkToken, refundId }) => { // linkToken → trang trạng thái /r/:linkToken cho người mua. // refundId → reference B2B API để approve / reject. window.location.href = `/r/${linkToken}`; }, onCancel: () => { /* người mua đã đóng popup */ }, onError: (err) => { /* xem tham chiếu SDK */ }, });

Xem Tham chiếu SDK → sdk.openRefundRequest() để biết bảng option đầy đủ.

Customer renewal — phát hành lại do người mua chủ động

Nếu người mua mở URL sau khi token đã hết hạn, trang sẽ hiển thị nút Request new link thay cho form. Bấm vào sẽ:

  1. Gửi yêu cầu renewal (không cần creds; bản thân link đã cấp quyền)
  2. Tùy chọn ghi nhận một ghi chú tự do (customer_note) mà người mua có thể để lại cho bạn
  3. Chuyển token sang RENEWAL_REQUESTED và phát refund_request.renewal_requested đến webhook của bạn

Dashboard của bạn sẽ hiển thị badge trên widget renewal-requests. Approve nó (một cú click) và một token ACTIVE mới được phát hành, phát refund_request.renewed, và cho phép bạn copy URL mới để gửi lại. URL cũ vẫn truy cập được nhưng hiển thị “Replaced — check your email” để một bản copy URL cũ bị forward không thể dùng để dò ra URL mới.


Thực thi refund crypto

API ghi nhận intent — nó không di chuyển vốn. Bạn ký và broadcast giao dịch chuyển on-chain từ ví của mình, rồi stamp tx hash ngược trở lại bản ghi refund:

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

Cả ba trường body đều bắt buộc: cùng một tx hash có thể tồn tại trên các chain khác nhau, và bạn có thể refund bằng stablecoin khác với loại mà thanh toán gốc đã capture.

Khi InfraIO Pay thấy giao dịch đó đạt đủ số confirmation yêu cầu (xem Chains & tài sản), refund chuyển sang EXECUTED và tổng số đã refund của Order được cập nhật.

Chúng tôi chủ ý không giữ custody vốn của merchant, nghĩa là chúng tôi không thể thực thi refund thay bạn. Hãy xây phần gửi on-chain vào tool admin của bạn — eth_sendRawTransaction từ một multisig hoặc ví hot, với workflow kết thúc bằng việc POST tx hash vào refund API.

Refund trên TRON, Solana và TON

Quy trình vẫn như cũ: bạn gửi refund từ ví của chính mình, rồi submit tx hash. Chi tiết thay đổi theo network:

  • Màn hình refund trong dashboard hiển thị đích đến, số tiền, network và token, kèm mã QR ở những network hỗ trợ: QR Solana Pay trên Solana và link chuyển TON trên TON. Trên TRON, màn hình hiển thị địa chỉ đích để bạn copy (không có link ví nào mang theo số tiền), nên bạn tự nhập số tiền.
  • token_address là địa chỉ của token trên network đó: contract TRC-20, mint SPL, hoặc địa chỉ Jetton master.
  • Định dạng tx hash khác nhau: hex trần trên TRON, chữ ký base58 trên Solana, hash hex hoặc base64 trên TON.
  • Nền tảng xác minh đúng giao dịch đó on-chain, rồi chuyển refund sang EXECUTED, dựa trên số confirmation trong Chains & tài sản.

Sự kiện webhook

Subsystem refund phát hai họ event:

Vòng đời token (refund_request.*)

EventPhát khi
refund_request.createdMột token được phát hành — data.source là b2b / dashboard / renewal
refund_request.renewal_requestedMột người mua bấm “Request new link” sau khi token của họ hết hạn. Hãy subscribe — đây là tín hiệu để merchant hành động.
refund_request.renewedBạn đã approve một renewal và một token mới thay thế cái cũ. data.old_token / data.new_token tạo thành chuỗi audit.
refund_request.canceledBạn đã chuyển một token sang CANCELED từ dashboard. Idempotent — chỉ transition đầu tiên phát event. data.reason là ghi chú tùy chọn của merchant.

Vòng đời refund (payment.refund.*)

EventPhát khi
payment.refund.requestedMột bản ghi Refund mới tồn tại — từ bất kỳ nguồn nào (form submit, API do merchant khởi tạo, dashboard).
payment.refund.approvedRefund được approve — hoặc auto-approve (do merchant khởi tạo) hoặc sau khi bạn gọi /approve trên một refund pending.
payment.refund.rejectedBạn đã gọi /reject trên một refund pending.
payment.refund.executedVốn đã di chuyển (tx hash crypto của bạn đã đạt confirmation yêu cầu).

payment.failed không được phát cho refund — refund có chuỗi event riêng dưới prefix payment.refund.*.

Tiếp theo