Gửi một lệnh hoàn tiền, SePay chuyển sang ngân hàng và trả về refund_id để bạn theo dõi trạng thái tới khi có kết quả cuối cùng.
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.
Mỗi lệnh hoàn phải kèm header X-Idempotency-Key do bạn tự sinh. Thiếu header này SePay trả 422 idempotency_key_required thay vì thực hiện lệnh.
Lý do: nếu request timeout và bạn gửi lại, khóa chống trùng cho phép SePay trả về đúng bản ghi cũ. Không có khóa, lần gửi lại là một lệnh chi tiền thứ hai, và hạn mức hoàn chỉ chặn trường hợp hoàn toàn bộ, không chặn hoàn một phần.
Dùng lại khóa cũ nhưng đổi nội dung request thì SePay trả 409 idempotency_key_reused. Mỗi lệnh hoàn khác nhau cần một khóa khác nhau.
Quy tắc số tiền
| Bạn gửi | SePay hoàn |
|---|---|
Bỏ trống refund_amount | Toàn bộ phần còn lại của giao dịch gốc |
refund_amount nhỏ hơn phần còn lại | Đúng số tiền đó, phần còn lại vẫn hoàn tiếp được |
refund_amount lớn hơn phần còn lại | Từ chối, 422 kèm lỗi trên trường refund_amount |
| Giao dịch gốc đã hoàn hết | Từ chối, 422 already_refunded |
Hạn mức tính trên giao dịch thu hộ gốc
Hạn mức hoàn không tính trên tổng tiền của đơn hàng, mà trên từng giao dịch thu hộ đã vào tài khoản. Tổng các lệnh hoàn của một giao dịch không bao giờ vượt quá số tiền chính giao dịch đó thu được. Lệnh đang pending cũng chiếm hạn mức, nên một lệnh chưa có kết quả không mở đường cho lệnh thứ hai vượt số tiền đã thu.
Điều này quyết định cách endpoint theo đơn hàng hoạt động:
| Đơn hàng có | Bỏ trống transaction_id | Truyền transaction_id |
|---|---|---|
| 0 giao dịch thu hộ | 422 no_original_transaction | 422 no_original_transaction |
| Đúng 1 giao dịch | SePay tự chọn giao dịch đó, hạn mức bằng số tiền của nó | Chọn đúng giao dịch được chỉ định |
| 2 giao dịch trở lên | 422 transaction_required | Chọn đúng giao dịch được chỉ định, hạn mức bằng số tiền của riêng nó |
Ví dụ một đơn hàng 500.000đ được khách trả làm hai đợt 300.000đ và 200.000đ: đơn có hai giao dịch thu hộ, nên gọi hoàn theo đơn hàng mà không truyền transaction_id sẽ nhận 422 transaction_required. Chỉ định đợt 300.000đ thì hoàn được tối đa 300.000đ cho đợt đó, muốn hoàn nốt phần còn lại phải gửi lệnh thứ hai chỉ định đợt 200.000đ.
Với đơn hàng nhiều đợt thu, mỗi đợt là một hạn mức riêng và cần một lệnh hoàn riêng, kèm khóa chống trùng riêng. Đừng gửi refund_amount bằng tổng paid_amount của đơn hàng, lệnh đó sẽ bị từ chối vì vượt hạn mức của giao dịch được chọn.
Hoàn theo đơn hàng hay theo giao dịch
Hai endpoint chạy qua cùng một lớp xử lý và cùng một hạn mức, chỉ khác ở cách bạn chỉ ra khoản thu cần hoàn:
| Hoàn theo đơn hàng | Hoàn theo giao dịch | |
|---|---|---|
| Định danh bằng | UUID đơn hàng | ID giao dịch thu hộ |
| Chọn giao dịch | Tự động khi đơn có đúng 1 giao dịch, ngoài ra phải truyền transaction_id | Đã nằm sẵn trong đường dẫn |
| Dùng được khi khoản thu không có đơn hàng | Không | Có |
order_type trong response | va_order | va_order nếu khoản thu thuộc một đơn hàng VA, null nếu không thuộc đơn hàng nào |
Cập nhật refund_status của đơn hàng | Có | Có, khi khoản thu thuộc một đơn hàng |
| Hạn mức | Số tiền của giao dịch được chọn | Số tiền của chính giao dịch đó |
Vì hạn mức gắn với giao dịch gốc chứ không gắn với đường gọi, hoàn một khoản thu qua endpoint giao dịch rồi gọi tiếp endpoint đơn hàng cho cùng khoản đó sẽ thấy phần còn lại đã bị trừ. Không có cách nào chi tiền hai lần bằng cách đổi endpoint.
Đơn hàng giữ nguyên status và paid_amount sau khi hoàn. Hoàn tiền là bản ghi riêng, tra cứu qua Danh sách hoàn tiền.
Trạng thái hoàn tiền
| Trạng thái | Nghĩa |
|---|---|
pending | Đang xử lý, chưa có kết quả từ ngân hàng |
succeeded | Ngân hàng xác nhận tiền đã chuyển đi |
failed | Ngân hàng xác nhận tiền không chuyển đi |
rejected | Ngân hàng từ chối lệnh ngay từ đầu |
Ba trạng thái cuối là kết thúc và không đổi nữa. Một lệnh đã succeeded không bao giờ quay lại pending, nên bạn có thể ghi nhận kết quả ngay khi đọc được.
SePay tự cập nhật trạng thái các lệnh đang pending. Bạn chỉ cần đọc Chi tiết hoàn tiền hoặc lọc danh sách theo status.
Các endpoint
https://userapi.sepay.vn/v2/bank-accounts/{ba_xid}/orders/{order_xid}/refundhttps://userapi.sepay.vn/v2/transactions/{transaction_id}/refundhttps://userapi.sepay.vn/v2/bank-accounts/{ba_xid}/refundshttps://userapi.sepay.vn/v2/bank-accounts/{ba_xid}/refunds/{refund_id}Các bước tích hợp
Chuẩn bị
- Tạo API Token
- Tài khoản ngân hàng đã liên kết thuộc ngân hàng có hỗ trợ hoàn tiền, và đã được SePay bật hoàn tiền cho chính tài khoản đó. Chưa bật thì lệnh tạo trả
422 not_available - Lấy UUID tài khoản từ API Tài khoản ngân hàng
Sinh khóa chống trùng
Khóa là chuỗi của riêng bạn, duy nhất cho mỗi lệnh hoàn. Ghép từ mã đơn hàng và số thứ tự lần hoàn là đủ, miễn là lần gửi lại của cùng một lệnh dùng lại đúng chuỗi đó.
Gửi lệnh hoàn tiền
curl -X POST "https://userapi.sepay.vn/v2/bank-accounts/{ba_uuid}/orders/{order_uuid}/refund" \-H "Content-Type: application/json" \-H "Authorization: Bearer YOUR_API_TOKEN" \-H "X-Idempotency-Key: refund-DH20250001-01" \-d '{"refund_amount": 50000}'
{"status": "success","data": {"refund_id": "RF-7QK3-M8PZ-4VNW","order_type": "va_order","order_id": "b2c3d4e5-f6a7-8901-bcde-f12345678902","order_code": "DH20250001","xid": "e5f6a7b8-c9d0-1234-ef56-789012345abc","transaction_id": "6398452","original_reference": "164T24200GKAJ7BY","amount": 50000,"currency": "VND","scope": "partial","status": "pending","created_at": "2026-08-28 14:05:00"}}
Lưu refund_id lại. Đây là mã tra cứu lệnh hoàn, và cũng là mã bạn đưa cho SePay khi cần hỗ trợ.
Theo dõi kết quả
curl -X GET "https://userapi.sepay.vn/v2/bank-accounts/{ba_uuid}/refunds/RF-7QK3-M8PZ-4VNW" \-H "Authorization: Bearer YOUR_API_TOKEN"
Đọc status. Còn pending thì kiểm tra lại sau; succeeded, failed hoặc rejected là kết quả cuối.