Step 2: Submit Payout request

After a customer has chosen Ozow as their preferred payout method and has reviewed the payout availability, you will need to make a post call to the following end point:

{baseUrl}/requestpayout

Post variables

Property TypeReq.Description
1.SiteCodeString (50)YesA 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 - [email protected]]
2.AmountDecimal (9,2)YesThe payout amount.
3.MerchantReferenceString (20)YesThe merchant's reference for the transaction.
4.CustomerBankReferenceString (20) YesThe 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.
5.IsRTCboolYesWhether 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
6.NotifyUrlString (150)No The URL that we should use to post all payout notifications.
7.BankingDetailsObjectYesPayout destination banking details. Refer to table below.
8.HashCheckString (250) YesSHA512 hash used to ensure that certain fields in the message have not been altered after the hash was generated. Check the generate hash section below for more details on how to generate the hash.

Account number encryption

For security and verification purposes, Ozow requires that the merchant encrypts the destination account number using a unique encryption key per payout request, as described below:

  1. The Advanced Encryption Standard (AES) encryption standard should be used to encrypt the destination bank account. More information can be obtained on Wikipedia and on Microsoft’s .Net Cryptography site.
  2. The following AES parameters should be used:
    • Key size: 256
    • Block Cypher Mode of operation: Cipher block chaining (CBC)
    • Padding: PKCS7
  3. The Initialization vector (IV) should be an SHA512 hash of the following (Note: the IV hash length should be 16 bytes. If the SHA512 has is longer than 16 bytes then the first 16 bytes should be used as the IV):
    • Key size: 256
    • Block Cypher Mode of operation: Cipher block chaining (CBC)
    • Padding: PKCS7
  4. The merchant will need to persist the encryption key per payout request
using System.Security.Cryptography;
using System.Text;

Console.WriteLine("Encrypting Payout Account Number");

var plaintextEncryptionKey = "E(H+MbQeThWmYq3t6w9z$C&F)[email protected]!A%D*G-KaPdSgVkYp3s6v8y";
var plaintextAccountNumber = "063228099";
var merchantReference = "ThisTest";
var payoutAmount = 10.00M;

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

Console.WriteLine($"Hash before encryption: {ivString}");
Console.WriteLine($"Encrypted account number: {encryptedAccountNumber}");

string EncryptAes(string data, string key, string ivString)
{
    byte[] dataBytes = Encoding.UTF8.GetBytes(data);
    byte[] encryptedBytes;
    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();

        encryptedBytes = encryptor.TransformFinalBlock(dataBytes, 0, dataBytes.Length);
        aes.Clear();
    }

    return Convert.ToBase64String(encryptedBytes, 0, encryptedBytes.Length);
}

string DecryptAes(string encrypted, string key, string ivString)
{
    encrypted = encrypted.Replace(' ', '+');

    var encryptedBytes = Convert.FromBase64String(encrypted);
    byte[] dataBytes;
    var 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 decryptor = aes.CreateDecryptor();

        dataBytes = decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length);
        aes.Clear();
    }

    return Encoding.UTF8.GetString(dataBytes);
}

string GetSha512Hash(string stringToHash)
{
    using (SHA512 alg = new SHA512CryptoServiceProvider())
    {
        byte[] bytes = alg.ComputeHash(Encoding.UTF8.GetBytes(stringToHash));

        var sb = new StringBuilder();
        foreach (byte b in bytes)
        {
            var hex = b.ToString("x2");
            sb.Append(hex);
        }

        return sb.ToString();
    }
}
<?php

echo "Encrypting Payout Account Number\n";

$plaintextEncryptionKey = "E(H+MbQeThWmYq3t6w9z\$C&F)[email protected]!A%D*G-KaPdSgVkYp3s6v8y";
$plaintextAccountNumber = "063228099";
$merchantReference = "ThisTest";
$payoutAmount = 10.00;

$ivString = $merchantReference . intval($payoutAmount * 100) . $plaintextEncryptionKey;
$encryptedAccountNumber = encryptAes($plaintextAccountNumber, $plaintextEncryptionKey, $ivString);

echo "Hash before encryption: $ivString\n";
echo "Encrypted account number: $encryptedAccountNumber\n";

