Refund a payment
Issue a refund with One API. Refunds are merchant-initiated backend operations with no customer-facing step, so the whole flow is in your backend.
On this page11 sections
Build with AI 2 packages
A build package is every page for one task, with the API operations they use. Copy the prompt into a coding assistant, or hand it the package itself: slim links to each page, full inlines all of them in one document.
- Refund a paymentEverything needed to refund a completed payment through One API, in full or in part, and to handle the statuses a refund moves through.
- Migrate refunds from the Payments API to One APIEverything needed to move an existing refunds integration onto One API, with the legacy guide and its One API counterpart side by side.
This guide walks you through issuing refunds to your customers using One API. Refunds are merchant-initiated backend operations, there is no customer-facing step. The entire flow happens in your backend.
This guide uses the One API: Ozow's recommended API for all new integrations.
Already integrated? If your integration posts to
api.ozow.comand builds a SHA512SHA-512 A hashing algorithm. Ozow uses it to sign the values in a request or a notification so you can tell that they arrived unaltered and came from us. Hashing is one-way: the hash cannot be turned back into what produced it.Wikipedia hash, you're on the Payments API: see Refund a payment under Legacy integrations, or Migrating to One API.
FloatFloat The balance held with Ozow that payouts and refunds are paid out of. Both draw on it, and neither will process while it is empty. Payins do not need one, so if you only take payments you never meet it. required
Ozow uses your float balance to fund refunds. Make sure your float has sufficient funds before issuing refunds. See the Float top-up guide to load funds into your float.
Before you start
- You have a valid One API access token: see Step 1: Obtain an access token for the token flow
- Your One API client must have the
refundsscope, and your token must request it. A token issued forpaymentsalone is refused by every endpoint on this page - Your float balance is sufficient to cover the refund amount
- You have the transaction ID of the original payment you want to refund
Environments
| Environment | Base URL | Dashboard |
|---|---|---|
| Production | https:/ |
dash.ozow.com |
| Staging | https:/ |
stagingdash.ozow.com |
How refunds work
You can request a refund two ways: against a specific transaction, or as a batch of one or more refunds in a single call. Both create the same kind of refund resource and follow the same lifecycle.
Refund a single transaction
Use this to refund a specific payment. You can issue a full refund or a partial refund by specifying the amount.
POST https://one.ozow.com/v1/transactions/{id}/refunds
Authorization: Bearer YOUR_ACCESS_TOKEN
Idempotency-Key: YOUR_UNIQUE_KEY
Content-Type: application/json
Replace {id} with the transaction ID of the original payment.
Idempotency keyIdempotency A request is idempotent when sending it twice has the same effect as sending it once. It matters most where a retry after a timeout could otherwise take a payment twice.IETF draft
Include a unique Idempotency-Key header with every refund request. If a
request fails and you retry, using the same idempotency key prevents the refund from being
processed twice.
curl -X POST "https://one.ozow.com/v1/transactions/497f6eca-6276-4993-bfeb-53cbbbba6f08/refunds" \
-H "Authorization: Bearer <YOUR_ACCESS_TOKEN>" \
-H "Idempotency-Key: YOUR_UNIQUE_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": {
"currency": "ZAR",
"value": 50.00
},
"reason": "Order cancellation",
"realTimePayment": false
}'
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer",
"YOUR_ACCESS_TOKEN"
);
client.DefaultRequestHeaders.Add("Idempotency-Key", "YOUR_UNIQUE_KEY");
var payload = new
{
amount = new { currency = "ZAR", value = 50.00 },
reason = "Order cancellation",
realTimePayment = false,
};
var response = await client.PostAsync(
"https://one.ozow.com/v1/transactions/497f6eca-6276-4993-bfeb-53cbbbba6f08/refunds",
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
);
var result = await response.Content.ReadAsStringAsync();
<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://one.ozow.com/v1/transactions/497f6eca-6276-4993-bfeb-53cbbbba6f08/refunds",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
"amount" => ["currency" => "ZAR", "value" => 50.00],
"reason" => "Order cancellation",
"realTimePayment" => false,
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_ACCESS_TOKEN",
"Idempotency-Key: YOUR_UNIQUE_KEY",
"Content-Type: application/json",
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
?>
const response = await fetch(
"https://one.ozow.com/v1/transactions/497f6eca-6276-4993-bfeb-53cbbbba6f08/refunds",
{
method: "POST",
headers: {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Idempotency-Key": "YOUR_UNIQUE_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: { currency: "ZAR", value: 50.00 },
reason: "Order cancellation",
realTimePayment: false,
}),
},
);
const data = await response.json();
import requests
response = requests.post(
"https://one.ozow.com/v1/transactions/497f6eca-6276-4993-bfeb-53cbbbba6f08/refunds",
headers={
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Idempotency-Key": "YOUR_UNIQUE_KEY",
"Content-Type": "application/json",
},
json={
"amount": {"currency": "ZAR", "value": 50.00},
"reason": "Order cancellation",
"realTimePayment": False,
},
)
print(response.json())
Key request fields
| Field | Type | Required | Description |
|---|---|---|---|
amount. |
string | Yes | Must be ZAR |
amount. |
number | Yes | Amount to refund. Must not exceed the original transaction amount |
reason |
string | Yes | Reason for the refund |
real |
boolean | Yes | Whether the refund pays out in real time. See Ozow Pricing for the cost of real-time refunds. Defaults to false |
notify |
string | No | URL to notify of the refund status. Use webhooks instead: see Handling the refund outcome |
For the full list of request fields see Request Refund.
Successful response
{
"links": {
"self": "https://one.ozow.com/v1/refunds/6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"cancel": "https://one.ozow.com/v1/refunds/6ba7b810-9dad-11d1-80b4-00c04fd430c8/cancel"
},
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"transactionId": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"amount": {
"currency": "ZAR",
"value": 50.00
},
"requested": "2026-01-01T00:00:00Z",
"status": "Pending",
"reason": "Order cancellation",
"realTimePayment": false
}
Store the refund id, you can use it to check the status of the refund.
To see the refunds already requested against a transaction, GET the same endpoint:
GET https://one.ozow.com/v1/transactions/{id}/refunds
Authorization: Bearer YOUR_ACCESS_TOKEN
Refund multiple transactions in one request
POST /refunds submits a batch of refund requests, each against its own transaction. Use this when
you need to issue several refunds at once; for a single refund it is simpler to use the transaction
endpoint above.
POST https://one.ozow.com/v1/refunds
Authorization: Bearer YOUR_ACCESS_TOKEN
Idempotency-Key: YOUR_UNIQUE_KEY
Content-Type: application/json
Request example
[
{
"transactionId": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"amount": {
"currency": "ZAR",
"value": 50.00
},
"reason": "Order cancellation",
"realTimePayment": false
},
{
"transactionId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"amount": {
"currency": "ZAR",
"value": 100.00
},
"reason": "Duplicate charge",
"realTimePayment": false
}
]
Key request fields
| Field | Type | Required | Description |
|---|---|---|---|
transaction |
string | Yes | The transaction ID of the payment being refunded |
amount. |
string | Yes | Must be ZAR |
amount. |
number | Yes | Amount to refund. Must not exceed the original transaction amount |
reason |
string | Yes | Reason for the refund |
real |
boolean | Yes | Whether the refund pays out in real time. Defaults to false |
notify |
string | No | URL to notify of the refund status. Use webhooks instead |
For the full list of request fields see Request Refunds.
Successful response
Ozow returns a Refund resource per item submitted, in the same shape shown above.
Check a refund's status
GET https://one.ozow.com/v1/refunds/{id}
Authorization: Bearer YOUR_ACCESS_TOKEN
Replace {id} with the refund ID returned when you created the refund.
Cancel a refund
Cancels the refund if it can still be cancelled.
POST https://one.ozow.com/v1/refunds/{id}/cancel
Authorization: Bearer YOUR_ACCESS_TOKEN
Idempotency-Key: YOUR_UNIQUE_KEY
Content-Type: application/json
Request body
{
"reason": "Requested in error"
}
reason is required. A successful cancellation returns 200 OK with no response body.
List refunds
Retrieve a paginated list of refunds filtered by date range.
GET https://one.ozow.com/v1/refunds?fromDate=2026-01-01T00:00:00Z&toDate=2026-01-31T23:59:59Z
Authorization: Bearer YOUR_ACCESS_TOKEN
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from |
string | Yes | Start of date range, ISO 8601ISO 8601 The international standard for writing dates and times, such as 2026-03-14. Unambiguous about ordering and time zone, which local formats are not.Wikipedia date-time |
to |
string | Yes | End of date range, ISO 8601 date-time |
limit |
integer | No | Maximum items to return. 1 to 50, defaults to 50 |
offset |
integer | No | Number of items to skip, defaults to 0 |
site |
string | No | Filter to a single site codeSite code The unique code for a site registered under a merchant. A site is a place to transact: a website, or a branch of a store. A merchant can have several, and each transaction names the one it belongs to, so sending the wrong code files the payment against the wrong place. |
Successful response
{
"links": {
"self": "https://one.ozow.com/v1/refunds?fromDate=2026-01-01T00:00:00Z&toDate=2026-01-31T23:59:59Z&offset=0"
},
"results": [
{
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"transactionId": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"status": "Complete"
}
],
"meta": {
"totalPages": 1,
"totalItems": 1
}
}
Refund statuses
| Status | Description |
|---|---|
pending |
The refund request has been submitted and accepted |
submitted |
The refund has been assigned to a batch and is being processed |
complete |
The refund has been paid successfully |
failed |
The refund payment has failed |
cancelled |
The refund was cancelled before it was submitted |
returned |
The refund payment was returned because the destination account no longer exists |
Handling the refund outcome
Ozow sends a refund.complete webhookWebhook A URL of yours that Ozow calls when something happens, rather than you polling to find out. The call carries no credential of yours and arrives at a public URL, so authenticate it before acting on it: a hash field on the Payments API, a Svix signature on One API. when a refund completes. Handle it the same way as a payment
webhook, verify the Svix signature before acting on it.
See Step 4: Handle the webhook notification in the One API redirectRedirect Sending the payer to the Ozow payment page to complete the payment, and returning them to your site afterwards. The alternative is embedding the checkout in your own page, where the payer never leaves it. guide for the full webhook verification process.
Note
Handle duplicate refund notifications idempotently. Receiving the same notification twice must not result in issuing a double refund.
Next steps
- Review the Building a secure integration checklist
- See the One API reference for the full refund endpoint specifications
- Need to issue refunds without writing code? See Refunds: No-code
In the API reference
8 entries
- POST
/tokenGenerate Authentication Token One API - POST
/transactions/{id}/refundsRequest Refund One API - GET
/transactions/{id}/refundsGet Refunds One API - POST
/refundsRequest Refunds One API - GET
/refundsList Refunds One API - GET
/refunds/{id}Get Refund One API - POST
/refunds/{id}/cancelCancel Refund One API - POST Ozow sends your notification URLRefund completed One API
Last updated