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

Before you start

N.B. Please ensure that the relevant SiteCode and ApiKey are included in the headers for all requests to the Payouts endpoints

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 = "[ENCRYPTION KEY]";
var plaintextAccountNumber = "[PLAIN TEXT ACCOUNT NUMBER]";
var merchantReference = "[MERCHANT REFERENCE]";
var payoutAmount = 1.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
$plaintext_encryption_key = "[ENCRYPTION KEY]"; 
$plain_text_account_number = "[PLAIN TEXT ACCOUNT NUMBER]";
$amount = [AMOUNT IN CENTS];  //Amount * 100
$merchant_Reference = "[MERCHANT REFERENCE]";

$cipher="AES-256-CBC";


////////////////////////////////////////////////////
// Format IV string
////////////////////////////////////////////////////

//get the IV string
$string_for_iv = $merchRef.$amount.$plaintext_encryption_key;

//lower case iv string
$string_for_iv_lowered = strtolower($string_for_iv);

//get sha512 string
$string_for_iv_hashed = hash('sha512', $string_for_iv_lowered, false);

//get only 16 chars
$iv = substr($string_for_iv_hashed, 0, 16);


////////////////////////////////////////////////////
// Format encryption key
////////////////////////////////////////////////////

//ensure key is 32 chars for the key
while( strlen($plaintext_encryption_key) < 32) {  
  $plaintext_encryption_key = $plaintext_encryption_key.$plaintext_encryption_key;
}

//get only 32 chars for the key as per AES protocol
$encryption_Key_shortened = substr($plaintext_encryption_key, 0, 32);


////////////////////////////////////////////////////
// Perform encryption
////////////////////////////////////////////////////
$encryptedAccountNumber_raw = openssl_encrypt($plain_text_account_number, $cipher, $encryption_Key_shortened, $options=OPENSSL_RAW_DATA, $iv);
$encryptedAccountNumberBase64 = base64_encode($encryptedAccountNumber_raw);
echo 'Encrypted account number: '.$encryptedAccountNumberBase64;

////////////////////////////////////////////////////
// Decryption
////////////////////////////////////////////////////
$encrNumdecoded = base64_decode($encryptedAccountNumberBase64);
$original_accountNumber = openssl_decrypt($encrNumdecoded, $cipher, $encryption_Key_shortened, $options=OPENSSL_RAW_DATA, $iv);

echo 'Original account number : '.$original_accountNumber;

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

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

var plaintextEncryptionKey = "[ENCRYPTION KEY]";
var plaintextAccountNumber = "[PLAIN TEXT ACCOUNT NUMBER]";
var merchantReference = "[MERCHANT REFERENCE]";
var payoutAmount = [AMOUNT];

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

console.log("Hash before encryption: " + ivString);
console.log("Encrypted account number: " + encryptedAccountNumber);

function encryptAes(data, key, ivString) {
  var dataBytes = Buffer.from(data, "utf8");
  var encryptedBytes;
  var iv = getSha512Hash(ivString.toLowerCase()).substring(0, 16);

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

  var aes = crypto.createCipheriv("aes-256-cbc", key.substring(0, 32), iv);
  encryptedBytes = Buffer.concat([aes.update(dataBytes), aes.final()]);
  aes = null;

  return encryptedBytes.toString("base64");
}

function decryptAes(encrypted, key, ivString) {
  encrypted = encrypted.replace(/\s/g, "+");

  var encryptedBytes = Buffer.from(encrypted, "base64");
  var dataBytes;
  var iv = getSha512Hash(ivString.toLowerCase()).substring(0, 16);

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

  var aes = crypto.createDecipheriv("aes-256-cbc", key.substring(0, 32), iv);
  dataBytes = Buffer.concat([aes.update(encryptedBytes), aes.final()]);
  aes = null;

  return dataBytes.toString("utf8");
}

