Hoàn tiền cho đơn hàng
https://userapi.sepay.vn/v2/bank-accounts/{ba_xid}/orders/{order_xid}/refundĐơn hàng phải ở trạng thái Paid hoặc Partially và đã nhận được tiền.
Hoàn tiền hiện chỉ áp dụng cho tài khoản VietinBank doanh nghiệp. Tài khoản thuộc ngân hàng khác nhận 422 unsupported_bank, tài khoản chưa được bật nhận 422 not_available. Sandbox không áp dụng khóa này, nên lệnh hoàn chạy được trên Sandbox vẫn có thể trả 422 not_available khi lên thật.
Thiếu header này SePay trả 422 idempotency_key_required. Gửi lại cùng một khóa trả về đúng bản ghi cũ thay vì chi tiền lần hai.
Luồng xử lý
Bước 1 và 6 là phía bạn, và thứ tự của chúng có lý do: khóa phải nằm trong cơ sở dữ liệu của bạn trước khi gọi API, vì nếu request timeout thì đó là thứ duy nhất cho phép gửi lại đúng lệnh cũ thay vì chi tiền lần hai. Bước 10 chỉ cộng sổ khi status là succeeded; lệnh pending đã chiếm hạn mức nhưng chưa chắc tiền đã đi. Chi tiết ở Chống trùng và đối soát.
Bước 3 quyết định lệnh có chạy hay không: tổng các lần hoàn của cùng giao dịch gốc không vượt quá số tiền đã thu, và lệnh đang pending cũng chiếm hạn mức.
Tham số đường dẫn
UUID tài khoản ngân hàng
UUID đơn hàng
Header
Khóa chống trùng do bạn tự sinh, duy nhất cho mỗi yêu cầu hoàn tiền, tối đa 100 ký tự. Gửi lại cùng khóa trả về bản ghi cũ thay vì chi tiền lần hai. Khóa dài hơn 100 ký tự trả 422 validation_error với lỗi trên idempotency_key.
Request body
Số tiền cần hoàn (VND, số nguyên dương). Bỏ trống để hoàn toàn bộ phần còn lại của giao dịch gốc.
Giao dịch thu hộ cần hoàn. Chỉ cần khi đơn hàng có nhiều giao dịch thu hộ, thiếu thì trả 422 transaction_required.
Hạn mức hoàn tính theo từng giao dịch thu hộ gốc, không cộng gộp cả đơn hàng. Đơn hàng có nhiều giao dịch mà không truyền transaction_id sẽ nhận 422 transaction_required. Lấy ID giao dịch từ Danh sách giao dịch.
Code mẫu
curl --request POST \--url https://userapi.sepay.vn/v2/bank-accounts/f9e8d7c6-b5a4-3210-fedc-ba0987654321/orders/b2c3d4e5-f6a7-8901-bcde-f12345678902/refund \--header 'Authorization: Bearer REPLACE_BEARER_TOKEN' \--header 'X-Idempotency-Key: refund-order-DH20250001-01' \--header 'content-type: application/json' \--data '{"refund_amount":50000,"transaction_id":"string"}'
Response (HTTP 200)
{
"status": "success",
"data": {
"refund_id": "RF-7QK3-M8PZ-4VNW",
"order_type": "va_order",
"order_id": "string",
"order_code": "string",
"xid": "string",
"transaction_id": "string",
"original_reference": "string",
"amount": 50000,
"currency": "VND",
"scope": "full",
"status": "pending",
"created_at": "2026-08-28 14:05:00"
}
}status và paid_amount của đơn hàng không đổi sau khi hoàn. Thứ thay đổi là refund_status, xem phần dưới.
Trạng thái hoàn tiền của đơn hàng
Ngoài status của từng lệnh hoàn, bản thân đơn hàng mang thêm trường refund_status tổng hợp mọi lệnh hoàn của nó. Trường này có trong response của Danh sách đơn hàng và Chi tiết đơn hàng.
refund_status | Nghĩa |
|---|---|
null | Chưa hoàn tiền lần nào, hoặc mọi lệnh đã hoàn đều thất bại |
RefundPending | Đã gửi lệnh, ngân hàng chưa xác nhận tiền đã đi |
PartiallyRefunded | Đã hoàn thành công một phần paid_amount |
Refunded | Đã hoàn thành công đủ paid_amount |
Giá trị này được tính lại từ toàn bộ lệnh hoàn của đơn hàng mỗi lần có thay đổi, nên nó lùi được: một lệnh đang RefundPending mà sau đó ngân hàng báo thất bại sẽ đưa refund_status về null. Đừng coi RefundPending là bằng chứng tiền đã đi; chỉ PartiallyRefunded và Refunded mới dựa trên lệnh đã xác nhận thành công.
Khác với refund_status, status của từng lệnh hoàn là đơn điệu: đã succeeded thì không quay lại. Theo dõi lệnh cụ thể qua refund_id ở Chi tiết hoàn tiền.
Lỗi
| HTTP | error_code | Mô tả |
|---|---|---|
| 404 | not_found | Không tìm thấy tài khoản ngân hàng hoặc đơn hàng |
| 404 | transaction_not_found | transaction_id đã truyền không thuộc đơn hàng này |
| 409 | idempotency_key_reused | Khóa X-Idempotency-Key này đã dùng cho một yêu cầu hoàn tiền khác. Dùng khóa mới. |
| 422 | idempotency_key_required | Thiếu header X-Idempotency-Key, hoặc khóa dài quá 100 ký tự |
| 422 | not_available | Tài khoản chưa được SePay mở hoàn tiền. Liên hệ SePay. |
| 422 | unsupported_bank | Ngân hàng của tài khoản này chưa hỗ trợ hoàn tiền |
| 422 | unsupported_currency | Ngân hàng không hoàn được loại tiền của giao dịch gốc |
| 422 | invalid_state | Đơn hàng chưa nhận tiền, hoặc không ở trạng thái Paid / Partially |
| 422 | transaction_required | Đơn hàng có nhiều giao dịch thu hộ, cần truyền transaction_id |
| 422 | not_refundable | Giao dịch này không hoàn tiền được |
| 422 | no_original_transaction | Không tìm thấy giao dịch thu hộ gốc |
| 422 | already_refunded | Giao dịch gốc đã được hoàn hết |
| 422 | unsupported_amount_precision | Số tiền gốc không biểu diễn được theo đơn vị của loại tiền đó |
| 422 | validation_error | refund_amount không hợp lệ, hoặc vượt phần còn lại |
| 422 | refund_declined | Ngân hàng từ chối lệnh hoàn tiền |
| 422 | refund_duplicate_reference | Mã hoàn tiền đã tồn tại ở ngân hàng |
| 422 | refund_amount_invalid | Số tiền hoàn không hợp lệ với giao dịch gốc |
| 422 | refund_failed | Ngân hàng nhận lệnh nhưng không thực hiện được |
| 422 | refund_rejected | Ngân hàng không chấp nhận lệnh hoàn tiền |
| 500 | unexpected_error | Lỗi phía SePay, không phải request của bạn. Lệnh có thể đã tới ngân hàng, nên tra lại bằng Danh sách hoàn tiền hoặc gửi lại cùng khóa, đừng sinh khóa mới. |
| 503 | refund_busy | Đang có lệnh hoàn tiền khác chạy cho cùng giao dịch gốc. Chưa có bản ghi nào được tạo, thử lại sau ít giây với cùng khóa. |
| 503 | vietinbank_map_connection_error | Không gọi được sang VietinBank. Bản ghi hoàn đã tạo và đang pending, SePay tự đối soát tiếp. Gửi lại cùng khóa để nhận lại bản ghi đó, đừng sinh khóa mới. |