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

Source: https://hub.ozow.com/integration-methods/apis/refunds/refund-a-payment/

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.com` and builds a SHA512 hash, you're
> on the Payments API: see [Refund a payment](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/refund-a-payment.md) under
> Legacy integrations, or [Migrating to One API](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/migrating-to-one-api.md).

> ℹ️ **Float 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](https://hub.ozow.com/payment-products/settlements-and-float/float-top-up.md) to load funds into your
> float.

## Before you start

- You have a valid One API access token: see [Step 1: Obtain an access
  token](https://hub.ozow.com/integration-methods/apis/payin/redirect-to-ozow.md#step-1-obtain-an-access-token) for the token flow
- Your One API client must have the `refunds` scope, and your token must request it. A token
  issued for `payments` alone 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://one.ozow.com/v1` | [dash.ozow.com](https://dash.ozow.com) |
| Staging | `https://stagingone.ozow.com/v1` | [stagingdash.ozow.com](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.

```mermaid
sequenceDiagram
    participant M as Your system
    participant O as One API
    participant B as Customer bank account

    M->>O: POST /transactions/{id}/refunds or POST /refunds
    O-->>M: Returns the refund resource with status pending
    O->>B: Processes the refund to the original bank account
    O-->>M: Sends a refund.complete webhook
    M->>M: Verifies the webhook signature
    M->>M: Updates the refund status
```

---

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

```endpoint
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 key**: 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**

```bash
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
  }'
```

**C#**

```csharp
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**

```php
<?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;
?>
```

**JavaScript**

```javascript
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();
```

**Python**

```python
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.currency` | string | Yes | Must be `ZAR` |
| `amount.value` | number | Yes | Amount to refund. Must not exceed the original transaction amount |
| `reason` | string | Yes | Reason for the refund |
| `realTimePayment` | boolean | Yes | Whether the refund pays out in real time. See [Ozow Pricing](https://ozow.com/pricing) for the cost of real-time refunds. Defaults to `false` |
| `notifyUrl` | string | No | URL to notify of the refund status. Use webhooks instead: see [Handling the refund outcome](#handling-the-refund-outcome) |

For the full list of request fields see [Request Refund](https://hub.ozow.com/api-reference/one-api/post-transactions-id-refunds.md).

**Successful response**

```json
{
  "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:

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

```endpoint
POST https://one.ozow.com/v1/refunds
Authorization: Bearer YOUR_ACCESS_TOKEN
Idempotency-Key: YOUR_UNIQUE_KEY
Content-Type: application/json
```

**Request example**

```json
[
  {
    "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 |
|---|---|---|---|
| `transactionId` | string | Yes | The transaction ID of the payment being refunded |
| `amount.currency` | string | Yes | Must be `ZAR` |
| `amount.value` | number | Yes | Amount to refund. Must not exceed the original transaction amount |
| `reason` | string | Yes | Reason for the refund |
| `realTimePayment` | boolean | Yes | Whether the refund pays out in real time. Defaults to `false` |
| `notifyUrl` | string | No | URL to notify of the refund status. Use webhooks instead |

For the full list of request fields see [Request Refunds](https://hub.ozow.com/api-reference/one-api/post-refunds.md).

**Successful response**

Ozow returns a `Refund` resource per item submitted, in the same shape shown above.

---

## Check a refund's status

```endpoint
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.

```endpoint
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**

```json
{
  "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.

```endpoint
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 |
|---|---|---|---|
| `fromDate` | string | Yes | Start of date range, ISO 8601 date-time |
| `toDate` | 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 |
| `siteCode` | string | No | Filter to a single site code |

**Successful response**

```json
{
  "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` webhook 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](https://hub.ozow.com/integration-methods/apis/payin/redirect-to-ozow.md#step-4-handle-the-webhook-notification) in the One API
redirect 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](https://hub.ozow.com/getting-started/building-a-secure-integration.md) checklist
- See the [One API reference](https://hub.ozow.com/api-reference/one-api.md) for the full refund endpoint specifications
- Need to issue refunds without writing code? See [Refunds: No-code](https://hub.ozow.com/integration-methods/no-code/refunds.md)