function encryptAes($data, $key, $ivString) {
    $dataBytes = mb_convert_encoding($data, 'UTF-8');
    $encryptedBytes;
    $iv = substr(getSha512Hash(strtolower($ivString)), 0, 16);

    while (strlen($key) < 32) {
        $key .= $key;
    }

    $aes = new \phpseclib\Crypt\AES();
    $aes->setKey(mb_convert_encoding(substr($key, 0, 32), 'UTF-8'));
    $aes->setIV(mb_convert_encoding($iv, 'UTF-8'));

    $encryptedBytes = $aes->encrypt($dataBytes);

    return base64_encode($encryptedBytes);
}

function decryptAes($encrypted, $key, $ivString) {
    $encrypted = str_replace(' ', '+', $encrypted);

    $encryptedBytes = base64_decode($encrypted);
    $dataBytes;
    $iv = substr(getSha512Hash(strtolower($ivString)), 0, 16);

    while (strlen($key) < 32) {
        $key .= $key;
    }

    $aes = new \phpseclib\Crypt\AES();
    $aes->setKey(mb_convert_encoding(substr($key, 0, 32), 'UTF-8'));
    $aes->setIV(mb_convert_encoding($iv, 'UTF-8'));

    $dataBytes = $aes->decrypt($encryptedBytes);

    return mb_convert_encoding($dataBytes, 'UTF-8');
}

function getSha512Hash($stringToHash) {
    $bytes = hash('sha512', mb_convert_encoding($stringToHash, 'UTF-8'), true);

    $sb = '';
    foreach (str_split($bytes) as $b) {
        $hex = bin2hex($b);
        $sb .= $hex;
    }

    return $sb;
}

?> 
Note: Use the phpseclib library for AES encryption and decryption.

// No direct equivalent for System.Security.Cryptography in JavaScript

console.log("Encrypting Payout Account Number");

const plaintextEncryptionKey = "E(H+MbQeThWmYq3t6w9z$C&F)[email protected]!A%D*G-KaPdSgVkYp3s6v8y";
const plaintextAccountNumber = "063228099";
const merchantReference = "ThisTest";
const payoutAmount = 10.00;

const ivString = merchantReference + parseInt(payoutAmount * 100) + plaintextEncryptionKey;
const encryptedAccountNumber = encryptAes(plaintextAccountNumber, plaintextEncryptionKey, ivString);

console.log(Hash before encryption: ${ivString});
console.log(Encrypted account number: ${encryptedAccountNumber});

function encryptAes(data, key, ivString) {
const dataBytes = new TextEncoder().encode(data);
let encryptedBytes;
const iv = getSha512Hash(ivString.toLowerCase()).substring(0, 16);

while (key.length < 32) {
key += key;
}

const aes = new aesjs.ModeOfOperation.cbc(
new TextEncoder().encode(key.substring(0, 32)),
new TextEncoder().encode(iv)
);

encryptedBytes = aes.encrypt(dataBytes);

return aesjs.utils.hex.fromBytes(encryptedBytes);
}

function decryptAes(encrypted, key, ivString) {
encrypted = encrypted.replace(' ', '+');

const encryptedBytes = aesjs.utils.hex.toBytes(encrypted);
let dataBytes;
const iv = getSha512Hash(ivString.toLowerCase()).substring(0, 16);

while (key.length < 32) {
key += key;
}

const aes = new aesjs.ModeOfOperation.cbc(
new TextEncoder().encode(key.substring(0, 32)),
new TextEncoder().encode(iv)
);

dataBytes = aes.decrypt(encryptedBytes);

return new TextDecoder().decode(dataBytes);
}

function getSha512Hash(stringToHash) {
const alg = new sha512();
const bytes = alg.digest(stringToHash);
let sb = '';

for (let i = 0; i < bytes.length; i++) {
let hex = bytes[i].toString(16);
if (hex.length === 1) {
hex = '0' + hex;
}
sb += hex;
}

return sb;
}
import hashlib
import base64
from Crypto.Cipher import AES

def encrypt_aes(data, key, ivString):
dataBytes = bytes(data, 'utf-8')
encryptedBytes = b''
iv = get_sha512_hash(ivString.lower())[0:16]

while len(key) < 32:
    key += key

cipher = AES.new(key[0:32].encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))

encryptedBytes = cipher.encrypt(pad(dataBytes))

return base64.b64encode(encryptedBytes).decode('utf-8')