function getSha512Hash(stringToHash) {
  var alg = crypto.createHash("sha512");
  var hash = alg.update(stringToHash, "utf8").digest("hex");

  return hash;
}

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 = "[ENCRYPTION KEY]"
plaintextAccountNumber = "[PLAIN TEXT ACCOUNT NUMBER]"
merchantReference = "[MERCHANT REFERENCE]"
payoutAmount = [AMOUNT]

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

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


Available banks list

In order to get a list of available banks you can invoke the following endpoint:

{baseUrl}/v1/getavailablebanks

For RTC Banks only you can add the "rtconly" parameter to return a filtered list:

{baseUrl}/v1/getavailablebanks?rtconly=true - For RTC Bank list

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

BankDetails Object

PropertyTypeReq.Description
BankGroupIdGuidYesThe unique bank identifier.
AccountNumberString (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.

GetAvailableBanks Example

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)

Payout Request Hash Calculation

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

GeneratePayoutRequestHash();

void GeneratePayoutRequestHash()
{    
    var siteCode = "[YOUR SITE CODE]";
    var amount = [AMOUNT];
    var MerchantReference = "[MERCHANT REFERENCE]";
    var CustomerBankReference = "[CUSTOMER BANK REFERENCE]";
    var apiKey = "[YOUR API KEY]";
    var IsRtc = false;
    var NotifyUrl = "[NOTIFY URL]";
    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() {

    echo "Before generatePayoutRequestHash";
    $payoutRequest = [
        'SiteCode' => '[SITECODE]',
        'Amount' => 0.01,
        'MerchantReference' => '[MERCHANT REFERENCE]',
        'CustomerBankReference' => '[CUSTOMER BANK REFERENCE]',
        'IsRtc' => 'false',
        'NotifyUrl' => '[NOTIFY URL]',
        'BankingDetails' => [
            'BankGroupId' => '[BANKGROUPID]',
            'AccountNumber' => '[ENCRYPTED ACCOUNT NUMBER]',
            'BranchCode' => '[BRANCH CODE]'
        ]
    ];

    $apiKey = '[API KEY]';

    $inputString = implode('', [
        $payoutRequest['SiteCode'],
        intval($payoutRequest['Amount']*100),
        $payoutRequest['MerchantReference'],
        $payoutRequest['CustomerBankReference'],
        $payoutRequest['IsRtc'],
        $payoutRequest['NotifyUrl'],
        $payoutRequest['BankingDetails']['BankGroupId'],
        $payoutRequest['BankingDetails']['AccountNumber'],
        $payoutRequest['BankingDetails']['BranchCode'],        
        $apiKey
    ]);

    echo "Before Hashcheck: $inputString\n";
    $calculatedHashResult = generateHashCheck($inputString);
    echo "Hashcheck: $calculatedHashResult\n";

    $payoutRequest['HashCheck'] = $calculatedHashResult;

    echo "request: " . json_encode($payoutRequest) . "\n";
}

function generateHashCheck($inputString) {
    $stringToHash = strtolower($inputString);
    echo "Before Hashcheck: $stringToHash\n";
    return getSha512Hash($stringToHash);
}

function getSha512Hash($stringToHash) {
    $bytes = hash('sha512', $stringToHash, true);
    $hexString = bin2hex($bytes);
    return $hexString;
}

function testGeneratePayoutRequestHash() {
    echo "Running testGeneratePayoutRequestHash...\n";
       
    $output = generatePayoutRequestHash();   
}

// Run the test
testGeneratePayoutRequestHash();

?>
function GeneratePayoutRequestHash() {
var siteCode = "[YOUR SITE CODE]";
var amount = 17.15;
var MerchantReference = "[MERCHANT REFERENCE]";
var CustomerBankReference = "[CUSTOMER BANK REFERENCE]";
var apiKey = "[YOUR API KEY]";
var IsRtc = false;
var NotifyUrl = "[NOTIFY URL]";
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": ""
   }
}