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ồng | Ai điền form | Auth | Trạng thái khi tạo |
|---|---|---|---|
| Merchant khởi tạo | Dashboard / backend của bạn | HMAC (sk_…) | APPROVED ngay lập tức |
| Khách hàng khởi tạo | Người mua, trên trang do hệ thống host | Token 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 |
|---|---|
PENDING | Refund đã 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. |
REJECTED | Refund bị từ chối. Trạng thái Order không đổi. |
EXECUTED | Giao 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ĩa | URL khách hàng hiển thị |
|---|---|---|
ACTIVE | Token đang chạy, now < expires_at | Form refund (refund_to_address, reason, amount, ghi chú tùy chọn → metadata.note) |
SUBMITTED | Người mua đã hoàn tất form; một bản ghi refund đã tồn tại | Thẻ trạng thái mirror /r/:linkToken |
EXPIRED_UNUSED | TTL đã hết trước khi người mua submit | Thông báo: “This link has expired. Request a new one” |
RENEWAL_REQUESTED | Người mua đã yêu cầu link mới | Thông báo chờ: “Your merchant has been notified” |
RENEWED | Merchant đã 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) |
CANCELED | Merchant đã 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ành | TTL mặc định | Vì sao |
|---|---|---|
POST /b2b/v1/merchants/{merchant_id}/refund-requests (HMAC) | 30 phút | Programmatic — giả định được giao cho người mua ngay lập tức. |
| Merchant dashboard | 24 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ẽ:
- Gửi yêu cầu renewal (không cần creds; bản thân link đã cấp quyền)
- 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 - Chuyển token sang
RENEWAL_REQUESTEDvà phátrefund_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_addresslà đị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.*)
| Event | Phát khi |
|---|---|
refund_request.created | Một token được phát hành — data.source là b2b / dashboard / renewal |
refund_request.renewal_requested | Mộ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.renewed | Bạ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.canceled | Bạ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.*)
| Event | Phát khi |
|---|---|
payment.refund.requested | Mộ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.approved | Refund đượ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.rejected | Bạn đã gọi /reject trên một refund pending. |
payment.refund.executed | Vố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
- Tham chiếu SDK →
sdk.openRefundRequest()— mở form refund do hệ thống host dưới dạng popup / redirect / embed. - Tham chiếu API → Refunds — catalog endpoint (mint, submit, renewal, status).
- Khái niệm → Đơn hàng — trạng thái Refund gắn lại vào vòng đời Order như thế nào.
- Webhooks → Tổng quan — catalog event đầy đủ.