def decrypt_aes(encrypted, key, ivString):
encrypted = encrypted.replace(' ', '+')
encryptedBytes = base64.b64decode(encrypted)
dataBytes = b''
iv = get_sha512_hash(ivString.lower())[0:16]

while len(key) < 32:
    key += key

cipher = AES.new(key[0:32].encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))

dataBytes = unpad(cipher.decrypt(encryptedBytes))

return dataBytes.decode('utf-8')

def get_sha512_hash(stringToHash):
alg = hashlib.sha512()
alg.update(stringToHash.encode('utf-8'))
return alg.hexdigest()

def pad(s):
return s + (16 - len(s) % 16) * chr(16 - len(s) % 16)

def unpad(s):
return s[:-ord(s[len(s)-1:])]

print("Encrypting Payout Account Number")

plaintextEncryptionKey = "E(H+MbQeThWmYq3t6w9z$C&F)[email protected]!A%D*G-KaPdSgVkYp3s6v8y"
plaintextAccountNumber = "063228099"
merchantReference = "ThisTest"
payoutAmount = 10.00

ivString = merchantReference + str(int(payoutAmount * 100)) + plaintextEncryptionKey
encryptedAccountNumber = encrypt_aes(plaintextAccountNumber, plaintextEncryptionKey, ivString)

print("Hash before encryption: " + ivString)
print("Encrypted account number: " + encryptedAccountNumber)


BankDetailsObject

PropertyTypeReq.Description
BankGroupIdGuidYesThe unique bank identifier.
AccountNumberStrong (32)YesThe bank account number the payment should be made to. The account number should be encrypted by the encryption method detailed above.
BranchCodeString (10)YesThe destination bank branch code.

Request Example

{baseUrl}/getavailablebanks

using RestSharp;

const string apiKey = "[YOUR API KEY]";
const string siteCode = "[YOUR SITE CODE]";
const string baseUrl = "{baseUrl}/getavailablebanks";

var client = new RestClient(baseUrl);
client.Options.MaxTimeout = -1;
var request = new RestRequest(baseUrl, Method.Get);
request.AddHeader("ApiKey", apiKey);
request.AddHeader("SiteCode", siteCode);
request.AddHeader("Accept", "application/json");
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddParameter("SiteCode", siteCode);
var response = await client.ExecuteAsync(request);
Console.WriteLine(response.Content);
<?php

use RestSharp;

$apiKey = "[YOUR API KEY]";
$siteCode = "[YOUR SITE CODE]";
$baseUrl = "{baseUrl}/getavailablebanks";

$client = new RestClient($baseUrl);
$client->Options->MaxTimeout = -1;
$request = new RestRequest($baseUrl, Method::GET);
$request->AddHeader("ApiKey", $apiKey);
$request->AddHeader("SiteCode", $siteCode);
$request->AddHeader("Accept", "application/json");
$request->AddHeader("Content-Type", "application/x-www-form-urlencoded");
$request->AddParameter("SiteCode", $siteCode);
$response = $client->Execute($request);
echo $response->Content;

?>
var apiKey = "[YOUR API KEY]";
var siteCode = "[YOUR SITE CODE]";
var baseUrl = "{baseUrl}/getavailablebanks";

var client = new XMLHttpRequest();
client.timeout = -1;
var request = new XMLHttpRequest();
request.open("GET", baseUrl, true);
request.setRequestHeader("ApiKey", apiKey);
request.setRequestHeader("SiteCode", siteCode);
request.setRequestHeader("Accept", "application/json");
request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
request.send(SiteCode=${siteCode});
request.onreadystatechange = function() {
if (request.readyState === 4 && request.status === 200) {
console.log(request.responseText);
}
}
import requests

apiKey = "[YOUR API KEY]"
siteCode = "[YOUR SITE CODE]"
baseUrl = "{baseUrl}/getavailablebanks"

client = requests.Session()
client.headers.update({'ApiKey': apiKey, 'SiteCode': siteCode, 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded'})
client.params.update({'SiteCode': siteCode})
response = client.get(baseUrl)
print(response.content)

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"
    }
]

Payout Request Hash Calculation

using System.Security.Cryptography;
using System.Text;

GeneratePayoutRequestHash();

