Refund an order
https://userapi.sepay.vn/v2/bank-accounts/{ba_xid}/orders/{order_xid}/refundThe order must be Paid or Partially and must have received money.
Refunds currently apply to VietinBank enterprise accounts. An account at another bank returns 422 unsupported_bank; an account that has not been enabled returns 422 not_available. The Sandbox does not apply this gate, so a refund that works there can still return 422 not_available in production.
Without the header SePay returns 422 idempotency_key_required. Replaying the same key returns the existing record instead of moving money a second time.
How it runs
Steps 1 and 6 are yours, and their order matters: the key has to be in your database before the API call, because if the request times out it is the only thing that lets you resend the same refund instead of paying out twice. Step 10 books the amount only once status is succeeded; a pending refund already holds budget but the money may still not have moved. See Idempotency and reconciliation for the detail.
Step 3 decides whether the refund runs at all: refunds on the same original payment never total more than what it collected, and a refund still in pending holds budget too.
Path parameters
Bank account UUID
Order UUID
Headers
A dedup key you generate, unique per refund request, at most 100 characters. Replaying the same key returns the existing record instead of moving money twice. A longer key answers 422 validation_error with the error on idempotency_key.
Request body
Amount to refund (VND, positive integer). Omit it to refund the whole remaining amount of the original payment.
The collection payment to refund. Only needed when the order has more than one collection payment; without it the call returns 422 transaction_required.
The refundable budget is tracked per original collection payment, not pooled across the order. An order with several payments returns 422 transaction_required unless you pass transaction_id. Get transaction ids from List transactions.
Code Examples
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"
}
}The order's status and paid_amount do not change after a refund. What does change is refund_status, covered below.
The order's refund status
Beyond the status of each individual refund, the order itself carries a refund_status field that aggregates all of its refunds. It appears in the response of List orders and Order detail.
refund_status | Meaning |
|---|---|
null | Nothing refunded yet, or every refund issued has failed |
RefundPending | Dispatched, the bank has not confirmed the money moved |
PartiallyRefunded | Part of paid_amount has been refunded successfully |
Refunded | The whole paid_amount has been refunded successfully |
This value is recomputed from all of the order's refunds on every change, so it can walk back: a refund sitting at RefundPending that the bank later reports as failed takes refund_status back to null. Do not read RefundPending as proof the money moved; only PartiallyRefunded and Refunded rest on confirmed refunds.
Unlike refund_status, the status of an individual refund is monotonic: once succeeded, it never goes back. Track a specific refund by its refund_id in Refund detail.
Errors
| HTTP | error_code | Description |
|---|---|---|
| 404 | not_found | Bank account or order not found |
| 404 | transaction_not_found | The transaction_id you passed does not belong to this order |
| 409 | idempotency_key_reused | This X-Idempotency-Key was already used for a different refund request. Use a new key. |
| 422 | idempotency_key_required | The X-Idempotency-Key header is missing, or the key is longer than 100 characters |
| 422 | not_available | SePay has not opened refunds on this account. Contact SePay. |
| 422 | unsupported_bank | This account's bank does not support refunds |
| 422 | unsupported_currency | The bank cannot refund the original payment's currency |
| 422 | invalid_state | The order collected nothing, or is not Paid / Partially |
| 422 | transaction_required | The order has several collection payments, pass transaction_id |
| 422 | not_refundable | This payment cannot be refunded |
| 422 | no_original_transaction | The original collection payment was not found |
| 422 | already_refunded | The original payment is already fully refunded |
| 422 | unsupported_amount_precision | The original amount cannot be represented in its currency |
| 422 | validation_error | refund_amount is invalid, or above the remaining amount |
| 422 | refund_declined | The bank declined the refund |
| 422 | refund_duplicate_reference | The refund reference already exists at the bank |
| 422 | refund_amount_invalid | The amount is not valid for the original payment |
| 422 | refund_failed | The bank accepted the request but could not complete it |
| 422 | refund_rejected | The bank did not accept the refund |
| 500 | unexpected_error | A fault on SePay's side, not in your request. The refund may already have reached the bank, so look it up via List refunds or resend the same key, never a new one. |
| 503 | refund_busy | Another refund for the same original payment is in progress. No record was created; retry in a few seconds with the same key. |
| 503 | vietinbank_map_connection_error | SePay could not reach VietinBank. The refund record exists and is pending; SePay reconciles it on its own. Resend the same key to get that record back, never a new one. |