Refund Order API

Refund what a VA order collected via SePay API. Only an order in Paid or Partially status that has received money can be refunded.

||

Refund an order

POST
https://userapi.sepay.vn/v2/bank-accounts/{ba_xid}/orders/{order_xid}/refund

The order must be Paid or Partially and must have received money.

VietinBank enterprise accounts only for now

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.

X-Idempotency-Key is required

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

Order Refund Flow
Rendering diagram...

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

ba_xidstringrequired

Bank account UUID

order_xidstringrequired

Order UUID

Headers

X-Idempotency-Keystringrequired

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

refund_amountinteger

Amount to refund (VND, positive integer). Omit it to refund the whole remaining amount of the original payment.

transaction_idstring

The collection payment to refund. Only needed when the order has more than one collection payment; without it the call returns 422 transaction_required.

When the order was paid more than once

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)

Response 200 - Refund request accepted
{
  "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
The order keeps its status

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.

Order Refund Status
Rendering diagram...
refund_statusMeaning
nullNothing refunded yet, or every refund issued has failed
RefundPendingDispatched, the bank has not confirmed the money moved
PartiallyRefundedPart of paid_amount has been refunded successfully
RefundedThe whole paid_amount has been refunded successfully
refund_status is not monotonic like status

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

HTTPerror_codeDescription
404not_foundBank account or order not found
404transaction_not_foundThe transaction_id you passed does not belong to this order
409idempotency_key_reusedThis X-Idempotency-Key was already used for a different refund request. Use a new key.
422idempotency_key_requiredThe X-Idempotency-Key header is missing, or the key is longer than 100 characters
422not_availableSePay has not opened refunds on this account. Contact SePay.
422unsupported_bankThis account's bank does not support refunds
422unsupported_currencyThe bank cannot refund the original payment's currency
422invalid_stateThe order collected nothing, or is not Paid / Partially
422transaction_requiredThe order has several collection payments, pass transaction_id
422not_refundableThis payment cannot be refunded
422no_original_transactionThe original collection payment was not found
422already_refundedThe original payment is already fully refunded
422unsupported_amount_precisionThe original amount cannot be represented in its currency
422validation_errorrefund_amount is invalid, or above the remaining amount
422refund_declinedThe bank declined the refund
422refund_duplicate_referenceThe refund reference already exists at the bank
422refund_amount_invalidThe amount is not valid for the original payment
422refund_failedThe bank accepted the request but could not complete it
422refund_rejectedThe bank did not accept the refund
500unexpected_errorA 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.
503refund_busyAnother refund for the same original payment is in progress. No record was created; retry in a few seconds with the same key.
503vietinbank_map_connection_errorSePay 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.