void GeneratePayoutRequestHash()
{    
    var siteCode = "[YOUR SITE CODE]";
    var amount = 17.15;
    var MerchantReference = "123";
    var CustomerBankReference = "ABC123";
    var apiKey = "[YOUR API KEY]";
    var IsRtc = false;
    var NotifyUrl = "https://requestcatcher.com/";
    var BankGroupId = "13999FA-3A32-4E3D-82F0-A1DF7E9E4F7B";
    var AccountNumber = "ff313a955ad9a8ddff32cb734d49fbcddd8eeb1e235009d59a801bc5af78270cfd";
    var BranchCode = "198765";
    var inputString = string.Concat(
        siteCode,
        Convert.ToInt32(amount * 100),
        MerchantReference,
        CustomerBankReference,
        IsRtc,
        NotifyUrl,
        BankGroupId,
        AccountNumber,
        BranchCode,
        apiKey);

    var calculatedHashResult = GenerateRequestHashCheck(inputString);
    Console.WriteLine($"Hashcheck: {calculatedHashResult}");
 }

string GenerateRequestHashCheck(string inputString)
{
    var stringToHash = inputString.ToLower();
    Console.WriteLine($"Before Hashcheck: {stringToHash}");

    return GetSha512Hash(stringToHash);
}

string GetSha512Hash(string stringToHash)
{
    using SHA512 alg = new SHA512CryptoServiceProvider();
    var bytes = alg.ComputeHash(Encoding.UTF8.GetBytes(stringToHash));

    var sb = new StringBuilder();
    foreach (var b in bytes)
    {
        var hex = b.ToString("x2");
        sb.Append(hex);
    }

    return sb.ToString();
}
<?php

function GeneratePayoutRequestHash() {
    $siteCode = "[YOUR SITE CODE]";
    $amount = 17.15;
    $MerchantReference = "123";
    $CustomerBankReference = "ABC123";
    $apiKey = "cNNd60%fUHV4kagv87k4zPT6";
    $IsRtc = false;
    $NotifyUrl = "https://requestcatcher.com/";
    $BankGroupId = "13999FA-3A32-4E3D-82F0-A1DF7E9E4F7B";
    $AccountNumber = "ff313a955ad9a8ddff32cb734d49fbcddd8eeb1e235009d59a801bc5af78270cfd";
    $BranchCode = "198765";
    $inputString = $siteCode . (int)($amount * 100) . $MerchantReference . $CustomerBankReference . $IsRtc . $NotifyUrl . $BankGroupId . $AccountNumber . $BranchCode . $apiKey;

    $calculatedHashResult = GenerateRequestHashCheck($inputString);
    echo "Hashcheck: " . $calculatedHashResult;
}

function GenerateRequestHashCheck($inputString) {
    $stringToHash = strtolower($inputString);
    echo "Before Hashcheck: " . $stringToHash;

    return GetSha512Hash($stringToHash);
}

function GetSha512Hash($stringToHash) {
    $alg = hash_init("sha512");
    hash_update($alg, $stringToHash);
    $bytes = hash_final($alg, true);

    $sb = "";
    for ($i = 0; $i < strlen($bytes); $i++) {
        $hex = dechex(ord($bytes[$i]));
        $sb .= str_pad($hex, 2, "0", STR_PAD_LEFT);
    }

    return $sb;
}

GeneratePayoutRequestHash();

?>
function GeneratePayoutRequestHash() {
var siteCode = "[YOUR SITE CODE]";
var amount = 17.15;
var MerchantReference = "123";
var CustomerBankReference = "ABC123";
var apiKey = "[YOUR API KEY]";
var IsRtc = false;
var NotifyUrl = "https://requestcatcher.com/";
var BankGroupId = "13999FA-3A32-4E3D-82F0-A1DF7E9E4F7B";
var AccountNumber = "ff313a955ad9a8ddff32cb734d49fbcddd8eeb1e235009d59a801bc5af78270cfd";
var BranchCode = "198765";
  
var inputString = siteCode +
parseInt(amount * 100) +
MerchantReference +
CustomerBankReference +
IsRtc +
NotifyUrl +
BankGroupId +
AccountNumber +
BranchCode +
apiKey;

var calculatedHashResult = GenerateRequestHashCheck(inputString);
console.log("Hashcheck: " + calculatedHashResult);
}

