# Refund a payment

> Issue a refund on the Payments API, the legacy path. Refunds are merchant-initiated backend operations, funded from your float balance.

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

> ⚠️ **Legacy integration**: The Payments API is a legacy integration path. For new integrations,
> use [Refunds: One API](https://hub.ozow.com/integration-methods/apis/refunds/refund-a-payment.md) instead. This guide is for merchants
> already using the Payments API.

This guide walks you through issuing refunds to your customers using the Payments API. Refunds are
merchant-initiated backend operations, there is no customer-facing step. The entire flow happens in
your backend.

> ℹ️ **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 your API key and site code from your [Ozow Dashboard](https://dash.ozow.com)
- Your float balance is sufficient to cover the refund amounts
- You have the transaction IDs of the original payments you want to refund
- If you use `notifyUrl` to receive refund status updates, it is publicly accessible via HTTPS

## Environments

| Environment | Base URL | Dashboard |
|---|---|---|
| Production | `https://api.ozow.com` | [dash.ozow.com](https://dash.ozow.com) |
| Staging | `https://stagingapi.ozow.com` | [stagingdash.ozow.com](https://stagingdash.ozow.com) |

---

## How refunds work

The Payments API processes refunds in batches. Even when you are refunding a single transaction, you
submit it as an array containing one refund request. Each refund item in the array requires its own
hash check.

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

    M->>O: POST /token
    O-->>M: Returns a bearer token
    M->>M: Generate a hash check per refund item
    M->>O: POST /secure/refunds/submit
    O-->>M: Returns a refundId per item
    O->>B: Processes the refund to the original bank account
    O-->>M: Sends a notification to notifyUrl
    M->>M: Verifies the notification hash
    M->>M: Updates the refund status
```

---

## Step 1: Get a bearer token

Refund endpoints authenticate with a bearer token rather than the API key directly. Request one
before submitting refunds.

```endpoint
POST https://api.ozow.com/token
ApiKey: YOUR_API_KEY
Content-Type: application/x-www-form-urlencoded
```

**cURL**

```bash
curl -X POST "https://api.ozow.com/token" \
  -H "ApiKey: <YOUR_API_KEY>" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=Password&SiteCode=YOUR_SITE_CODE"
```

**C#**

```csharp
var client = new HttpClient();
client.DefaultRequestHeaders.Add("ApiKey", "YOUR_API_KEY");

var content = new FormUrlEncodedContent(
    new[]
    {
        new KeyValuePair<string, string>("grant_type", "Password"),
        new KeyValuePair<string, string>("SiteCode", "YOUR_SITE_CODE"),
    }
);
var response = await client.PostAsync("https://api.ozow.com/token", content);
var result = await response.Content.ReadAsStringAsync();
```

**PHP**

```php
<?php
$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_URL => "https://api.ozow.com/token",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        "grant_type" => "Password",
        "SiteCode" => "YOUR_SITE_CODE",
    ]),
    CURLOPT_HTTPHEADER => [
        "ApiKey: YOUR_API_KEY",
        "Content-Type: application/x-www-form-urlencoded",
    ],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
?>
```

**JavaScript**

```javascript
const response = await fetch("https://api.ozow.com/token", {
  method: "POST",
  headers: {
    "ApiKey": "YOUR_API_KEY",
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    grant_type: "Password",
    SiteCode: "YOUR_SITE_CODE",
  }),
});
const data = await response.json();
```

**Python**

```python
import requests

response = requests.post(
    "https://api.ozow.com/token",
    headers={"ApiKey": "YOUR_API_KEY"},
    data={"grant_type": "Password", "SiteCode": "YOUR_SITE_CODE"},
)
data = response.json()
```

**Successful response**

```json
{
  "access_token": "YOUR_ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": "14400"
}
```

Store the `access_token` and request a new one before it expires. Include it in the `Authorization`
header of every refund request:

```http
Authorization: Bearer YOUR_ACCESS_TOKEN
```

---

## Step 2: Generate the hash check

Each refund item in the batch requires its own SHA512 hash check.

> ⚠️ **Critical, field order matters**: Concatenate the fields in exactly the order shown below.
> Using the wrong order results in a hash check failure and the refund is rejected.

**Hash field concatenation order**

| Position | Field |
|---|---|
| 1 | `transactionId` |
| 2 | `amount`, formatted with two decimal places, for example `100.00` |
| 3 | `refundReason` |
| 4 | `notifyUrl` |

**Steps**

1. Concatenate the fields above in order
2. Append your private key to the concatenated string
3. Generate a SHA512 hash of the result and send the digest as lowercase hexadecimal

**C#**

```csharp
using System.Security.Cryptography;
using System.Text;

var transactionId = "00000000-0000-0000-0000-000000000000";
var amount = "50.00";
var refundReason = "Order cancellation";
var notifyUrl = "https://yourstore.com/notify";
var privateKey = "YOUR_PRIVATE_KEY";

var inputString = string.Concat(transactionId, amount, refundReason, notifyUrl, privateKey);

using SHA512 sha = SHA512.Create();
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(inputString));
var hashCheck = string.Concat(bytes.Select(b => b.ToString("x2")));
Console.WriteLine($"hashCheck: {hashCheck}");
```

**PHP**

```php
<?php
$inputString =
    $transactionId . $amount . $refundReason . $notifyUrl . $privateKey;

