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.
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; payment-service
dùng chúng để điều khiển saga crypto. 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; 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. |
POST /payment/v1/merchants/{merchant_id}/refund-requests (dashboard JWT) | 24 giờ | Thủ công — merchant paste URL vào email / SMS. |
Cả hai endpoint chấp nhận trường body ttl_seconds nếu bạn muốn
override. Hiện không có ngưỡng min/max cứng phía server — giá trị
thường gặp là 1 phút đến 7 ngày. Giữ trong khoảng đó để tránh làm
người mua bất ngờ hoặc giữ chỗ trên các token đã hủ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 legacy { "order_id": "..." } vẫn được chấp nhận để tương
thích ngược — bên trong nó được map thành
(ref_type=order_id, ref_value=...) — nhưng các tích hợp mới nên dùng
cặp ref_type + ref_value tường minh.
Response:
{
"token": "rfqt_01J7P3Q9R…",
"refund_url": "https://checkout.infraio.xyz/refund-request/rfqt_01J7P3Q9R…",
"expires_at": "2026-05-28T10:32:00Z"
}Phát refund_request.created đến các webhook endpoint của bạn (để bạn
có thể log / audit token nào hiện đang active cho một order).
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ẽ gọi POST /payment/v1/merchants/{merchant_id}/refund-requests
ở hậu trường (xác thực JWT, body cùng cấu trúc như B2B ở trên), rồi
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ẽ:
- POST tới
/pub/v1/refund-requests/:token/request-renewal(không creds — bản thân token là bearer-of-truth) - Tùy chọn ghi nhận một ghi chú tự do (
customer_note) mà người mua có thể để lại cho merchant - 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à hệ thống phát hành một token ACTIVE mới, 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í merchant 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 chain watcher thấy tx đó clear đủ số confirmation cấu hình (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.
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 đủ.