function GenerateRequestHashCheck(inputString) {
var stringToHash = inputString.toLowerCase();
console.log("Before Hashcheck: " + stringToHash);

return GetSha512Hash(stringToHash);
}

function GetSha512Hash(stringToHash) {
var crypto = require('crypto');
var sha512 = crypto.createHash('sha512');
var bytes = sha512.update(stringToHash, 'utf8');

return bytes.digest('hex');
}

GeneratePayoutRequestHash();
import hashlib
import string

def GeneratePayoutRequestHash():
siteCode = "[YOUR SITE CODE]"
amount = 17.15
MerchantReference = "123"
CustomerBankReference = "ABC123"
apiKey = "[YOUR API KEY]"
IsRtc = False
NotifyUrl = "https://requestcatcher.com/"
BankGroupId = "13999FA-3A32-4E3D-82F0-A1DF7E9E4F7B"
AccountNumber = "ff313a955ad9a8ddff32cb734d49fbcddd8eeb1e235009d59a801bc5af78270cfd"
BranchCode = "198765"
inputString = ''.join([
siteCode,
str(int(amount * 100)),
MerchantReference,
CustomerBankReference,
str(IsRtc),
NotifyUrl,
BankGroupId,
AccountNumber,
BranchCode,
apiKey
])

calculatedHashResult = GenerateRequestHashCheck(inputString)
print("Hashcheck: " + calculatedHashResult)

def GenerateRequestHashCheck(inputString):
stringToHash = inputString.lower()
print("Before Hashcheck: " + stringToHash)

return GetSha512Hash(stringToHash)

def GetSha512Hash(stringToHash):
sha = hashlib.sha512()
sha.update(stringToHash.encode())

return sha.hexdigest()

GeneratePayoutRequestHash()




Response

PayoutRequestResult - A successful call will return a PayoutRequestResult object. The PayoutRequestResult object is described below.

PropertyTypeDescription
PayoutIdGuidA unique identifier that should be used to identify the payout.
PayoutStatusObjectPayout status. Refer to table below.

PayoutStatus Object

Property TypeDescription
StatusIntThe payout status. Possible values are:
1: PayoutReceived – The payout has been received.
2: Verification – The payout is being verified.
3: SubmittedForProcessing – The payout is being processed.
4: PayoutProcessingError – There was an error with the payout.
5: PayoutComplete – The payout has been completed.
6: PayoutPendingInvestigation – The payout is being investigated.
90: PayoutReturned – The payout could not be paid into recipient account.
99: PayoutCancelled – The payout was cancelled.
SubStatusIntThe payout sub status. Possible values are:
100: Payout_Unclassified – No sub status.
101: Payout_ValidationFailed – Request validation
failed and error description will be in the ErrorMessage field.
201: Verification_Pending – Awaiting webook verification.
202: Verification_Failed – The verification webhook returned a failed.
203: Verification_Success – Successful payout verification via webhook.
204: Verification_Error – Unable to reach the verification webhook.
205: Verification_AccountNumberDecryptionFailed – Decryption of the account number failed using the key received via webhook.
301: SubmittedForProcessing_PayoutAddedToBatch – The payout has been added to the payout batch.
302: SubmittedForProcessing_PayoutSubmittedToBank – The payout batch has been processed and submitted to the bank.
303: SubmittedForProcessing_PayoutSubmittedToPpi – Payout submitted for processing.
401: PayoutProcessingError_PayoutRejected – The payout has been rejected by the bank.
402: PayoutProcessingError_PayoutCancelled – The payout has been cancelled.
403: PayoutProcessingError_Insufficient_Balance – Insufficient balance.
404: PayoutProcessingError_PayoutInternalError.
405: PayoutProcessingError_InvalidAccountNumber – The payout has an invalid account number.
601: PayoutPendingInvestigation_AmountMismatch – The payout failed due to mismatch in amounts.
9001: PayoutReturned_Unpaid – Rejected by destination bank.
9901: Cancellation_AddedToBatch – Cancellation request added to batch for processing.
9902: Cancellation_SubmittedToBank – Cancellation request has been submitted to the bank.
9903: Cancellation_RejectedByBank – Cancellation request has been rejected by the bank.
9904 - Cancellation_AccountNumberValidationFailed – CDV account number validation failed.
ErrorMessageString (250)Error message generated when validating the request.

Response Example

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