# Send a payout

> Everything needed to pay money out to a customer's bank account or voucher with the Payouts API, including how to exercise the failure paths before going live.

Paying out is not a payin in reverse. The money leaves a float balance you have
topped up in advance, a request above that balance fails validation rather than
queueing, and the outcome arrives on a notification rather than in the response
to your call.

Decide first whether you are paying to a bank account or a voucher: the fields
differ. Then work through the mock test cases, which are the only way to
exercise a decryption failure or an insufficient float before a real one
happens.

**Payout statuses reuse names that mean something different from payin
statuses.** Handle every payout status the statuses page lists.

## What this was built from

- Ozow Hub, commit `e0b2a572`
- `payouts-api` version 1.0, OpenAPI document: https://hub.ozow.com/api-reference/specs/payouts-api.yaml
- Build against `https://payoutsapi.ozow.com/v1` for `payouts-api`
- 9 pages, 5 operations, inlined in full below
- The same package as links: https://hub.ozow.com/bundles/send-a-payout.md

---

# Implement against these

Every field name, order and format below is exact. Copy them as written.

---

# Send a payout

> Transfer funds to a recipient's bank account with the Payouts API. There is no customer-facing step, so the whole integration lives in your backend.

Source: https://hub.ozow.com/integration-methods/apis/payout/send-a-payout/

This guide walks you through integrating Ozow payouts using the Payouts API. Payouts are
merchant-initiated transfers to a recipient's bank account. There is no customer-facing payment
step, the entire integration lives in your backend.

> ℹ️ This guide uses the **Payouts API**, the current API for all payouts. Payouts are separate
> from payins and have their own API: the One API you use to accept payments doesn't handle payouts.

## Before you start

> 🚨 **Payout integration requires explicit Ozow approval before you can begin.** You must be
> approved by Ozow's onboarding team before starting a payout integration. Payout credentials are
> not issued until approval is in place. To request approval, contact your account manager or
> [support@ozow.com](mailto:support@ozow.com).

