Ozow Hub
On this page14 sections
Build with AI 1 package

A build package is every page for one task, with the API operations they use. Copy the prompt into a coding assistant, or hand it the package itself: slim links to each page, full inlines all of them in one document.

  • Send a payoutEverything 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.
    View package

This guide walks you through integrating Ozow payoutsPayout Money sent from a merchant to a bank account. Unlike a refund, a payout is not tied to a payment anyone made you, so you can pay anyone with a bank account. Payouts draw on your float rather than on your incoming payments, and they are not self-service: they need approval from Ozow and testing in staging first. 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 payinsPayin A payment made by a consumer to a merchant. The direction most of this site is about: money coming in. Its counterpart is a payout, which sends money out and is not tied to any payment anyone made you. 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.

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 for the full requirements.

Once approved, ensure you have the following in place:

  • Your Payout API key and site codeSite code The unique code for a site registered under a merchant. A site is a place to transact: a website, or a branch of a store. A merchant can have several, and each transaction names the one it belongs to, so sending the wrong code files the payment against the wrong place., 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 webhookWebhook A URL of yours that Ozow calls when something happens, rather than you polling to find out. The call carries no credential of yours and arrives at a public URL, so authenticate it before acting on it: a hash field on the Payments API, a Svix signature on One API. endpoint is set up and publicly accessible via HTTPS
  • Your notification URL is set up and publicly accessible via HTTPS
  • Your Ozow floatFloat The balance held with Ozow that payouts and refunds are paid out of. Both draw on it, and neither will process while it is empty. Payins do not need one, so if you only take payments you never meet it. is funded, Ozow uses your float balance to process payouts. See Float top-up guide to load funds into your float before going live.

How payout integration works

Environments

Environment Base URL Mock base URL Dashboard
Production https://payoutsapi.ozow.com/v1 https://payoutsapi.ozow.com/mock/v1 dash.ozow.com
Staging https://stagingpayoutsapi.ozow.com/v1 https://stagingpayoutsapi.ozow.com/mock/v1 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

Before submitting a payout request, check which banks are available and whether real-time clearingReal-Time Clearing Payments that clear immediately rather than waiting for a batch. A batch run settles at set times through the day; a Real-Time Clearing payment moves the funds between the two bank accounts as it is made, so the recipient can rely on them straight away.PayInc (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.

GET Get Available Banks Reference
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

[
  {
    "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 SHA512SHA-512 A hashing algorithm. Ozow uses it to sign the values in a request or a notification so you can tell that they arrived unaltered and came from us. Hashing is one-way: the hash cannot be turned back into what produced it.Wikipedia hash 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.

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();
}

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.

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}");

Step 4: Submit the payout request

POST Request Payout Reference
POST https://payoutsapi.ozow.com/v1/requestpayout
SiteCode: YOUR_SITE_CODE
ApiKey: YOUR_API_KEY
Content-Type: application/json

Request example

{
  "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

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

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

{
  "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 tokenBearer token An access token sent in the Authorization header as Authorization: Bearer <token>. Anyone holding the token can use it, which is why it belongs on your server and never in a browser or a mobile app.RFC 6750 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:

{
  "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 idempotentlyIdempotency A request is idempotent when sending it twice has the same effect as sending it once. It matters most where a retry after a timeout could otherwise take a payment twice.IETF draft, receiving the same notification twice must not result in double-crediting or double-debiting.


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

GET Get Payout Reference
GET https://payoutsapi.ozow.com/v1/getpayout?payoutId={payoutId}
SiteCode: YOUR_SITE_CODE
ApiKey: YOUR_API_KEY

By merchant reference

POST Get Payouts by Reference Reference
POST https://payoutsapi.ozow.com/v1/getpayoutbyreference
SiteCode: YOUR_SITE_CODE
ApiKey: YOUR_API_KEY
{
  "pageSize": 10,
  "pageIndex": 1,
  "searchFields": [1],
  "searchString": "YOUR_MERCHANT_REFERENCE"
}

Response example

{
  "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


Next steps

In the API reference

5 entries

Last updated