$hashCheck = hash("sha512", $inputString);
echo "hashCheck: " . $hashCheck;
?>
```

**JavaScript**

```javascript
const crypto = require("crypto");

const inputString =
  transactionId + amount + refundReason + notifyUrl + privateKey;

const hashCheck = crypto.createHash("sha512").update(inputString).digest("hex");
console.log("hashCheck:", hashCheck);
```

**Python**

```python
import hashlib

input_string = transaction_id + amount + refund_reason + notify_url + private_key

hash_check = hashlib.sha512(input_string.encode()).hexdigest()
print("hashCheck:", hash_check)
```

---

## Step 3: Submit the refund request

```endpoint
POST https://api.ozow.com/secure/refunds/submit
Authorization: Bearer YOUR_ACCESS_TOKEN
Accept: application/json
Content-Type: application/json
```

Submit every refund you want to process in a single request, as an array. Each item includes its own
hash check from Step 2.

**Request example**

```json
[
  {
    "transactionId": "00000000-0000-0000-0000-000000000000",
    "amount": 50.00,
    "refundReason": "Order cancellation",
    "notifyUrl": "https://yourstore.com/notify",
    "hashCheck": "YOUR_GENERATED_HASH"
  },
  {
    "transactionId": "00000000-0000-0000-0000-000000000001",
    "amount": 100.00,
    "refundReason": "Duplicate charge",
    "notifyUrl": "https://yourstore.com/notify",
    "hashCheck": "YOUR_GENERATED_HASH"
  }
]
```

**Request fields**

| Field | Type | Required | Description |
|---|---|---|---|
| `transactionId` | string | Yes | The Ozow transaction ID of the original payment |
| `amount` | number | Yes | Amount to refund. Must not exceed the original transaction amount |
| `refundReason` | string | No | Reason for the refund |
| `notifyUrl` | string | No | URL Ozow posts the refund notification to |
| `isRtc` | boolean | No | Whether the refund is processed as an RTC refund. Defaults to `false` |
| `hashCheck` | string | Yes | SHA512 hash generated in Step 2 |

For the full field reference see [Submit refund](https://hub.ozow.com/api-reference/payments-api/post-secure-refunds-submit.md).

**Successful response**

```json
[
  {
    "refundId": "00000000-0000-0000-0000-000000000000",
    "transactionId": "00000000-0000-0000-0000-000000000000",
    "refundAmount": "50.00",
    "errors": null
  },
  {
    "refundId": null,
    "transactionId": "00000000-0000-0000-0000-000000000001",
    "refundAmount": "100.00",
    "errors": ["Hash check invalid"]
  }
]
```

> ℹ️ **Note**: The API returns a response for each refund item in the array. Check the `errors`
> field for each item, a `null` value means the refund was accepted. A non-null value means the
> refund was rejected and includes the reason.

Store the `refundId` for each accepted refund, you can use it to check the status later.

---

## Step 4: Handle the notification

Ozow sends a notification to your `notifyUrl` when a refund either completes or fails.

**Notification fields**

| Field | Description |
|---|---|
| `refundId` | The refund identifier |
| `transactionId` | The transaction identifier of the payment that was refunded |
| `currencyCode` | The refund currency |
| `amount` | The refund amount |
| `status` | The refund status. See [Notification statuses](#notification-statuses) |
| `bankName` | The name of the bank the refund was paid to |
| `accountNumber` | The masked account number the refund was paid to |
| `statusMessage` | Message about the refund status. Not always present |
| `isRtc` | Whether RTC was used to pay the refund |
| `hash` | SHA512 hash used to verify the notification |

**Verifying the notification hash**

1. Concatenate the fields in this order: `refundId`, `transactionId`, `currencyCode`, `amount`,
   `status`, `bankName`, `accountNumber`, `statusMessage`, excluding `isRtc` and `hash`
2. Append your private key
3. Lowercase the entire string
4. Generate a SHA512 hash and compare it to the `hash` field received

> ⚠️ **Important**: Always verify the notification hash before updating your records. Never update
> refund status without first verifying the hash.

---

## Refund statuses

Two different endpoints represent refund status differently.

### Refund resource status

Returned by `getrefund`, `getrefunds`, and `getrefundsbytransactionid`:

| Status | Description |
|---|---|
| `0` | Pending, the refund request has been submitted and accepted |
| `1` | Complete, the refund has been paid successfully |
| `2` | Submitted, the refund has been assigned to a batch and is being processed |
| `3` | Failed, the refund payment has failed |
| `4` | Cancelled, the refund was cancelled before it was submitted |
| `5` | Returned, the refund payment was returned because the destination account no longer exists |

### Notification statuses

Sent in the `status` field of the `notifyUrl` notification:

| 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 |
| `PendingInvestigation` | The refund is being investigated |
| `Invalid` | The refund notification could not be validated |
| `Error` | The refund could not be processed due to an error |

---

## Querying refunds

### Get refund by ID

```endpoint
GET https://api.ozow.com/secure/refunds/getrefund?refundId={refundId}
Authorization: Bearer YOUR_ACCESS_TOKEN
```

### Get refunds by transaction ID

Retrieve every refund issued against a specific original payment transaction:

```endpoint
GET https://api.ozow.com/secure/refunds/getrefundsbytransactionid?transactionId={transactionId}
Authorization: Bearer YOUR_ACCESS_TOKEN
```

### Get refunds by date and status

```endpoint
GET https://api.ozow.com/secure/refunds/getrefunds?refundDate={refundDate}&status={status}
Authorization: Bearer YOUR_ACCESS_TOKEN
```

**Query parameters**

| Parameter | Type | Required | Description |
|---|---|---|---|
| `refundDate` | string | Yes | Date of refunds to return. See [Get refunds](https://hub.ozow.com/api-reference/payments-api/get-secure-refunds-getrefunds.md) for the exact accepted format |
| `status` | string | Yes | Refund status code to filter by, from the [Refund resource status](#refund-resource-status) table |

---

## Next steps

- Review the [Building a secure
  integration](https://hub.ozow.com/getting-started/building-a-secure-integration.md) checklist
- See the [Payments API reference](https://hub.ozow.com/api-reference/payments-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)
- Considering migrating to One API? See [Refunds: One API](https://hub.ozow.com/integration-methods/apis/refunds/refund-a-payment.md)