API hoàn tiền theo đơn hàng

Hoàn tiền phần đã thu của một đơn hàng VA qua SePay API. Chỉ đơn hàng ở trạng thái Paid hoặc Partially và đã nhận tiền mới hoàn được.

||

Hoàn tiền cho đơn hàng

POST
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.

Hiện chỉ hỗ trợ VietinBank doanh nghiệp

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.

Bắt buộc gửi X-Idempotency-Key

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ý

Luồng hoàn tiền theo đơn hàng
Rendering diagram...

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 statussucceeded; 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

ba_xidstringrequired

UUID tài khoản ngân hàng

order_xidstringrequired

UUID đơn hàng

X-Idempotency-Keystringrequired

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

refund_amountinteger

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.

transaction_idstring

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.

Khi đơn hàng có nhiều lần thu

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)

Response 200 - Đã ghi nhận lệnh hoàn tiền
{
  "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"
  }
}
statusstring
dataobject
Đơn hàng giữ nguyên trạng thái

statuspaid_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àngChi tiết đơn hàng.

Trạng thái hoàn tiền của đơn hàng
Rendering diagram...
refund_statusNghĩa
nullChư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
refund_status không đơn điệu như status

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ỉ PartiallyRefundedRefunded 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_idChi tiết hoàn tiền.

Lỗi

HTTPerror_codeMô tả
404not_foundKhông tìm thấy tài khoản ngân hàng hoặc đơn hàng
404transaction_not_foundtransaction_id đã truyền không thuộc đơn hàng này
409idempotency_key_reusedKhó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.
422idempotency_key_requiredThiếu header X-Idempotency-Key, hoặc khóa dài quá 100 ký tự
422not_availableTài khoản chưa được SePay mở hoàn tiền. Liên hệ SePay.
422unsupported_bankNgân hàng của tài khoản này chưa hỗ trợ hoàn tiền
422unsupported_currencyNgân hàng không hoàn được loại tiền của giao dịch gốc
422invalid_stateĐơn hàng chưa nhận tiền, hoặc không ở trạng thái Paid / Partially
422transaction_requiredĐơn hàng có nhiều giao dịch thu hộ, cần truyền transaction_id
422not_refundableGiao dịch này không hoàn tiền được
422no_original_transactionKhông tìm thấy giao dịch thu hộ gốc
422already_refundedGiao dịch gốc đã được hoàn hết
422unsupported_amount_precisionSố tiền gốc không biểu diễn được theo đơn vị của loại tiền đó
422validation_errorrefund_amount không hợp lệ, hoặc vượt phần còn lại
422refund_declinedNgân hàng từ chối lệnh hoàn tiền
422refund_duplicate_referenceMã hoàn tiền đã tồn tại ở ngân hàng
422refund_amount_invalidSố tiền hoàn không hợp lệ với giao dịch gốc
422refund_failedNgân hàng nhận lệnh nhưng không thực hiện được
422refund_rejectedNgân hàng không chấp nhận lệnh hoàn tiền
500unexpected_errorLỗ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.
503refund_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.
503vietinbank_map_connection_errorKhô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.