> 🚨 **Staging testing is mandatory before production access is granted.** Unlike payins, payouts
> cannot be reversed once processed; if funds are sent to the wrong account or with incorrect
> details, they cannot be recovered. Completing the mandatory test cases in the staging environment
> protects you as a merchant by ensuring your integration handles all scenarios correctly before
> real funds are involved. Production access will not be granted until Ozow has reviewed and signed
> off your test results. See [Payout test cases](https://hub.ozow.com/integration-methods/testing/payout-test-cases.md) for the full
> requirements.

Once approved, ensure you have the following in place:

- Your Payout API key and site code, provided by Ozow after approval. The Payout API key is specific
  to payout integrations and is different from your standard API key. It is not available by
  default.
- Your verification webhook endpoint is set up and publicly accessible via HTTPS
- Your notification URL is set up and publicly accessible via HTTPS
- Your Ozow float is funded, Ozow uses your float balance to process payouts. See [Float top-up
  guide](https://hub.ozow.com/payment-products/settlements-and-float/float-top-up.md) to load funds into your
  float before going live.

## How payout integration works

```mermaid
sequenceDiagram
    participant M as Your system
    participant O as Payouts API
    participant V as Your verification webhook
    participant B as Recipient bank

    M->>O: GET /getavailablebanks (optional)
    O-->>M: Returns available banks and RTC status
    M->>M: Encrypt account number (AES-256-CBC)
    M->>M: Generate hash check
    M->>O: POST /requestpayout
    O-->>M: Returns payoutId and initial status
    O->>V: POST verification request
    V->>V: Verify hash, validate payout details
    V-->>O: Returns IsVerified + decryption key
    O->>B: Processes payout to recipient bank
    O-->>M: Sends notification to NotifyUrl
    M->>M: Verifies notification hash
    M->>O: GET /getpayout (recommended backup)
```

## Environments

| Environment | Base URL | Mock base URL | Dashboard |
|---|---|---|---|
| Production | `https://payoutsapi.ozow.com/v1` | `https://payoutsapi.ozow.com/mock/v1` | [dash.ozow.com](https://dash.ozow.com) |
| Staging | `https://stagingpayoutsapi.ozow.com/v1` | `https://stagingpayoutsapi.ozow.com/mock/v1` | [stagingdash.ozow.com](https://stagingdash.ozow.com) |

> ℹ️ **Mock API**: The mock API lets you test your integration without processing real payouts.
> Payout requests submitted to mock endpoints are not visible on the Dashboard.

## Authentication

All Payouts API requests require two headers:

| Header | Description |
|---|---|
| `SiteCode` | Your Ozow site code |
| `ApiKey` | Your Payout API key, provided by Ozow after payout approval |

---

## Core integration

### Step 1: Check payout availability (optional but recommended)

Before submitting a payout request, check which banks are available and whether real-time clearing
(RTC) is supported for the destination bank. This lets you show accurate processing times to your
customers.

You do not need to call this endpoint before every payout request. We recommend checking
availability periodically, at least once a week, to stay informed of any changes. Changes to bank
availability are infrequent but do occur, and building this check into a scheduled job rather than a
per-request call is the recommended approach.

```endpoint
GET https://payoutsapi.ozow.com/v1/getavailablebanks
SiteCode: YOUR_SITE_CODE
ApiKey: YOUR_API_KEY
```

Add `?rtconly=true` to return only RTC-enabled banks.

**Response example**

```json
[
  {
    "bankGroupId": "00000000-0000-0000-0000-000000000000",
    "bankGroupName": "ABSA",
    "universalBranchCode": "632005"
  },
  {
    "bankGroupId": "00000000-0000-0000-0000-000000000000",
    "bankGroupName": "Capitec Bank",
    "universalBranchCode": "470010"
  },
  {
    "bankGroupId": "00000000-0000-0000-0000-000000000000",
    "bankGroupName": "FNB",
    "universalBranchCode": "250655"
  },
  {
    "bankGroupId": "00000000-0000-0000-0000-000000000000",
    "bankGroupName": "Nedbank",
    "universalBranchCode": "198765"
  },
  {
    "bankGroupId": "00000000-0000-0000-0000-000000000000",
    "bankGroupName": "Standard Bank",
    "universalBranchCode": "051001"
  }
]
```

Use the `bankGroupId` from this response as the `BankGroupId` in your payout request.

> ℹ️ **Note**: RTC is not available in the staging environment. Set `IsRtc` to `false` for all
> staging tests.

---

### Step 2: Encrypt the account number

> ⚠️ **Security requirement**: Ozow requires that the destination bank account number is encrypted
> before being included in the payout request. Never send a plain text account number.

**AES-256-CBC is the standard and required encryption method for all payout integrations.** This is
the method you must use unless Ozow has specifically approved an alternative for your account.

> ℹ️ **RSA/OAEP-SHA256 encryption**: An alternative RSA-based encryption method is available for
> specific use cases that meet Ozow's security requirements. This method is not available by default
> and requires explicit approval from Ozow before it can be used. If you believe RSA encryption is
> appropriate for your integration, discuss this with your account manager before proceeding. Do not
> implement RSA encryption without prior approval.

#### AES-256-CBC encryption

**Parameters**

- Key size: 256
- Mode: Cipher Block Chaining (CBC)
- Padding: PKCS7

**Generating the IV**

The initialisation vector (IV) is derived from an SHA512 hash of the concatenation of:

1. Merchant reference
2. Amount in cents (e.g. `10000` for R100.00)
3. Your encryption key

Take the first 16 bytes of the SHA512 hash as the IV.

> ⚠️ **Important**: Generate and persist a unique encryption key per payout request. Never reuse an
> encryption key across multiple payout requests.

**C#**

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

var plaintextEncryptionKey = "YOUR_ENCRYPTION_KEY";
var plaintextAccountNumber = "YOUR_ACCOUNT_NUMBER";
var merchantReference = "ORDER-001";
var payoutAmount = 100.00M;

var ivString = string.Concat(
    merchantReference,
    Convert.ToInt32(payoutAmount * 100),
    plaintextEncryptionKey
);

var encryptedAccountNumber = EncryptAes(plaintextAccountNumber, plaintextEncryptionKey, ivString);

string EncryptAes(string data, string key, string ivString)
{
    byte[] dataBytes = Encoding.UTF8.GetBytes(data);
    string iv = GetSha512Hash(ivString.ToLower()).Substring(0, 16);

    while (key.Length < 32)
        key += key;

    using var aes = new AesCryptoServiceProvider();
    aes.Key = Encoding.UTF8.GetBytes(key.Substring(0, 32));
    aes.Mode = CipherMode.CBC;
    aes.IV = Encoding.UTF8.GetBytes(iv);

    var encryptor = aes.CreateEncryptor();
    var encryptedBytes = encryptor.TransformFinalBlock(dataBytes, 0, dataBytes.Length);
    return Convert.ToBase64String(encryptedBytes);
}

string GetSha512Hash(string input)
{
    using SHA512 sha = new SHA512CryptoServiceProvider();
    var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(input));
    var sb = new StringBuilder();
    foreach (var b in bytes)
        sb.Append(b.ToString("x2"));
    return sb.ToString();
}
```

**PHP**

```php
<?php
$plaintextEncryptionKey = "YOUR_ENCRYPTION_KEY";
$plaintextAccountNumber = "YOUR_ACCOUNT_NUMBER";
$merchantReference = "ORDER-001";
$amount = 10000; // Amount in cents

$ivString = strtolower($merchantReference . $amount . $plaintextEncryptionKey);
$ivHash = hash("sha512", $ivString, false);
$iv = substr($ivHash, 0, 16);

$key = $plaintextEncryptionKey;
while (strlen($key) < 32) {
    $key .= $plaintextEncryptionKey;
}
$key = substr($key, 0, 32);

$encrypted = openssl_encrypt(
    $plaintextAccountNumber,
    "AES-256-CBC",
    $key,
    OPENSSL_RAW_DATA,
    $iv,
);
$encryptedAccountNumber = base64_encode($encrypted);
echo "Encrypted: " . $encryptedAccountNumber;
?>
```

**JavaScript**

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

const plaintextEncryptionKey = "YOUR_ENCRYPTION_KEY";
const plaintextAccountNumber = "YOUR_ACCOUNT_NUMBER";
const merchantReference = "ORDER-001";
const payoutAmount = 100.00;

const ivString = (
  merchantReference +
  Math.round(payoutAmount * 100) +
  plaintextEncryptionKey
).toLowerCase();

const iv = crypto
  .createHash("sha512")
  .update(ivString)
  .digest("hex")
  .substring(0, 16);

let key = plaintextEncryptionKey;
while (key.length < 32) key += plaintextEncryptionKey;
key = key.substring(0, 32);

const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
const encrypted = Buffer.concat([
  cipher.update(plaintextAccountNumber, "utf8"),
  cipher.final(),
]);
const encryptedAccountNumber = encrypted.toString("base64");
console.log("Encrypted:", encryptedAccountNumber);
```

**Python**

```python
import hashlib
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

plaintext_encryption_key = "YOUR_ENCRYPTION_KEY"
plaintext_account_number = "YOUR_ACCOUNT_NUMBER"
merchant_reference = "ORDER-001"
payout_amount = 100.00

iv_string = (
    merchant_reference + str(round(payout_amount * 100)) + plaintext_encryption_key
).lower()

iv = hashlib.sha512(iv_string.encode()).hexdigest()[:16]

key = plaintext_encryption_key
while len(key) < 32:
    key += plaintext_encryption_key
key = key[:32].encode("utf-8")

cipher = AES.new(key, AES.MODE_CBC, iv.encode("utf-8"))
encrypted = cipher.encrypt(
    pad(plaintext_account_number.encode("utf-8"), AES.block_size)
)
encrypted_account_number = base64.b64encode(encrypted).decode("utf-8")
print("Encrypted:", encrypted_account_number)
```

---

### Step 3: Generate the request hash

Generate a SHA512 hash to sign the payout request.

> ⚠️ **Critical, field order matters**: Fields must be concatenated in exactly the order shown
> below. Using the wrong order is the most common cause of hash failures. Only include fields that
> have a value, exclude empty fields entirely.

**Hash field concatenation order**

| Position | Field |
|---|---|
| 1 | `siteCode` |
| 2 | `amount` in cents, so `10000` for R100.00 |
| 3 | `merchantReference` |
| 4 | `customerBankReference` |
| 5 | `isRtc` |
| 6 | `notifyUrl` |
| 7 | `bankingDetails.bankGroupId` |
| 8 | `bankingDetails.accountNumber`, encrypted |
| 9 | `bankingDetails.branchCode` |
| 10 | Your API key |

**Steps**

1. Concatenate the fields above in order
2. Convert the entire string to lowercase
3. Generate a SHA512 hash of the lowercase string

> ℹ️ **Note**: Amount must be in cents as an integer. Boolean values must be the strings `true` or
> `false`, not `1` or `0`.

> ⚠️ **Important**: Round the amount to cents, do not truncate it. `1.15 * 100` is
> `114.99999999999999` in binary floating point, so truncating gives `114` where Ozow calculated
> `115`, and the hash is rejected. Use your language's rounding function, or hold the amount in
> cents from the start.

**C#**

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

var siteCode = "YOUR_SITE_CODE";
var amount = 100.00;
var merchantReference = "ORDER-001";
var customerBankReference = "ABC123";
var apiKey = "YOUR_API_KEY";
var isRtc = false;
var notifyUrl = "https://yourstore.com/notify";
var bankGroupId = "YOUR_BANK_GROUP_ID";
var accountNumber = "YOUR_ENCRYPTED_ACCOUNT_NUMBER";
var branchCode = "YOUR_BRANCH_CODE";

var inputString = string.Concat(
        siteCode,
        Convert.ToInt32(amount * 100),
        merchantReference,
        customerBankReference,
        isRtc,
        notifyUrl,
        bankGroupId,
        accountNumber,
        branchCode,
        apiKey
    )
    .ToLower();

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

**PHP**

```php
<?php
$inputString = strtolower(
    $siteCode .
        round($amount * 100) .
        $merchantReference .
        $customerBankReference .
        "false" .
        $notifyUrl .
        $bankGroupId .
        $accountNumber .
        $branchCode .
        $apiKey,
);
$hashCheck = hash("sha512", $inputString);
echo "HashCheck: " . $hashCheck;
?>
```

**JavaScript**

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

const inputString = (
  siteCode +
  Math.round(amount * 100) +
  merchantReference +
  customerBankReference +
  isRtc +
  notifyUrl +
  bankGroupId +
  accountNumber +
  branchCode +
  apiKey
).toLowerCase();

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

**Python**

```python
import hashlib

input_string = (
    site_code
    + str(round(amount * 100))
    + merchant_reference
    + customer_bank_reference
    + str(is_rtc).lower()
    + notify_url
    + bank_group_id
    + account_number
    + branch_code
    + api_key
).lower()

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

---

### Step 4: Submit the payout request

```endpoint
POST https://payoutsapi.ozow.com/v1/requestpayout
SiteCode: YOUR_SITE_CODE
ApiKey: YOUR_API_KEY
Content-Type: application/json
```

**Request example**

```json
{
  "siteCode": "YOUR_SITE_CODE",
  "amount": 100.00,
  "merchantReference": "ORDER-001",
  "customerBankReference": "ABC123",
  "isRtc": false,
  "notifyUrl": "https://yourstore.com/notify",
  "bankingDetails": {
    "bankGroupId": "YOUR_BANK_GROUP_ID",
    "accountNumber": "YOUR_ENCRYPTED_ACCOUNT_NUMBER",
    "branchCode": "YOUR_BRANCH_CODE"
  },
  "hashCheck": "YOUR_GENERATED_HASH"
}
```

**Successful response**

```json
{
  "payoutId": "00000000-0000-0000-0000-000000000000",
  "payoutStatus": {
    "status": 1,
    "subStatus": 201,
    "errorMessage": ""
  }
}
```

`status` is `1`, Payout Received: the request is queued, not paid. Only `5` means the payout
completed. The full list is in [Transaction and settlement statuses](https://hub.ozow.com/integration-methods/statuses.md#payout-statuses).

Store the `payoutId`, you will use it to check payout status in Step 6.

> ⚠️ **Important**: A rejected payout is also an HTTP 200. A failed hash, a reference that is
> too long, an amount above your float balance: all of them come back with status 200, an
> empty `payoutId`, and the reason in `payoutStatus.errorMessage`. Treat a payout as accepted
> only when `payoutId` is populated and `errorMessage` is empty. A status code on its own
> tells you the request arrived, not that the payout was taken.

**Rejected response**

```json
{
  "payoutId": "",
  "payoutStatus": {
    "status": 0,
    "subStatus": 0,
    "errorMessage": "The HashCheck value has failed"
  }
}
```

The API returns a status code other than 200 only when the request does not reach the payouts
service at all: `400` when a required header is missing or the body is malformed, `403` when
the `SiteCode` and `ApiKey` pair is not accepted, and `500` when something fails on the Ozow
side. Those carry a `message` field rather than a `payoutStatus`.

---

### Step 5: Implement the verification webhook

Ozow calls your verification webhook to verify each payout request and obtain the AES decryption key
for the encrypted account number. Your webhook must respond exactly as specified in this guide, any
deviation from the required response format will cause verification to fail and the payout will be
cancelled.

Your webhook must:

1. Validate the incoming request using bearer token authentication
2. Verify the webhook hash
3. Validate that the payout details match a payout your system actually initiated
4. Return the decryption key and verification status in the exact format specified below

**Webhook authentication**

Ozow passes your access token in the `AccessToken` header of the verification request. Validate this
token before processing.

**Verifying the webhook hash**

Generate a SHA512 hash using the fields below in this exact order:

| Position | Field |
|---|---|
| 1 | `PayoutId` |
| 2 | `SiteCode` |
| 3 | `Amount` in cents |
| 4 | `MerchantReference` |
| 5 | `CustomerBankReference` |
| 6 | `IsRtc` |
| 7 | `NotifyUrl` |
| 8 | `BankGroupId` |
| 9 | `AccountNumber` (encrypted) |
| 10 | `BranchCode` |
| 11 | `ApiKey` |

Compare your generated hash to the `HashCheck` field in the verification request.

> ⚠️ **Security**: Always validate the access token AND the hash before returning the decryption
> key. Also confirm that the `PayoutId` matches a payout your system initiated: do not return the
> decryption key based on token or hash checks alone.

**Verification response**

Your webhook must return HTTP 200 with the following JSON:

```json
{
  "payoutId": "00000000-0000-0000-0000-000000000000",
  "isVerified": true,
  "accountNumberDecryptionKey": "YOUR_ENCRYPTION_KEY",
  "reason": ""
}
```

If verification fails, set `isVerified` to `false` and provide a reason. Ozow will send a payout
cancelled notification if it cannot decrypt the account number with the key provided.

---

### Step 6: Handle the notification response

Ozow posts a notification to your `NotifyUrl` when the payout completes.

**Verifying the notification hash**

1. Concatenate the notification fields (excluding `hashCheck`) in the order below
2. Append your API key
3. Convert to lowercase
4. Generate a SHA512 hash
5. Compare to the `hashCheck` value in the notification

**Notification hash field order**

| Position | Field |
|---|---|
| 1 | `payoutId` |
| 2 | `siteCode` |
| 3 | `merchantReference` |
| 4 | `customerMerchantReference` |
| 5 | `payoutStatus.status`, as its integer value |
| 6 | `payoutStatus.subStatus`, as its integer value |
| 7 | Your API key |

> ⚠️ **Important**: The two status fields go into the hash as integers, not as their
> names. Concatenate `3`, not `Complete`.

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

> ℹ️ **Note**: Ozow may occasionally send duplicate notifications for the same payout. Handle these
> idempotently, receiving the same notification twice must not result in double-crediting or
> double-debiting.

---

### Step 7: Confirm payout status via API (recommended)

In addition to receiving notifications, implement the status check endpoint as a backup mechanism.
Webhook delivery can be affected by network issues, endpoint downtime, or platform instability.

Your system must treat the initial payout request as pending and, if no notification is received
within your defined SLA (typically 2-5 minutes): call the status endpoint using the `payoutId`
returned in Step 4.

**By payout ID**

```endpoint
GET https://payoutsapi.ozow.com/v1/getpayout?payoutId={payoutId}
SiteCode: YOUR_SITE_CODE
ApiKey: YOUR_API_KEY
```

**By merchant reference**

```endpoint
POST https://payoutsapi.ozow.com/v1/getpayoutbyreference
SiteCode: YOUR_SITE_CODE
ApiKey: YOUR_API_KEY
```

```json
{
  "pageSize": 10,
  "pageIndex": 1,
  "searchFields": [1],
  "searchString": "YOUR_MERCHANT_REFERENCE"
}
```

**Response example**

```json
{
  "id": "00000000-0000-0000-0000-000000000000",
  "amount": 100.00,
  "merchantReference": "ORDER-001",
  "customerBankReference": "ABC123",
  "siteCode": "YOUR_SITE_CODE",
  "isRtc": false,
  "payoutStatus": {
    "status": 5,
    "subStatus": 0,
    "errorMessage": "Complete"
  }
}
```

---

## Payout outcome flow

```mermaid
flowchart LR
    A[Submit payout request] --> B{Receive notification}
    B -->|Within SLA| C{Verify notification hash}
    B -->|No notification - SLA exceeded| D[Call getpayout API]
    C -->|Invalid| E[Log and alert - do not process]
    C -->|Valid| F{Check payout status}
    D --> F
    F -->|Complete| G[Update ledger and confirm payout]
    F -->|Cancelled| H[Investigate and resubmit if appropriate]
    F -->|Failed| I[Check sub-status and take action]
    F -->|Pending| J[Continue polling with backoff]
```

---

## Next steps

- Complete mandatory payout testing: see [Payout test cases](https://hub.ozow.com/integration-methods/testing/payout-test-cases.md)
- Review the [Building a secure
  integration](https://hub.ozow.com/getting-started/building-a-secure-integration.md) checklist before going
  live
- See the [Payouts API reference](https://hub.ozow.com/api-reference/payouts-api.md) for the full technical specification

---

# Payout test cases

> The mandatory tests for a payout integration, for bulk payouts from the Dashboard and for the Payouts API. You must pass these before going live.

Source: https://hub.ozow.com/integration-methods/testing/payout-test-cases/

Payout test cases are mandatory. You must complete all relevant tests below and receive sign-off
from Ozow's integration team before your payout integration can go live in production.

This page covers test cases for two integration paths:

- **Bulk payouts**: no-code payout submission via the Ozow Dashboard
- **API payouts**: programmatic payout integration via the Payouts API, covering both standard API
  tests and mock API simulation tests

Complete only the test cases relevant to your integration. If you are integrating both, you must
complete the test cases for each separately.

## How the process works

1. Complete all relevant test cases in the staging environment
2. Submit your test evidence via the link provided by your Ozow integration team contact
3. Ozow reviews your submission internally
4. If approved, Ozow collects your production configuration details and sets up your production environment
5. You receive confirmation that your integration is ready for go-live

> ℹ️ **Postman collection**: A Postman collection for the Payouts API is available in the [Payouts
> API reference](https://hub.ozow.com/api-reference/payouts-api.md). Download it to run these test cases directly in Postman.

## Important notes before testing

- All tests must be completed in the staging environment, no real funds are required and no real
  transactions take place in staging
- Your staging float will be set up with test funds by Ozow's integration team as part of the
  staging environment setup
- RTC payments are not available in staging: set `IsRtc` to `false` for all tests

---

## Bulk payout test cases

If you are integrating bulk payouts via the Dashboard, you must complete the following test in the
staging environment before bulk payouts can be enabled in production.

> ℹ️ **Note**: Bulk payout testing is separate from API payout testing. If you are integrating both,
> you must complete the relevant test cases for each.

### Bulk payout test 1: Successful bulk payout submission

Verify that you can successfully submit and complete a bulk payout via the Dashboard.

**Steps**

1. Log in to your staging Dashboard at [stagingdash.ozow.com](https://stagingdash.ozow.com)
2. Navigate to **Payouts** → **Bulk Payouts**
3. Download the CSV template and the Available Banks file
4. Complete the template with at least one valid payout
5. Upload the completed template
6. Approve the batch using an account with the bulk payout approver role
7. Confirm that at least one payout in the batch completes successfully

**Evidence required**

- Screenshot or URL of the completed batch on the Bulk Payouts page showing at least one successful
  payout

---

## Standard API test cases

Run these tests against the standard staging endpoint, `https://stagingpayoutsapi.ozow.com/v1/requestpayout`.

### Test 1: Request payout below minimum amount

Verify that your integration correctly handles a payout request below the minimum amount of R1.

**Steps**

1. Submit a payout request for an amount less than R1.00
2. Capture the JSON response from the API

**Evidence required**

- JSON response from the API

> ℹ️ **Note**: This validation does not show on the Ozow Dashboard. The JSON API response is the
> only evidence required for this test.

---

### Test 2: Request payout above maximum amount

Verify that your integration correctly handles a payout request above the maximum amount of R20.

**Steps**

1. Submit a payout request for an amount greater than R20.00
2. Capture the JSON response from the API

**Evidence required**

- JSON response from the API

> ℹ️ **Note**: This validation does not show on the Ozow Dashboard. The JSON API response is the
> only evidence required for this test.

---

### Test 3: Receive verification request and respond successfully

Verify that your verification webhook receives the request from Ozow and responds correctly.

**Steps**

1. Submit a valid payout request
2. Confirm that your verification webhook receives the verification request from Ozow
3. Respond to the verification request correctly with `IsVerified: true` and the decryption key
4. Confirm that a verification success timestamp appears on the payout details in the Dashboard

**Evidence required**

- URL of the payout from the Dashboard showing the verification success timestamp

---

### Test 4: Receive payout verification success message

Verify that your notification URL receives the verification success message from Ozow.

**Steps**

1. This test occurs automatically on successful completion of Test 3
2. Confirm that your notification URL receives a `verificationSuccess` message
3. Confirm this is visible under "Payout responses" on the payout details in the Dashboard

**Evidence required**

- URL of the payout from the Dashboard showing the `verificationSuccess` message sent to your
  notification URL

---

### Test 5: Receive payout complete message

Verify that your integration correctly handles a successfully completed payout.

**Steps**

1. Submit a valid payout request and complete the verification flow
2. Confirm that your notification URL receives a payout complete notification
3. Confirm the payout shows as successful on the Dashboard

**Evidence required**

- URL of the payout from the Dashboard showing a successful payout and notification to your
  notification URL

---

### Test 6: Receive payout cancelled message

Verify that your integration correctly handles a cancelled payout.

**Steps**

1. Submit a payout request that results in a cancellation
2. Confirm that your notification URL receives a payout cancelled notification
3. Confirm the cancellation is visible on the Dashboard

**Evidence required**

- URL of the payout from the Dashboard showing the cancellation

---

### Test 7: Receive low float balance message

Verify that your low float balance alert is working correctly.

**Steps**

1. Your low float balance alert is triggered when your float balance reaches R99.00 in staging
2. Confirm that the alert email is received by the user configured for float balance alerts

**Evidence required**

- Screenshot of the low float balance alert email received

---

### Test 8: CDV error, account number validation error

Verify that your integration correctly handles an account number validation error.

**Steps**

1. Submit a payout request using account number `1234567890` to trigger a CDV error
2. Confirm the error is visible on the Dashboard

**Evidence required**

- URL of the payout from the Dashboard showing the CDV error

---

### Test 9: Get payout status

Verify that you can successfully retrieve payout status via the API.

**Steps**

1. Submit a valid payout request and note the `payoutId`
2. Call the `getPayout` endpoint using the `payoutId`
3. Capture the JSON response from the API

**Evidence required**

- JSON response from the API

---

## Mock API test cases

Run these tests against the mock staging endpoint, `https://stagingpayoutsapi.ozow.com/mock/v1`, to
simulate specific failure scenarios.

> ℹ️ **Note**: Payout requests submitted to mock endpoints are not visible on the Dashboard. The
> JSON API response is the only evidence required for these tests.

For each of the three mock scenarios below, follow these steps:

**Step 1: Get current test configuration**

```http
GET https://stagingpayoutsapi.ozow.com/mock/v1/gettestconfiguration?siteCode={siteCode}
```

**Step 2: Set test configuration**

```http
POST https://stagingpayoutsapi.ozow.com/mock/v1/settestconfiguration
```

Set only the relevant field to `true` for the scenario you are testing. All other fields must be `false`.

**Step 3: Verify configuration**

Call `getTestConfiguration` again to confirm the configuration was set correctly before proceeding.

**Step 4, Submit mock payout request**

Submit a payout request to the mock endpoint to trigger the configured simulation response.

**Step 5, Get mock payout status**

Call `getMockPayout` to retrieve the result and capture the JSON response.

> ⚠️ **Important**: Repeat all five steps for each simulation scenario. Set exactly one field to
> `true` at a time. Reset the configuration between each test.

---

### Mock test 1: Account decryption failed

Set `IsAccountDecryptionFailed: true` in the test configuration and complete the five steps above.

**Evidence required**

- JSON response from the mock payout request
- JSON response from getMockPayout

---

### Mock test 2: Not verified response

Set `IsNotVerifiedResponse: true` in the test configuration and complete the five steps above.

**Evidence required**

- JSON response from the mock payout request
- JSON response from getMockPayout

---

### Mock test 3: Account decryption key missing

Set `IsAccountDecryptionKeyMissing: true` in the test configuration and complete the five steps above.

**Evidence required**

- JSON response from the mock payout request
- JSON response from getMockPayout

---

# Background

Context for the above. Nothing here is implemented against.

---

# How Ozow works

> How Ozow connects you to South African banks and payment methods, and the two directions money moves: payins from customers, payouts to recipients.

Source: https://hub.ozow.com/getting-started/

Ozow is a payment infrastructure layer that connects merchants to multiple payment methods and
banking rails. Instead of building separate integrations for each bank or payment method, you
integrate with Ozow to gain access to the full suite of Ozow payment products.

## The two directions of money movement

Every Ozow integration moves money in one of two directions.

**Payin**: a customer pays you. The customer initiates the payment, Ozow processes it, and you
receive the funds. This covers checkout and any other payment collection.

**Payout**: you send funds to a recipient. Your system initiates the transfer, Ozow processes it,
and the recipient receives the funds in their bank account. This covers disbursements, refunds to
bank accounts, and bulk payments.

The distinction runs through everything: different APIs, different credentials, different approval
processes, and a different structure in these docs. Work out which direction you need before you
start. The [Integration methods](https://hub.ozow.com/integration-methods.md) section is
organised around it.

## One integration, every way to pay

Payment methods are enabled on your Ozow account, not in your code.

Pay by Bank is enabled by default. Other methods you opt into (card, PayShap Request, voucher, buy
now pay later, crypto) are enabled by Ozow on your account, and they then appear on the Ozow payment
page automatically. You don't build a new integration or call a different endpoint for each one.

This means you can go live with Pay by Bank and add methods later as a commercial decision rather
than a development project.

## The core payment flows

### Payin

```mermaid
sequenceDiagram
    participant C as Customer
    participant M as Your system
    participant O as Ozow

    C->>M: Reaches checkout
    M->>O: Creates payment request
    O-->>M: Returns payment URL
    M->>C: Sends customer to Ozow
    C->>O: Completes payment
    O-->>M: Notifies your webhook of the outcome
    M->>O: Verifies the status
    M-->>C: Updates the order
```

Two things to notice. The payment request is created by **your server**, never by the customer's
browser. And the outcome arrives on **your webhook**, not in the customer's redirect back to your
site; the customer landing on your success page is not proof of payment. See [Building a secure
integration](https://hub.ozow.com/getting-started/building-a-secure-integration.md).

### Payout

There is no customer-facing step. The whole flow happens between your backend and Ozow.

```mermaid
sequenceDiagram
    participant M as Your system
    participant O as Ozow
    participant R as Recipient bank

    M->>O: Checks payout availability
    M->>O: Sends payout request
    O->>M: Calls your verification webhook
    M-->>O: Confirms the payout
    O->>R: Submits the payout to the bank
    R-->>O: Confirms the outcome
    O-->>M: Notifies your webhook of the final status
```

Before Ozow moves any money, it calls back to your system to confirm the payout is genuine.
If that call fails or can't be reached, the payout does not proceed. That's deliberate, and
it's why payout integrations require testing and sign-off before they go live.

## Getting paid: transactions and settlements

A completed transaction is not money in your bank account. These are two separate stages with two
separate status vocabularies.

```mermaid
flowchart LR
    A["Customer pays"] --> B["Transaction completes"]
    B --> C["Included in a settlement"]
    C --> D["Funds in your bank account"]
```

The transaction status tells you whether the customer's payment succeeded. The settlement status
tells you whether the money has actually reached you. Settlement happens on a delay that depends on
the payment method.

Use transaction status to fulfil orders. Use settlement status to reconcile your bank account. See
[Transaction and settlement statuses](https://hub.ozow.com/integration-methods/statuses.md).

## Paying out: your float

Money leaving Ozow doesn't come out of your incoming payments. It comes from a **float**: a balance
you pre-fund by transferring money to Ozow.

Both payouts and refunds draw on the float. If it's empty, they won't process. Payins don't need a
float at all, so if you're only collecting payments you can ignore this entirely.

See [Float top-up](https://hub.ozow.com/payment-products/settlements-and-float/float-top-up.md).

## Environments

Ozow provides separate staging and production environments. They're completely isolated, and staging
credentials are different from your production credentials.

Testing requirements differ by direction. Payin integrations can go straight to production. We
recommend working through the [payin test
cases](https://hub.ozow.com/integration-methods/testing/payin-test-cases-one-api.md), but you don't need to submit anything.
Payout integrations require mandatory staging testing and formal sign-off from Ozow before they're
enabled in production.

**Getting your credentials:**

- **Production credentials** are available to you directly in the [Ozow
  Dashboard](https://dash.ozow.com). Ozow will never send them to you.
- **Staging credentials** are issued on request. Ask your account manager or contact [support@ozow.com](mailto:support@ozow.com).

> 🚨 Ozow will never share your production credentials with you, and will never ask you for them. If
> anyone contacts you offering to send production credentials, or asking you to share yours, treat
> it as fraudulent and report it to [support@ozow.com](mailto:support@ozow.com).

## Where to go next

How you integrate depends on how much control you want over the payment experience and how much you
want to build, from no-code payment requests through to a full API integration.

Head to [Integration methods: overview](https://hub.ozow.com/integration-methods.md) to choose
the right path.

---

# Prerequisites and onboarding

> What to have in place before you write any code: a merchant account, Dashboard access, your credentials, and payout eligibility if you need it.

Source: https://hub.ozow.com/getting-started/prerequisites-and-onboarding/

Before you start integrating Ozow, make sure you have everything in place. This page covers what you
need before writing a single line of code.

## 1. Register as an Ozow merchant

You need an active Ozow merchant account before you can integrate. If you don't have one yet,
[join our merchant family](https://ozow.com/merchants) or speak to your account manager to get set up.
 If you signed up through a commercial manager or already have an account, you can skip this step and
 log in to the [Ozow Dashboard](https://dash.ozow.com) directly.

If you need assistance with your account, contact [support@ozow.com](mailto:support@ozow.com) or
reach out to your account manager.

## 2. Access the Ozow Dashboard

Once your merchant account is active, you can log in to your [Ozow Dashboard](https://dash.ozow.com).
The Dashboard is where you'll find everything you need to begin your integration.

## 3. Retrieve your credentials

Which credentials you need depends on the API you are integrating against. Collect the row for
yours and ignore the rest.

| | One API | Payments API | Payouts API |
|---|---|---|---|
| Client ID and Client Secret | **Yes** | No | No |
| API key | No | Yes | Yes, a **different** key |
| Private key | No | Yes, to sign the hash | Yes, to sign the hash |
| Where to find them | One API Clients | Merchant Details and Site | Issued once payouts are approved |

**On One API, the Client ID and Client Secret are all you need.** You exchange them for an access
token. There is no API key to send and no hash to compute.

**On the Payments API and the Payouts API you need both keys, and they do different things.** The
API key goes in the `ApiKey` header. The private key is never sent: you use it to compute the
`hashCheck` field on the request, and again to verify the hash on a notification.

**The Payouts API takes its own API key, not the one the Payments API takes.** Sending the Payments
API key to a payout endpoint is rejected.

Your **site code** identifies which of your sites a request belongs to and is in the Site section of
the Dashboard. Every path needs one. It is not a secret.

> ℹ️ **One API clients and payout API keys are scoped per site or per merchant.** A key issued for
> one site does not work for another, so check which you have been given before you assume it covers
> your whole account.

> ⚠️ **Security note**: Keep your credentials secure at all times. Never expose them in client-side
> code, public repositories, or logs. Ozow does not publish credentials publicly and will never ask
> you to share them in an unsecured channel.

## 4. Understand your project setup

When you log in to the Ozow Dashboard, you'll see your merchant account. Within your account, you
can have one or more sites; each representing a separate website, merchant, or integration point.

Each site has its own unique site code, and your site code and API credentials work together to
identify which site a payment belongs to. Payment requests and transactions are always tied to a
specific site, so it's important to use the correct site code for the integration you're building.

```mermaid
graph TD
    A[Ozow Dashboard] --> B[Site 1\nsite code: ABC-001]
    A --> C[Site 2\nsite code: ABC-002]
    A --> D[Site 3\nsite code: ABC-003]
    B --> E[Payments & transactions\ntied to Site 1]
    C --> F[Payments & transactions\ntied to Site 2]
    D --> G[Payments & transactions\ntied to Site 3]
```

## 5. Integrating payouts? Check your eligibility first

If you intend to integrate payouts, you must be approved by Ozow's onboarding team before you can
begin. Payout credentials are not issued until this approval is in place; you will not be able to
start a payout integration without them.

Contact your account manager or [support@ozow.com](mailto:support@ozow.com) to request payout eligibility.

> ℹ️ **Note**: Payin credentials are issued automatically as part of standard merchant onboarding.
> Payout credentials require a separate approval process before they are issued.

## 6. Choose your integration path

Once your credentials are in place, you're ready to choose how you'll integrate. Head to
[choose your integration](https://hub.ozow.com/integration-methods.md) to understand your
options and choose the right path for your use case.

If you're new to Ozow and want to get to your first payment as quickly as possible, go straight to
the [quick start guide](https://hub.ozow.com/getting-started/quick-start.md).

---

# Payout to bank

> Paying money out from your Ozow float to a bank account.

Source: https://hub.ozow.com/payment-products/payout/payout-to-bank/

Payout to bank sends money from your Ozow float account into someone else's South African bank account.

Unlike a refund, a payout isn't tied to a payment anyone made you. You can pay anyone with a supported
bank account.

Payouts run entirely from your backend or from the Ozow Dashboard. There's no customer-facing step
and nothing for the recipient to do.

> ⚠️ Payouts are not self-service. They need explicit approval from Ozow, mandatory testing in
> staging, and formal sign-off before they're enabled in production. See [Enabling
> payouts](#enabling-payouts).

## How a payout works

1. You submit a payout with the recipient's bank account details and the amount.
2. Ozow confirms the payout is genuinely intended before any money moves, through your verification
   webhook if you're using the API, or an approval if you're uploading from the Dashboard.
3. Ozow submits the payout to the recipient's bank.
4. The funds arrive, immediately if the payout is sent in real time, otherwise within 1-2 business days.
5. Ozow notifies your system of the final status.

There is no customer-facing step. The whole flow happens between your backend and Ozow.

```mermaid
sequenceDiagram
    participant M as Your system
    participant O as Ozow
    participant R as Recipient bank

    M->>O: Checks payout availability
    M->>O: Sends payout request
    O->>M: Calls your verification webhook
    M-->>O: Confirms the payout
    O->>R: Submits the payout to the bank
    R-->>O: Confirms the outcome
    O-->>M: Notifies your webhook of the final status
```

## Enabling payouts

Payouts move money out of your account, so the process to switch them on is deliberately strict.
There are no exceptions to any of the three steps.

**1. Approval.** Speak to your account manager. Ozow reviews your use case before payouts are enabled.

**2. Staging testing.** You must complete the [payout test
cases](https://hub.ozow.com/integration-methods/testing/payout-test-cases.md) in staging. This is mandatory and
evidence is required.

**3. Sign-off.** Ozow signs off on your staging results before payouts are enabled in production.

You'll also need a **funded float** before any payout will process. See [Float top-up](https://hub.ozow.com/payment-products/settlements-and-float/float-top-up.md).

> ⚠️ **Payments from your customers don't fund your float.** When a customer pays you, that money is
> settled to your bank account. Your float is separate, and you fund it yourself by transferring
> money to Ozow. A busy sales day doesn't give you more capacity to refund or pay out; only a top-up
> does.

> ℹ️ Need a different arrangement? Speak to your account manager if you'd like your incoming
> payments to flow into your float rather than being settled to your bank account. Ozow approves
> these at its discretion based on your use case; approval isn't guaranteed.

## Things to know

**Payouts come from your float, not from your incoming payments.** Money customers pay you gets
settled to your bank account; it doesn't top up your payout balance. If your float is empty, payouts
won't process. Keep it funded ahead of when you need it; topping up takes time to clear.

**Timing depends on how the payout is sent.** Real-time payouts arrive immediately. Standard payouts
take 1-2 business days. You choose per payout.

**Every payout is checked before money moves.** How depends on how you send it. On the API, Ozow
calls back to your system to confirm the payout is legitimate; if your webhook rejects it, or Ozow
can't reach it, the payout fails and nothing moves. From the Dashboard, a second person has to
approve the batch before it's released. Either way the check is deliberate: it's what stops a single
compromised credential or one person's mistake from draining your float. If you're on the API, that
means keeping your webhook reachable and making sure Ozow has the current URL if you ever change it.

**Recipient account details are your responsibility.** Ozow validates what it can, but an account
number that's valid and belongs to the wrong person will still be paid. Verify recipient details
before you submit.

**Payouts can be returned.** If the destination account has closed or can't accept the payment, the
payout comes back and the funds return to your float. You'll see this as a returned status rather
than a failure at submission.

**Never blindly resubmit a failed payout.** Most failures are safe to retry once you've fixed the
cause. An insufficient-balance failure is not: top up your float instead, and the payout processes
automatically. Resubmitting that one risks paying the recipient twice. See [Transaction and
settlement statuses](https://hub.ozow.com/integration-methods/statuses.md) for what each status means and what to
do about it.

**Recipient has no bank account?** See [Payout to voucher](https://hub.ozow.com/payment-products/payout/payout-to-voucher.md).

## Integrating payouts

**Bulk payouts from the Dashboard**: upload a CSV, no development required. Uses a two-role approval
model; Ozow recommends that the person who uploads a batch is different from the person who approves
it. Same approval and staging process as the API. → [Bulk
payouts](https://hub.ozow.com/integration-methods/no-code/bulk-payouts.md)

**Payouts API**: submit payouts from your own system, individually or in volume. Covers the
verification webhook, account number encryption, status notifications and the status check API. →
[Send a payout](https://hub.ozow.com/integration-methods/apis/payout/send-a-payout.md)

**Before you go live**: [Payout test cases](https://hub.ozow.com/integration-methods/testing/payout-test-cases.md)

---

# Payout to voucher

> Paying money out from your Ozow float as a voucher.

Source: https://hub.ozow.com/payment-products/payout/payout-to-voucher/

Payout to voucher sends money to someone's cellphone number as a voucher they can redeem in cash or
spend in store, no bank account needed.

It solves the problem bank payouts can't: a large share of South Africans are unbanked or
underbanked, and many are hesitant to provide their bank details for a once-off payment.

Payouts to voucher run from the same integration as [payouts to bank](https://hub.ozow.com/payment-products/payout/payout-to-bank.md), so you
don't build twice.

## How a payout works

1. You submit a payout with the recipient's cellphone number and the amount.
2. Ozow confirms the payout is genuinely intended before any money moves, through your verification
   webhook if you're using the API, or an approval if you're uploading from the Dashboard.
3. The recipient receives a voucher on their phone.
4. They redeem it at a participating store, for cash, or to spend in store.
5. Ozow notifies your system of the final status.

There is no customer-facing step. The whole flow happens between your backend and Ozow.

```mermaid
sequenceDiagram
    participant M as Your system
    participant O as Ozow
    participant R as Recipient bank

    M->>O: Checks payout availability
    M->>O: Sends payout request
    O->>M: Calls your verification webhook
    M-->>O: Confirms the payout
    O->>R: Submits the payout to the bank
    R-->>O: Confirms the outcome
    O-->>M: Notifies your webhook of the final status
```

## Enabling payouts to voucher

The process is the same as for bank payouts, and just as strict:

**1. Approval.** Speak to your account manager. Ozow reviews your use case before payouts are enabled.

**2. Staging testing.** Complete the [payout test
cases](https://hub.ozow.com/integration-methods/testing/payout-test-cases.md) in staging. Mandatory, with evidence
required.

**3. Sign-off.** Ozow signs off on your staging results before payouts are enabled in production.

You'll also need a **funded float**. Voucher payouts draw on the same float as bank payouts. See
[Float top-up](https://hub.ozow.com/payment-products/settlements-and-float/float-top-up.md).

If you already have bank payouts enabled, adding vouchers is a smaller step; talk to your account
manager rather than assuming it's automatically available.

> ⚠️ **Payments from your customers don't fund your float.** When a customer pays you, that money is
> settled to your bank account. Your float is separate, and you fund it yourself by transferring
> money to Ozow. A busy sales day doesn't give you more capacity to refund or pay out; only a top-up
> does.

> ℹ️ Need a different arrangement? Speak to your account manager if you'd like your incoming
> payments to flow into your float rather than being settled to your bank account. Ozow approves
> these at its discretion based on your use case; approval isn't guaranteed.

## Things to know

**The cellphone number is the destination.** There's no account name to check against, so a mistyped
number sends money to whoever holds that number. Validate numbers before you submit, and be more
careful here than you would be with a bank payout; a bank account at least fails on an invalid
number.

**Vouchers can be forwarded.** Once a recipient has a voucher they may be able to pass it on. Treat
a voucher as cash: once it's sent to a number, control of it has left your hands.

**A redeemed voucher can't be reversed.** If you send to the wrong person and they redeem it, the
money is gone. Contact Ozow Support immediately if you spot an error, but don't count on recovery.

**Vouchers expire.** An unredeemed voucher doesn't sit there indefinitely, so tell your recipients
to redeem promptly, and expect some proportion never to be redeemed.

**Your recipient needs to reach a store.** Bank payouts arrive wherever the recipient is; a voucher
needs a trip to a participating outlet.

**Payouts come from your float**, not from your incoming payments. If the float is empty, payouts
won't process.

## Integrating payouts to voucher

Voucher payouts use the same Payouts API as bank payouts, you send a cellphone number instead of
bank account details.

**Bulk payouts from the Dashboard**: upload a CSV, no development required.
→ [Bulk payouts](https://hub.ozow.com/integration-methods/no-code/bulk-payouts.md)

**Payouts API**: submit voucher payouts from your own system, individually or in volume.
→ [Send a payout](https://hub.ozow.com/integration-methods/apis/payout/send-a-payout.md)

**Before you go live**: [Payout test cases](https://hub.ozow.com/integration-methods/testing/payout-test-cases.md)

**Status handling**: voucher payouts use the same statuses as bank payouts. See [Transaction and
settlement statuses](https://hub.ozow.com/integration-methods/statuses.md).

---

# Float top-up guide

> Payouts and refunds are funded from your float balance. Set up your static top-up reference once, then load funds whenever your float runs low.

Source: https://hub.ozow.com/payment-products/settlements-and-float/float-top-up/

Ozow uses your float balance to fund payout transactions and refunds. Without sufficient funds in
your float, payouts and refunds will fail. Before you can process any payouts or refunds, your float
must have sufficient funds loaded. This guide walks you through setting up your static top-up
reference and loading funds into your float.

> ℹ️ **You only need to complete this setup once.** Your static top-up reference and beneficiary
> details are permanent, they will not change. Once set up, all you need to do is make a payment to
> Ozow using your saved beneficiary details whenever you want to top up your float.

## Before you start

- You need an active Ozow merchant account with payout and/or refund access enabled
- You need access to your internet banking to add Ozow as a beneficiary and make a payment

## Step 1: Log in to the Ozow Dashboard

Visit [dash.ozow.com](https://dash.ozow.com) and log in to your merchant account.

## Step 2: Navigate to Float Top-ups

Go to [dash.ozow.com/MerchantAdmin/Refund/ReferenceTopups](https://dash.ozow.com/MerchantAdmin/Refund/ReferenceTopups)

You can also find this page by navigating to **Float** in the left-hand menu.

## Step 3: Click "Top-up Float"

Click the **Top-up Float** button on the right-hand side of the page.

## Step 4: Select your top-up type

You will be redirected to the top-up type selection page at [dash.ozow.com/MerchantAdmin/Refund/ReferenceTopUpTypeSelection](https://dash.ozow.com/MerchantAdmin/Refund/ReferenceTopUpTypeSelection).

Select **No, Thanks** and click **Submit**.

## Step 5: Copy your static reference

You will be redirected to your static top-up reference page at [dash.ozow.com/MerchantAdmin/Refund/GetStaticTopUpReference](https://dash.ozow.com/MerchantAdmin/Refund/GetStaticTopUpReference).

Copy your static reference by clicking the copy icon next to it.

> ⚠️ **Important**: Do not alter your static reference in any way. It must be used exactly as shown.
> This reference is a unique identifier for your float in Ozow's system.

## Step 6: Add Ozow as a beneficiary in your internet banking

Log in to your internet banking and add Ozow as a beneficiary using the banking details shown on the
static reference page in Step 5. Save your static reference against the beneficiary details in your
internet banking.

> ⚠️ **Important**: Save your static reference against the beneficiary details in your internet
> banking. You must use this exact reference every time you top up. Do not change it.

> ⚠️ **Security warning**: Only use the banking details shown on your Ozow Dashboard. Never use
> banking details shared by a third party or found outside of your official Ozow Dashboard.

## Step 7: Make a payment

Once you have added Ozow as a beneficiary, make a payment for the amount you want to load into your
float. Enter the desired top-up amount when making the payment.

Once Ozow receives your payment, the system will automatically assign the value to your float. You
can confirm your updated float balance at
[dash.ozow.com/MerchantAdmin/Refund/ReferenceTopups](https://dash.ozow.com/MerchantAdmin/Refund/ReferenceTopups).

> ℹ️ **Plan ahead**: Given the 1-2 business day processing time, we recommend topping up your float
> before it runs low rather than waiting until funds are depleted. Your low float balance alert will
> help you stay ahead of this.

## Low float balance alerts

As part of your payout and/or refund integration setup, Ozow will configure a low float balance
email alert for your account. This alert notifies you automatically when your float balance drops
below a threshold agreed on during setup, giving you time to top up before payouts or refunds start
failing.

You do not need to set this up yourself, Ozow configures it as part of your integration. If you want
to adjust your alert threshold at any time, contact [support@ozow.com](mailto:support@ozow.com) or
your account manager.

## Topping up in future

You never need to repeat the setup process above. Your beneficiary details and static reference are
permanent. Whenever you want to top up your float:

1. Log in to your internet banking
2. Make a payment to the Ozow beneficiary you saved in Step 6
3. Enter the desired top-up amount
4. Your float will be updated automatically once the payment is received

## Support

If you have any questions about your float or need assistance with the top-up process, contact
[support@ozow.com](mailto:support@ozow.com) or reach out to your account manager.

---

# Transaction and settlement statuses

> Every payin, payout, refund and settlement status, which are final, and what to do about each.

Source: https://hub.ozow.com/integration-methods/statuses/

Every payment through Ozow moves through a series of statuses. This page lists all of them, tells
you which ones are final, and tells you what to do about each.

> 🚨 **Read this first.** `Pending` and `Complete` appear on a payin, on a settlement and on a
> refund. `Complete` on a payin is a customer's payment succeeding, on a settlement it is the
> payment out to you confirmed by Ozow's bank, and on a refund it is your customer having their
> money back. Refunds spell theirs as integers rather than strings. A status value on its own tells
> you nothing. Always check which object it belongs to before you act on it.

## How the lifecycles relate

A payment has more than one lifecycle. The transaction completing and the money reaching your bank
account are two separate events with two separate status vocabularies.

```mermaid
flowchart TB
    subgraph in["Money in"]
        direction LR
        A["Customer<br/>pays"] --> B["Payin<br/>Complete"]
        B --> C["Settlement<br/>Complete"]
        C --> D["Your bank<br/>account"]
    end
    subgraph out["Money out"]
        direction LR
        T["Your bank<br/>account"] -. "top up" .-> E["Your<br/>float"]
        E --> F["PayoutComplete"]
        E --> H["Refund"]
        F --> G["Recipient's<br/>bank account"]
        H --> G
    end
    in ~~~ out
```

**A completed transaction is not settled money.** A payin status of `Complete` means the customer's
payment succeeded and the funds *will* be settled to you. The settlement status tells you whether
that has actually happened. Don't use payin status to reconcile your bank account. Every other final
payin status settles nothing.

**Refunds and payouts draw on your float, not on the original transaction.** A refund needs a funded
float balance even though it's returning money the customer already paid, because that money has
already been settled to you. See [Float
top-up](https://hub.ozow.com/payment-products/settlements-and-float/float-top-up.md).

## Look up a status

[Status decoder](https://hub.ozow.com/integration-methods/statuses/), a tool on this page.

The tables below list every status in full.

## How to read a status

### Final vs non-final

A **final** status will not change on its own. A **non-final** status will be followed by another
update, and you must wait for it rather than acting.

Getting this wrong is the most common integration bug in payments. Never release goods, mark an
order paid, or notify a customer on a non-final status.

> ⚠️ One exception: payin `PendingInvestigation` is final in the sense that no automatic update is
> coming, but Ozow Support can change it to `Complete` or `Error` after a manual check. Treat it as
> needing human action, not as a settled outcome.

### Status and sub-status

Payouts return a status and, in most cases, a sub-status. **Branch on the sub-status whenever one is
present.**

**The API sends the code, not the name.** `subStatus` on a payout status response is a number, so
`405` is what arrives where this page says `PayoutProcessingError_InvalidAccountNumber`. Both are
listed. The name is for reading; the code is what your `switch` matches.

This matters because a parent status can be non-final while the sub-status under it is final.
`PayoutReceived` is non-final, but `Payout_ValidationFailed` beneath it is a final failure. If you
only read the parent status, you will wait forever for a payout that has already failed.

### Never infer status from the browser redirect

The customer's browser returning to your success page is not proof of payment. Statuses arrive on
your notification or webhook URL, and you can query them with the status check API. See [Building a
secure integration](https://hub.ozow.com/getting-started/building-a-secure-integration.md).

---

## Payin statuses

```mermaid
stateDiagram-v2
    direction LR

    state "Not paid" as NotPaid {
        Cancelled
        Abandoned
        Voided
        Error
    }

    [*] --> Created
    Created --> PendingInvestigation
    Created --> Pending
    Created --> Complete
    Created --> NotPaid

    PendingInvestigation --> Complete
    PendingInvestigation --> NotPaid

    Pending --> Complete
    Pending --> NotPaid
```

Which of the four unpaid outcomes you can get depends on where the payment was when it stopped.
`Voided` follows only `Created`. From `Pending` the only unpaid outcome is `Error`, and from
`PendingInvestigation` it is `Cancelled`, `Abandoned` or `Error`.

**Non-final:** `Created`, `Pending`
**Final:** `Complete`, `PendingInvestigation`, `Cancelled`, `Abandoned`, `Voided`, `Error`

**The `One API` column is the value `GET /payments/{id}/transactions` returns for that status.**

| Status | One API | Final? | Settles? | What it means | What to do |
|---|---|---|---|---|---|
| `Created` | `Incomplete` | No | No | The transaction has been created and the customer has opened the payment page, but hasn't completed it. | Wait for a final status. |
| `Complete` | `Successful` | Yes | **Yes** | The payment succeeded and the funds will be settled to you. | Nothing. Fulfil the order. |
| `Cancelled` | `Error` | Yes | No | The transaction was cancelled: either the customer pressed cancel, or the payment failed Ozow's verification with the bank. `StatusMessage` says which. | Nothing. If they still want to pay, ask them to start a new transaction. |
| `Abandoned` | `Error` | Yes | No | The customer started the transaction but didn't finish it, typically they closed the Ozow payment page. | Nothing. If they still want to pay, ask them to start a new transaction. |
| `Voided` | `Error` | Yes | No | The transaction was invalidated and won't be processed. Happens when the customer changes bank partway through the Ozow flow, or when the saved profile they chose is deleted, deactivated or fails to load. | Nothing. A new transaction will have been created for the method they switched to. **No notification is sent for a voided transaction**, so find these by polling rather than by waiting. |
| `Error` | `Error` | Yes | No | An error occurred while the transaction was being processed. Not caused by anything the customer did. | Ask the customer to retry the transaction. |
| `Pending` | `Pending` | No | No | The payment still has to be verified. The outcome follows once it has been. | Wait for the update on your notification URL. Do not release anything on it. |
| `PendingInvestigation` | `Pending` | Yes | No | Ozow could not complete its verification with the bank, so the payment has to be checked manually against your bank statement. | Check your bank statement. If the funds arrived, contact Ozow Support with proof of payment to have the status updated to `Complete`. If they didn't, contact Support to have it set to `Error`. |

> ℹ️ `Pending` is non-final: an update follows. Handle it as "wait", never as an outcome. It means
> neither that the payment failed nor that it succeeded.

> ⚠️ Refunding a transaction does not change its status. If you need to know whether a transaction
> has been refunded, check the refund records rather than relying on the transaction status.

> ℹ️ Settlement timing depends on the payment method.

---

## Payout statuses

```mermaid
stateDiagram-v2
    direction LR

    state "Ended without paying" as Ended {
        PayoutProcessingError
        PayoutReturned
    }

    [*] --> PayoutReceived
    PayoutReceived --> Verification
    Verification --> SubmittedForProcessing
    SubmittedForProcessing --> PayoutComplete
    PayoutComplete --> PayoutReturned

    Verification --> Ended
    SubmittedForProcessing --> Ended

    Verification --> PayoutPendingInvestigation
    SubmittedForProcessing --> PayoutPendingInvestigation
    PayoutPendingInvestigation --> PayoutComplete
    PayoutPendingInvestigation --> Ended
```

The happy path is `PayoutReceived` → `Verification` → `SubmittedForProcessing` → `PayoutComplete`.
`Verification` can also reach `PayoutComplete` directly, so a payout that never appears in
`SubmittedForProcessing` has not skipped a step. `PayoutProcessingError` and `PayoutReturned` are
grouped because nothing follows either of them.

> ⚠️ **`PayoutComplete` is not final, though it almost never changes.** A completed payout moves
> to `PayoutReturned` if the destination bank sends the money back, which is a rare event rather
> than one to plan a flow around. Keep handling status updates for a payout you have already marked
> paid, and you will hear about it on the day it happens. The only two statuses nothing follows are
> `PayoutProcessingError` and `PayoutReturned`.

**Payout timing:** with `isRtc` set to `true` the payout is instant. With `isRtc` set to `false` it
takes 1-2 business days.

> ⚠️ Branch on the **sub-status**, not the parent status. `PayoutReceived`, `Verification` and
> `PayoutPendingInvestigation` are all non-final while carrying sub-statuses that are final
> failures.

### PayoutReceived

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | No | The payout request has been received. | Wait for a final status. |
| `Payout_Unclassified` | 100 | Yes | No sub-status could be determined. The payout has failed. | Rare edge case. Treat as a failure and investigate. |
| `Payout_ValidationFailed` | 101 | Yes | Request validation failed. | Check the `ErrorMessage` field for the reason, correct the request, then resubmit. |

### Verification

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | No | The payout is being verified. | Wait for a final status. |
| `Verification_Pending` | 201 | No | Ozow is waiting for a response from your verification webhook. | Wait for a final status. |
| `Verification_Success` | 203 | No | Your webhook verified the payout successfully. Processing continues. | Wait for a final status. |
| `Verification_Failed` | 202 | Yes | Your verification webhook returned a response that failed verification. | Check why your webhook rejected it, then resubmit the payout. |
| `Verification_Error` | 204 | Yes | Ozow couldn't reach your verification webhook. | Check that your webhook is reachable. If you've changed its URL, confirm Ozow has the new one. Then resubmit. |
| `Verification_AccountNumberDecryptionFailed` | 205 | Yes | The key returned by your webhook failed to decrypt the account number. | Check your webhook's key handling, then resubmit the payout. |
| `Verification_Success_Awaiting_Funds` | 206 | No | The payout verified, but your float balance is too low to cover it. | Top up your float. The payout continues on its own once the funds are there. Do not resubmit. |
| `Verification_Success_Awaiting_Submission` | 207 | No | The payout verified and is queued for submission. | Wait for a final status. |

### SubmittedForProcessing

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | No | The payout is being processed. | Wait for a final status. |
| `SubmittedForProcessing_PayoutAddedToBatch` | 301 | No | The payout has been added to a batch. | Wait for a final status. |
| `SubmittedForProcessing_PayoutSubmittedToBank` | 302 | No | The batch has been processed and submitted to the bank. | Wait for a final status. |
| `SubmittedForProcessing_PayoutSubmittedToPpi` | 303 | No | The payout has been submitted for processing. | Wait for a final status. |

### PayoutComplete

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | No | The payout completed successfully. | Release whatever the payout was for, and keep handling updates: this can still become `PayoutReturned`. |

### PayoutProcessingError

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | Yes | An error occurred while processing the payout. | Resubmit the payout. |
| `PayoutProcessingError_PayoutRejected` | 401 | Yes | The bank rejected the payout. | Resubmit the payout. |
| `PayoutProcessingError_PayoutCancelled` | 402 | Yes | Ozow stopped the payout before it was paid. | Check the `ErrorMessage` field, then resubmit. |
| `PayoutProcessingError_Insufficient_Balance` | 403 | Yes | Your float balance was too low to cover the payout. | **Top up your float. Do not resubmit**: see the warning below. |
| `PayoutProcessingError_PayoutInternalError` | 404 | Yes | An internal error occurred during processing. | Resubmit the payout. |
| `PayoutProcessingError_InvalidAccountNumber` | 405 | Yes | The account number is invalid. | Correct the account number, then resubmit. |

> 🚨 **On `PayoutProcessingError_Insufficient_Balance` you must not resubmit.** Top up your float
> instead. Once the float is allocated, the payout processes automatically. Resubmitting risks
> paying the recipient twice.

### PayoutReturned

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | Yes | The payout couldn't be paid into the recipient's account. | Resubmit the payout. |
| `PayoutReturned_Unpaid` | 9001 | Yes | The destination bank rejected the payment. | Check that the destination account is still active, then resubmit. |

### PayoutPendingInvestigation

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | No | The payout is under investigation. | Wait for a final status. |
| `PayoutPendingInvestigation_AmountMismatch` | 601 | Yes | The payout failed because the amounts didn't match. | Check the payout details, then resubmit. |

> ⚠️ **A payout cannot be cancelled once it is submitted.** Check the request before you send it:
> the account number, the amount and the reference are all final from that point.

---

## Refund statuses

A refund returns money to a customer for a payin you've already received. Refunds draw on your float
balance, not on the original transaction; the funds from that transaction have already been settled
to you.

> ⚠️ Refund statuses are returned as **integers**, not strings. This is different from payin, payout
> and settlement statuses, which are returned as text.

> ⚠️ **The numeric values are not in lifecycle order.** A refund progresses `0` → `2` → `1`. Don't
> treat a higher value as further along, and don't use greater-than comparisons to test progress.

```mermaid
stateDiagram-v2
    [*] --> Pending: 0
    Pending --> Submitted: 2
    Pending --> Cancelled: 4
    Submitted --> Complete: 1
    Submitted --> Failed: 3
    Submitted --> Returned: 5
```

This is the path a refund takes. Three further statuses sit outside it, `Invalid` (-1),
`PendingInvestigation` (-2) and `Error` (-3). Each one has a row below. Handle them wherever they
turn up rather than by position.

**Non-final:** `Pending` (0), `Submitted` (2), `PendingInvestigation` (-2)
**Final:** `Complete` (1), `Failed` (3), `Cancelled` (4), `Returned` (5), `Invalid` (-1), `Error` (-3)

**The negative values are real.** `Invalid`, `PendingInvestigation` and `Error` are returned by the
API alongside the six above. Switch on all nine, or give your default branch something safe to do.

| Value | Status | Final? | What it means | What to do |
|---|---|---|---|---|
| `0` | `Pending` | No | The refund request has been submitted and accepted, but not yet processed. | Wait for a final status. |
| `2` | `Submitted` | No | The refund has been assigned to a batch and is being processed. | Wait for a final status. |
| `1` | `Complete` | Yes | The refund was paid successfully. The customer has their money. | Nothing. |
| `3` | `Failed` | Yes | The refund payment failed. | Check that your float is funded, then submit a new refund. |
| `4` | `Cancelled` | Yes | The refund was cancelled before it was submitted for processing. | Nothing. If the customer is still owed a refund, submit a new one. |
| `5` | `Returned` | Yes | The refund was paid and came back. The destination account did not accept the credit, or the destination bank could not apply it. | Confirm the account details with the customer before you try again. Resubmitting the same details without checking returns the money a second time. |
| `-1` | `Invalid` | Yes | The refund could not be accepted as submitted. | Check the refund details against the original transaction, then submit a corrected refund. |
| `-2` | `PendingInvestigation` | No | The refund is being checked manually. | Wait. Ozow reports this as `Pending` on a `thin` webhook, so a handler reading the webhook rather than the API will not see this value. |
| `-3` | `Error` | Yes | The refund failed because of an error rather than a rejection. | Ozow reports this as `Failed` on a `thin` webhook. Treat it as a failure and submit a new refund. |

> ⚠️ **On `Returned`, do not resubmit the same details without checking them.** The money came back
> from the destination, so sending it again the same way returns it again. Confirm the account with
> the customer first.

> ℹ️ Refunds require a funded float. If your float is empty, refunds will not process. See [Float top-up](https://hub.ozow.com/payment-products/settlements-and-float/float-top-up.md).

> ℹ️ **Refund status is the source of truth for whether a customer has been refunded.** The original
> payin transaction keeps the status it had, so don't use transaction status to check refund state.

---

## Settlement statuses

A settlement is the transfer of your collected funds into your bank account. Settlement statuses
tell you where that transfer is, and they are **not** the same thing as the status of the
transactions inside it.

```mermaid
stateDiagram-v2
    direction LR
    [*] --> Pending
    Pending --> Submitted
    Submitted --> Complete
    Submitted --> PendingInvestigation
    PendingInvestigation --> Complete
```

**Non-final:** `Pending`, `Submitted`, `PendingInvestigation`
**Final:** `Complete`

**A settlement has four statuses, and none of them is a payin status.**

| Status | Final? | What it means | What to do |
|---|---|---|---|
| `Pending` | No | The settlement has been created. The payment out to you has not started. | Wait. If it stays here for an extended period, email [support@ozow.com](mailto:support@ozow.com). |
| `Submitted` | No | The settlement has been submitted to Ozow's bank. It might not have reached yours yet. | Wait. If it stays here for an extended period, email [support@ozow.com](mailto:support@ozow.com). |
| `Complete` | Yes | Ozow has submitted the payment and confirmed with Ozow's bank that it went out. Your own bank still has to clear it. | Reconcile against your bank statement rather than against this status. `Complete` is Ozow's leg finished; the clearing leg is your bank's and can lag it. |
| `PendingInvestigation` | No | The settlement is being checked manually. | Wait. If it stays here for an extended period, email [support@ozow.com](mailto:support@ozow.com) with your settlement reference. |

Settlement timing depends on the payment method the funds came in through.

---

## Which statuses mean money moved

Only these mean money moved:

- **Payin:** `Complete`, and only `Complete` settles
- **Payout:** `PayoutComplete`, which can still become `PayoutReturned` if the bank sends it back
- **Refund:** `Complete` (`1`), the integer and not the string
- **Settlement:** `Complete`, which is the payment out confirmed by Ozow's bank, with your own bank
  still to clear it

Everything else is either in progress or a failure. In particular:

- `Pending`, `Created`, `Verification`, `SubmittedForProcessing` and `PayoutReceived` are **in
  progress**. Wait.
- A payin `PendingInvestigation` needs a human. It is not a success.
- A payin `Complete` is not settled money. Check the settlement status for that.

---

# Building a secure integration

> Where Ozow's security responsibility ends and yours begins: credentials, webhook endpoints, verifying notifications, and validating amounts.

Source: https://hub.ozow.com/getting-started/building-a-secure-integration/

Security is a shared responsibility between Ozow and you as the merchant. Understanding where Ozow's
responsibility ends and yours begins is essential to building an integration that is safe for your
customers and your business.

## The shared security model

Ozow secures the payment infrastructure. You secure how you integrate with it.

| Ozow is responsible for | You are responsible for |
|---|---|
| The security of the payment processing infrastructure | How you store and handle your API credentials |
| Encryption of payment data in transit and at rest within Ozow systems | The security of your callback and webhook endpoints |
| The integrity and availability of Ozow APIs | Verifying that notifications genuinely came from Ozow |
| Fraud monitoring within the Ozow platform | Validating transaction details before crediting orders |
| Physical and network security of Ozow's environments | Access controls on your systems and Ozow Dashboard |
| Compliance with applicable payment regulations on Ozow's side | Monitoring your own integration for anomalous activity |

A secure Ozow integration is not only about calling the right endpoints, it is about what happens on
your side of the connection too.

## Your security responsibilities

### Credentials and secrets

Your API credentials are the keys to your Ozow integration. If they are compromised, an attacker
could initiate payments or payouts on your behalf.

- Store all Ozow credentials, API keys, private keys, Client IDs, Client Secrets, and Payout API
  keys; in a secrets manager or environment variables. Never hardcode them or commit them to source
  control.
- Keep test and production credentials in strictly separate environments. Never use production
  credentials in a development or staging environment.
- Restrict access to production credentials to a named list of people and services. Access must be least-privilege.
- Have a documented process and a named owner for rotating credentials. Know what you would do if a
  key were compromised.

### Callback and webhook endpoint security

Ozow communicates payment outcomes by sending notifications to a URL you specify. This endpoint is a
critical part of your integration.

- Your callback and webhook URLs must be HTTPS only, using TLS 1.2 or later.
- Your endpoint must not expose stack traces, internal errors, or verbose logs in its response to callers.

### Verifying notifications

Receiving a notification is not the same as trusting it. You must verify that every notification
genuinely came from Ozow before acting on it.

- For Payments API integrations: verify every incoming notification using the hash check before
  updating any order status.
- For One API integrations: validate the message signature on every incoming webhook before acting
  on it.
- Log and alert on verification failures rather than silently discarding them. A pattern of
  verification failures is a signal worth investigating.
- Never mark a payment as complete based on the browser redirect alone. Always confirm status via
  the API or a verified webhook notification.
- Implement replay protection so that a previously processed transaction reference cannot be
  reprocessed to double-credit an order.

> ⚠️ **Important**: Ozow may occasionally send duplicate notifications for the same transaction.
> Your system must handle this gracefully, processing the same transaction twice must not result in
> double-crediting an order.

### Transaction integrity

Before crediting an order, validate that the payment details match what you originally requested.

- Verify that the amount, currency, and merchant reference in the notification match your original
  payment request.
- Handle duplicate notifications idempotently, receiving the same notification twice must have no
  additional effect.
- Periodically reconcile your order records against Ozow's transaction records rather than relying
  solely on webhook delivery.

### Payout-specific responsibilities

Payouts carry additional security requirements because they involve outgoing funds.

**Authorisation**

- For bulk payouts: Ozow recommends that the person who requests a bulk payout is different from the
  person who approves it. Ozow does not enforce this.
- For API payouts: access to the systems, credentials, and code that can trigger a payout must be
  restricted to a named list of people, with any changes requiring review.
- Your system must enforce a business-level authorisation step before calling Ozow's payout API.
  Being authenticated is not sufficient, there must be a deliberate approval within your own system
  before a payout is initiated.

**Beneficiary handling**

- Verify beneficiary bank details before the first payout to any new beneficiary.
- If you are not using stored beneficiary profiles, validate destination bank details on every
  payout request.
- Any change to stored beneficiary details must trigger a mandatory review or re-verification step
  before the next payout.
- Generate and persist a unique encryption key per payout request. Never reuse an encryption key
  across multiple payout requests.
- Enforce velocity and amount limits on payouts.
- Implement real-time alerting for anomalous payout activity, unusual amounts, unfamiliar
  beneficiaries, or off-hours activity are all signals worth acting on immediately.

**Verification request handling**

- Validate the access token on all incoming payout verification requests.
- Verify the hash on every verification request to confirm it genuinely originated from Ozow.
- Validate that the payout details in the verification request match a payout your system actually
  initiated: do not return a decryption key based on token and hash checks alone without confirming
  the payout is expected.

**Payout status verification**

- Confirm payout completion via the API, rather than assuming completion from the initial payout response.
- Verify the hash on every incoming payout status notification before trusting it.

### Access and monitoring

- Apply least-privilege access for all roles with access to your Ozow merchant Dashboard.
- Monitor for abnormal patterns in your payin traffic, spikes in failed verifications or unusual
  volumes are worth investigating.
- Maintain an immutable audit trail of who requested and who approved every payout, and when.
- Reconcile your internal ledger against Ozow's payout records on a regular cadence.

## Quick reference checklist

Use this checklist before going live with any Ozow integration.

### Payin integrations

- [ ] API credentials are stored securely and never hardcoded or committed to source control
- [ ] Test and production credentials are in strictly separate environments
- [ ] Production credentials are restricted to a named list of people and services
- [ ] Credential rotation process is documented with a named owner
- [ ] Callback URL is HTTPS only with TLS 1.2 or later
- [ ] Callback endpoint does not expose internal errors or stack traces
- [ ] Every notification is verified using hash check (Payments API) or message signature (One API)
  before being trusted
- [ ] Verification failures are logged and alerted on
- [ ] Payment status is confirmed via API, not the browser redirect alone
- [ ] Replay protection is in place for transaction references
- [ ] Amount, currency, and merchant reference are validated against the original request before crediting
- [ ] Duplicate notifications are handled idempotently
- [ ] Order records are periodically reconciled against Ozow transaction records
- [ ] Dashboard access follows least-privilege
- [ ] Monitoring is in place for anomalous payin traffic

### Payout integrations

- [ ] Payout API key is stored securely and never hardcoded or committed to source control
- [ ] Test and production payout credentials are in strictly separate environments
- [ ] Access to payout-triggering systems and code is restricted to a named list
- [ ] Bulk payout requestor and approver are different people (recommended, not enforced by Ozow)
- [ ] Business-level authorisation step is enforced before calling the payout API
- [ ] Beneficiary bank details are verified before the first payout to any new beneficiary
- [ ] A unique encryption key is generated and persisted per payout request
- [ ] Velocity and amount limits are enforced on payouts
- [ ] Real-time alerting is in place for anomalous payout activity
- [ ] Incoming verification requests are validated on token, hash, and expected payout details
- [ ] Payout completion is confirmed via API, not assumed from the initial response
- [ ] Payout status notifications are verified by hash before being trusted
- [ ] An immutable audit trail exists for every payout
- [ ] Internal ledger is reconciled against Ozow payout records regularly

---

# The contract

The operations those pages declare, as the specification defines them.

---

# Get Available Banks

> GET `/getavailablebanks`
> Part of the Payouts API reference. Source: https://hub.ozow.com/api-reference/payouts-api/get-getavailablebanks/

Server: `https://payoutsapi.ozow.com/v1` (Production)

Other environments: `https://stagingpayoutsapi.ozow.com/v1` (Staging)

This method can be called to retrieve the set of banks that are available for making payouts to.

The call returns a list of banks with a unique identifier (BankGroupId) per bank. The BankGroupId is utilised as an input parameter to check payout availability for a particular bank, as well as to identify the destination bank for a payout.

## Authentication

- `ApiKey` (API key in the ApiKey header)

## Query parameters

- `rtconly` (boolean) - For RTC Banks only you can add the "rtconly" parameter to return a filtered list.

## Header parameters

- `SiteCode` (string, required) - A unique code for the site currently in use. A site code is generated when adding a site in the Ozow merchant admin section. [Please contact support for SiteCode - support@ozow.com]

## Responses

### 200 OK

array of BankGroup

### 400 Bad Request. The request did not reach the API. A required header or query parameter is missing, or the body does not match the expected shape.

- `message` (string) - What was wrong with the request.

### 403 Forbidden. The `SiteCode` and `ApiKey` pair was not accepted.

- `message` (string) - What was wrong with the request.

### 500 Internal Server Error. The gateway could not reach the service or the integration failed. A failure inside the service is not this one. Those come back as a 200 with the reason in the payload, because the gateway maps every integration response it has no other rule for onto 200.

- `message` (string) - What was wrong with the request.


---

# Get Payout

> GET `/getpayout`
> Part of the Payouts API reference. Source: https://hub.ozow.com/api-reference/payouts-api/get-getpayout/

Server: `https://payoutsapi.ozow.com/v1` (Production)

Other environments: `https://stagingpayoutsapi.ozow.com/v1` (Staging)

Retrieve a single payout by its identifier.

## Authentication

- `ApiKey` (API key in the ApiKey header)

## Query parameters

- `payoutId` (string, required) - The unique payout identifier.

## Header parameters

- `SiteCode` (string, required) - A unique code for the site currently in use. A site code is generated when adding a site in the Ozow merchant admin section. [Please contact support for SiteCode - support@ozow.com]

## Responses

### 200 OK. The payout was found, which says nothing about how it went. Its outcome is `payoutStatus`, and a failed payout is a 200 with the reason in `payoutStatus.subStatus` and `payoutStatus.errorMessage`.

- `id` (string, uuid, required, max length 50) - Ozow's unique reference for the payout.
- `amount` (number, double, required, min 0) - The payout amount.
- `siteCode` (string, required, max length 50) - A unique code for the site currently in use. A site code is generated when adding a site in the Ozow merchant admin section. [Please contact support for SiteCode - support@ozow.com]
- `merchantReference` (string, required, max length 20) - The merchant's reference for the transaction.
- `customerBankReference` (string, required, max length 20, pattern ^[A-Za-z0-9 -]+) - The reference that will appear on the merchant’s bank statement and can be used for recon purposes. Only alphanumeric characters, spaces and dashes are allowed.
- `notifyUrl` (string, uri, max length 150) - The URL that we should use to post all payout notifications.
- `isRtc` (boolean, required) - Whether the payout should be processed as an RTC payout. ***RTC is not available in the staging environment so should always be set to false when testing in this environment***
- `bankingDetails` (object, required) - Payout destination banking details.
- `payoutStatus` (object, required) - Payout status.

### 400 Bad Request. The request did not reach the API. A required header or query parameter is missing, or the body does not match the expected shape.

- `message` (string) - What was wrong with the request.

### 403 Forbidden. The `SiteCode` and `ApiKey` pair was not accepted.

- `message` (string) - What was wrong with the request.

### 500 Internal Server Error. The gateway could not reach the service or the integration failed. A failure inside the service is not this one. Those come back as a 200 with the reason in the payload, because the gateway maps every integration response it has no other rule for onto 200.

- `message` (string) - What was wrong with the request.


---

# Get Payouts by Reference

> POST `/getpayoutbyreference`
> Part of the Payouts API reference. Source: https://hub.ozow.com/api-reference/payouts-api/post-getpayoutbyreference/

Server: `https://payoutsapi.ozow.com/v1` (Production)

Other environments: `https://stagingpayoutsapi.ozow.com/v1` (Staging)

Search a site's payouts. Filtering on the merchant reference is the common case, and is what a status check falls back to when the payout identifier was not recorded.

## Authentication

- `ApiKey` (API key in the ApiKey header)

## Header parameters

- `SiteCode` (string, required) - A unique code for the site currently in use. A site code is generated when adding a site in the Ozow merchant admin section. [Please contact support for SiteCode - support@ozow.com]

## Request body (required)

- `pageSize` (integer) - How many payouts to return per page. Below 1 is treated as 1.
- `pageIndex` (integer) - Which page to return, counting from 1. Below 1 is treated as 1.
- `searchFields` (array of PayoutField) - Which fields `searchString` is matched against.
- `searchString` (string) - The value to search for in the fields named by `searchFields`.
- `sortField` (integer, one of 0, 1, 2, 3, 4) - The field to order the results by.
- `minAmount` (number) - Exclude payouts below this amount.
- `maxAmount` (number) - Exclude payouts above this amount.
- `dateFrom` (string, date-time) - Exclude payouts created before this moment.
- `dateTo` (string, date-time) - Exclude payouts created after this moment.
- `isRtc` (boolean) - Return only real time clearing payouts.
- `bulkReference` (string) - Return only payouts from the bulk upload with this reference.

## Responses

### 200 OK

- Header `X-Pagination`: Pagination metadata for the result set, as a JSON object. Carries the page counts and the flags for whether further pages exist.

array of PayOut

### 400 Bad Request. The request did not reach the API. A required header or query parameter is missing, or the body does not match the expected shape.

- `message` (string) - What was wrong with the request.

### 403 Forbidden. The `SiteCode` and `ApiKey` pair was not accepted.

- `message` (string) - What was wrong with the request.

### 500 Internal Server Error. The gateway could not reach the service or the integration failed. A failure inside the service is not this one. Those come back as a 200 with the reason in the payload, because the gateway maps every integration response it has no other rule for onto 200.

- `message` (string) - What was wrong with the request.


---

# Request Payout

> POST `/requestpayout`
> Part of the Payouts API reference. Source: https://hub.ozow.com/api-reference/payouts-api/post-requestpayout/

Server: `https://payoutsapi.ozow.com/v1` (Production)

Other environments: `https://stagingpayoutsapi.ozow.com/v1` (Staging)

Request a payout of the specified amount to the destination.

## Authentication

- `ApiKey` (API key in the ApiKey header)

## Header parameters

- `SiteCode` (string, required) - A unique code for the site currently in use. A site code is generated when adding a site in the Ozow merchant admin section. [Please contact support for SiteCode - support@ozow.com]

## Request body

- `siteCode` (string, required, max length 50) - A unique code for the site currently in use. A site code is generated when adding a site in the Ozow merchant admin section. [Please contact support for SiteCode - support@ozow.com]
- `amount` (number, double, required) - The payout amount in ZAR.
- `merchantReference` (string, required, max length 20) - The merchant's reference for the transaction.
- `customerBankReference` (string, required, max length 20, pattern ^[A-Za-z0-9 -]+) - The reference that will appear on the customer’s bank statement. Only alphanumeric characters, spaces and dashes are allowed.
- `isRtc` (boolean, required) - Whether the payout should be processed as an RTC payout. ***RTC is not available in the staging environment so should always be set to false when testing in this environment***
- `notifyUrl` (string, uri, max length 150) - The URL that we should use to post all payout notifications.
- `bankingDetails` (object, required) - Payout destination banking details.
- `hashCheck` (string, required) - SHA512 hash used to ensure that certain fields in the message have not been altered after the hash was generated. Check the generate hash section in the documentation for more details on how to generate the hash.

## Responses

### 200 OK. The request reached the API, which is not the same as the payout being accepted. A rejected payout is also a 200, with the reason in `payoutStatus.errorMessage` and no `payoutId`. Check that field, not the status code.

- `payoutId` (string, uuid, required) - A unique identifier that should be used to identify the payout.
- `payoutStatus` (object, required) - Payout status.

### 400 Bad Request. The request did not reach the API. A required header or query parameter is missing, or the body does not match the expected shape.

- `message` (string) - What was wrong with the request.

### 403 Forbidden. The `SiteCode` and `ApiKey` pair was not accepted.

- `message` (string) - What was wrong with the request.

### 500 Internal Server Error. The gateway could not reach the service or the integration failed. A failure inside the service is not this one. Those come back as a 200 with the reason in the payload, because the gateway maps every integration response it has no other rule for onto 200.

- `message` (string) - What was wrong with the request.


---

# Payout notification

> POST to your notification URL
> Sent by Ozow. Part of the Payouts API reference. Source: https://hub.ozow.com/api-reference/payouts-api/webhooks/notification-response/

Sent to the notification URL once a payout reaches a final status.

The URL comes from the `NotifyUrl` field on the payout request, or from the site configuration in the merchant admin site. Without one, no notification is sent.

Verify the `hashCheck` field before acting on the contents. A notification is an unauthenticated POST to a URL that anyone can call.

**The same payout can be notified more than once.** Ozow works to avoid duplicates and cannot guarantee their absence, so your handler must be idempotent: a repeated notification for a payout you have already processed must not credit or debit anyone a second time.

## Authentication

Ozow sends no credential with this call, so this check is the only thing standing between a real delivery and a stranger’s. Verify the hashCheck field before acting on the contents: your notification URL is public, and anyone can post to it.

## Payload

**application/json**

- `payoutId` (string, uuid) - Ozow's unique reference for the payout.
- `siteCode` (string, required, max length 50) - A unique code for the site currently in use. A site code is generated when adding a site in the Ozow merchant admin section. [Please contact support for SiteCode - support@ozow.com]
- `merchantReference` (string, required, max length 20) - The merchant's reference for the transaction.
- `customerMerchantReference` (string, required, max length 20, pattern ^[A-Za-z0-9 -]+) - The reference that will be prepopulated in the "their reference" field in the customers online banking site. This will be the payout reference that appears on the merchant’s bank statement and can be used for recon purposes.
- `payoutStatus` (object, required) - Payout status.
- `hashCheck` (string, required) - SHA512 hash used to ensure that certain fields in the message have not been altered after the hash was generated. Check the generate hash section in the documentation for more details on how to generate the hash.

## Your response

### 200 Acknowledged. Return this once you have stored the notification.

No body.
