Refunds
A refund returns money from a successful payment to the customer. You can refund the full amount in one go, or refund part of it and make more partial refunds later until the whole payment has been returned.
Refunds are often not final when the API responds. The payment provider can take seconds or days
to confirm. Treat the status in the response as the current state, and rely on
webhooks to learn the final outcome.
Refund status
| Status | Meaning |
|---|---|
pending | VNSwitch has sent the refund to the provider and is waiting for the result. |
succeeded | The provider has accepted the refund. The money is on its way to the customer. |
failed | The refund did not go through. See error_code and error_message. |
review | The refund needs a manual check before it can finish. |
VNSwitch sends these webhook events for refunds:
Event (event_type) | When |
|---|---|
refund_succeeded | The refund reached succeeded. |
refund_failed | The refund reached failed. |
refund_review | The refund was moved to review. |
No webhook is sent while a refund is pending.
Update your order records from the webhook, not only from the create response. Verify the signature and handle each event once, as described in Webhooks.
Create a refund
Creates a refund against a payment with status succeeded or partially_captured. The example
refunds 50,000 VND of a 150,000 VND payment. Send more requests later to refund more; the total
of all refunds cannot exceed the amount captured.
POST /refunds
Authentication: secret API key (api-key header).
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
payment_id | string | Yes | The payment to refund. |
amount | integer | No | Amount to refund, in the minor unit of the payment currency (for VND, whole dong: 50000 is 50,000 VND). Leave it out to refund the full captured amount. If you have already made a partial refund, send the amount you want instead, because the total of all refunds cannot exceed the amount captured. |
refund_type | string | No | instant (default) or scheduled. |
reason | string | No | Why you are refunding, up to 255 characters. Shown in the dashboard and useful for support staff. |
metadata | object | No | Your own key-value data, for example your order ID. Up to 50 keys; key names up to 40 characters, values up to 500 characters. |
refund_id | string | No | Your own ID for this refund. Sending the same refund_id again does not create a second refund, so you can retry safely. If you leave it out, VNSwitch creates an ID. |
Example request
Response
{
"refund_id": "ref_3b7e5a1c4d",
"payment_id": "pay_8f2c1d9e0a",
"amount": 50000,
"currency": "VND",
"status": "pending",
"reason": "Customer returned one item",
"metadata": { "order_id": "ORD-1001" },
"error_code": null,
"error_message": null,
"connector": "<PROVIDER>",
"created_at": "2026-11-01T09:30:00.000Z",
"updated_at": "2026-11-01T09:30:00.000Z"
}To refund the whole payment, send only payment_id (and reason if you want). VNSwitch then refunds the full captured amount.
Response
A refund object. Key fields:
| Field | Description |
|---|---|
refund_id | The refund ID. Store it to look the refund up later. |
payment_id | The payment that was refunded. |
amount, currency | The refunded amount and its currency. |
status | pending, succeeded, failed or review. See
Refund status. |
error_code, error_message | Set when the refund failed. unified_code and unified_message give a provider-independent version when available. |
connector | The payment provider that handles the refund. |
created_at, updated_at | ISO 8601 timestamps. |
Retrieve a refund
Returns the current state of a refund. Use it to check a refund that is still pending, for
example if you missed a webhook.
GET /refunds/{refund_id}
Authentication: secret API key (api-key header).
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
refund_id | string | Yes | The ID returned when you created the refund. |
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
force_sync | boolean | No | true asks the payment provider for the latest status before responding. Default false, which returns the status VNSwitch already has. |
Use force_sync=true sparingly, for example once when a refund has stayed pending longer than expected. For normal updates, wait for the webhook.
Example request
Response
{
"refund_id": "ref_3b7e5a1c4d",
"payment_id": "pay_8f2c1d9e0a",
"amount": 50000,
"currency": "VND",
"status": "succeeded",
"reason": "Customer returned one item",
"metadata": { "order_id": "ORD-1001" },
"error_code": null,
"error_message": null,
"connector": "<PROVIDER>",
"created_at": "2026-11-01T09:30:00.000Z",
"updated_at": "2026-11-01T09:31:12.000Z"
}Response
The refund object, with the same fields as Create a refund.
Update a refund
Changes the reason or metadata of a refund. It does not change the amount or resend the
refund to the provider.
POST /refunds/{refund_id}
Authentication: secret API key (api-key header).
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
refund_id | string | Yes | The refund to update. |
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
reason | string | No | New reason, up to 255 characters. |
metadata | object | No | New metadata. Same limits as when you create a refund. |
Example request
Response
{
"refund_id": "ref_3b7e5a1c4d",
"payment_id": "pay_8f2c1d9e0a",
"amount": 50000,
"currency": "VND",
"status": "succeeded",
"reason": "Item damaged in delivery",
"metadata": { "order_id": "ORD-1001", "ticket": "CS-2231" },
"error_code": null,
"error_message": null,
"connector": "<PROVIDER>",
"created_at": "2026-11-01T09:30:00.000Z",
"updated_at": "2026-11-01T10:05:40.000Z"
}Response
The updated refund object.
List refunds
Returns refunds that match the filters in the request body. This is a POST
because the filters are sent as JSON. All filters are optional; send {} to list your most
recent refunds.
POST /refunds/list
Authentication: secret API key (api-key header).
Body parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
payment_id | string | No | Only refunds of this payment. |
refund_id | string | No | Only this refund. |
start_time | string | No | ISO 8601 date-time. Only refunds created at or after this time. |
end_time | string | No | ISO 8601 date-time. Only refunds created up to this time. Defaults to now. Use it together with start_time. |
amount_filter | object | No | { "start_amount": 10000, "end_amount": 100000 }. Either bound can be left out. Amounts are in the minor unit. |
currency | array of strings | No | Currency codes, for example ["VND"]. |
refund_status | array of strings | No | Status filter. See the note below. |
limit | integer | No | Number of refunds to return, 1 to 100. Default 10. |
offset | integer | No | Number of refunds to skip, for paging. Default 0. |
The refund_status filter uses VNSwitch’s stored status names, which differ from the
status values in responses: pending, success (shown as
succeeded), failure and transaction_failure (shown as
failed), and manual_review (shown as review).
Example request
Response
{
"count": 1,
"total_count": 1,
"data": [
{
"refund_id": "ref_3b7e5a1c4d",
"payment_id": "pay_8f2c1d9e0a",
"amount": 50000,
"currency": "VND",
"status": "succeeded",
"reason": "Item damaged in delivery",
"metadata": { "order_id": "ORD-1001", "ticket": "CS-2231" },
"connector": "<PROVIDER>",
"created_at": "2026-11-01T09:30:00.000Z",
"updated_at": "2026-11-01T10:05:40.000Z"
}
]
}Response
| Field | Description |
|---|---|
count | Number of refunds in this response. |
total_count | Number of refunds that match the filters. Use it with limit and offset to page through results. |
data | Array of refund objects. |