Redirect to Ozow
Build a redirect payin on the Payments API, the legacy path. Post the payment, redirect the customer, and handle the notification response.
On this page14 sections
- Before you start
- Environments
- How redirect works
- Core integration
- Step 1: Generate the hash check
- Step 2: Create a payment request
- Step 3: Redirect the customer
- Step 4: Handle the notification response
- Step 5: Confirm the transaction outcome
- Step 6: Cancel a payment
- Optional features
- Standalone button
- Customer Identity Verification
- Next steps
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.
- Migrate a payin from the Payments API to One APIEverything needed to move an existing redirect payin onto One API, with the legacy guide and its One API counterpart side by side.
This guide walks you through a redirectRedirect Sending the payer to the Ozow payment page to complete the payment, and returning them to your site afterwards. The alternative is embedding the checkout in your own page, where the payer never leaves it. payinPayin 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. integration using the Payments API. Your system posts payment information to Ozow, redirects the customer to the Ozow-hosted payment page, and receives a notification response when the payment is complete.
Legacy integration: use One API for new integrations
Payments API is a legacy integration path and will not receive new features. If you are starting a new payin integration, use Redirect: One API instead. This guide exists to support merchants already integrated on Payments API.
Before you start
- You have completed Prerequisites and onboarding
- You have your API key, private 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. from your Ozow Dashboard
- Your notification URLWebhook 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., success URL, cancel URL, and error URL are set up and publicly accessible via HTTPS
Environments
| Environment | API endpoint | Dashboard |
|---|---|---|
| Production | https:/ |
dash.ozow.com |
| Staging | https:/ |
stagingdash.ozow.com |
How redirect works
Core integration
Step 1: Generate the hash check
The Payments API uses 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-based authentication. Before posting a payment request, you must generate a hash check to sign the request.
How to generate the hash check
Critical, field order matters
The fields must be concatenated in exactly the order listed in the request fields table below. Using the wrong order is the most common cause of hash check failures. Only include fields that have a value, empty or unused fields must be excluded from the hash entirely, not included as empty strings.
- Concatenate the post variables (excluding
HashCheckandToken) in the order they appear in the request fields table below - Append your private key to the concatenated string
- Convert the entire string to lowercase
- Generate a SHA512 hash of the lowercase string
Note
Boolean values must be represented as the strings true or false in the
concatenated string. Some languages may convert booleans to 0 or 1 which will result in a
failed hash check.
Important
The amount must be formatted with exactly two decimal places, 100.00 rather
than 100 or 100.0. Most languages drop a trailing zero when a number is converted to a string,
so format the amount before you concatenate it. This is the most common cause of a failed hash
check.
Hash check example
Given the following values:
| Field | Value |
|---|---|
| SiteCode | TSTSTE0001 |
| CountryCode | ZA |
| CurrencyCode | ZARZAR The ISO 4217 code for the South African rand, and the currency every amount on this site is in unless a page says otherwise. Amounts are decimal rand rather than cents, so 100.00 is one hundred rand. |
| Amount | 25.00 |
| TransactionReference | 123 |
| BankReference | ABC123 |
| CancelUrl | http:/ |
| ErrorUrl | http:/ |
| SuccessUrl | http:/ |
| NotifyUrl | http:/ |
| IsTest | false |
The concatenated string before hashing would be:
tstste0001zazar25.00123abc123http://demo.ozow.com/cancel.aspxhttp://demo.ozow.com/error.aspxhttp://demo.ozow.com/success.aspxhttp://demo.ozow.com/notify.aspxfalse[your private key]
Resulting hash:
4a2e7db32f76747b0f434edeb62e8b3ebb04125025feae622bc09092296bc965cec49a8cbeeef0e21f3c4c04249de69dd0b330a5b7da21c51a95d360d03f54ba
Code examples
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
void GenerateRequestHash()
{
string siteCode = "YOUR_SITE_CODE";
string countryCode = "ZA";
string currencyCode = "ZAR";
string amount = 100.00M.ToString("0.00", CultureInfo.InvariantCulture);
string transactionReference = "ORDER-001";
string bankReference = "ABC123";
string cancelUrl = "https://yourstore.com/cancel";
string errorUrl = "https://yourstore.com/error";
string successUrl = "https://yourstore.com/success";
string notifyUrl = "https://yourstore.com/notify";
string privateKey = "YOUR_PRIVATE_KEY";
bool isTest = false;
string inputString = string.Concat(
siteCode,
countryCode,
currencyCode,
amount,
transactionReference,
bankReference,
cancelUrl,
errorUrl,
successUrl,
notifyUrl,
isTest,
privateKey
)
.ToLower();
using SHA512 sha512 = new SHA512CryptoServiceProvider();
var bytes = sha512.ComputeHash(Encoding.UTF8.GetBytes(inputString));
var hash = BitConverter.ToString(bytes).Replace("-", "").ToLower();
Console.WriteLine($"HashCheck: {hash}");
}
<?php
function generateRequestHash()
{
$siteCode = "YOUR_SITE_CODE";
$countryCode = "ZA";
$currencyCode = "ZAR";
$amount = number_format(100.00, 2, ".", "");
$transactionReference = "ORDER-001";
$bankReference = "ABC123";
$cancelUrl = "https://yourstore.com/cancel";
$errorUrl = "https://yourstore.com/error";
$successUrl = "https://yourstore.com/success";
$notifyUrl = "https://yourstore.com/notify";
$privateKey = "YOUR_PRIVATE_KEY";
$isTest = "false";
$inputString = strtolower(
$siteCode .
$countryCode .
$currencyCode .
$amount .
$transactionReference .
$bankReference .
$cancelUrl .
$errorUrl .
$successUrl .
$notifyUrl .
$isTest .
$privateKey,
);
echo "HashCheck: " . hash("sha512", $inputString) . "\n";
}
generateRequestHash();
?>
const crypto = require("crypto");
function generateRequestHash() {
const siteCode = "YOUR_SITE_CODE";
const countryCode = "ZA";
const currencyCode = "ZAR";
const amount = (100.00).toFixed(2);
const transactionReference = "ORDER-001";
const bankReference = "ABC123";
const cancelUrl = "https://yourstore.com/cancel";
const errorUrl = "https://yourstore.com/error";
const successUrl = "https://yourstore.com/success";
const notifyUrl = "https://yourstore.com/notify";
const privateKey = "YOUR_PRIVATE_KEY";
const isTest = false;
const inputString =
`${siteCode}${countryCode}${currencyCode}${amount}${transactionReference}${bankReference}${cancelUrl}${errorUrl}${successUrl}${notifyUrl}${isTest}${privateKey}`.toLowerCase();
const hash = crypto.createHash("sha512").update(inputString).digest("hex");
console.log(`HashCheck: ${hash}`);
}
generateRequestHash();
import hashlib
def generate_request_hash():
site_code = "YOUR_SITE_CODE"
country_code = "ZA"
currency_code = "ZAR"
amount = f"{100.00:.2f}"
transaction_reference = "ORDER-001"
bank_reference = "ABC123"
cancel_url = "https://yourstore.com/cancel"
error_url = "https://yourstore.com/error"
success_url = "https://yourstore.com/success"
notify_url = "https://yourstore.com/notify"
private_key = "YOUR_PRIVATE_KEY"
is_test = False
input_string = (
str(site_code)
+ str(country_code)
+ str(currency_code)
+ amount
+ str(transaction_reference)
+ str(bank_reference)
+ str(cancel_url)
+ str(error_url)
+ str(success_url)
+ str(notify_url)
+ str(is_test)
+ str(private_key)
).lower()
hash_result = hashlib.sha512(input_string.encode()).hexdigest()
print(f"HashCheck: {hash_result}")
generate_request_hash()
Complete field concatenation order
Only include fields that have a value. Empty or unused fields must be excluded.
| Position | Field | Required |
|---|---|---|
| 1 | site |
Yes |
| 2 | country |
Yes |
| 3 | currency |
Yes |
| 4 | amount |
Yes |
| 5 | transaction |
Yes |
| 6 | bank |
Yes |
| 7 | optional1 |
No |
| 8 | optional2 |
No |
| 9 | optional3 |
No |
| 10 | optional4 |
No |
| 11 | optional5 |
No |
| 12 | customer |
No |
| 13 | cancel |
No |
| 14 | error |
No |
| 15 | success |
No |
| 16 | notify |
No |
| 17 | is |
Yes |
| 18 | selected |
No |
| 19 | bank |
No |
| 20 | branch |
No |
| 21 | bank |
No |
| 22 | payee |
No |
| 23 | expiry |
No |
| 24 | allow |
No |
| 25 | variable |
No |
| 26 | variable |
No |
| 27 | customer |
No |
| 28 | customer |
No |
| 29 | hash |
Yes: do not include in hash |
Step 2: Create a payment request
Post the payment request to the Ozow API to generate a payment URL.
POST https://api.ozow.com/postpaymentrequest
ApiKey: YOUR_API_KEY
Content-Type: application/json
Accept: application/json
curl -X POST "https://api.ozow.com/postpaymentrequest" \
-H "Accept: application/json" \
-H "ApiKey: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"siteCode": "YOUR_SITE_CODE",
"countryCode": "ZA",
"currencyCode": "ZAR",
"amount": "100.00",
"transactionReference": "ORDER-001",
"bankReference": "ABC123",
"cancelUrl": "https://yourstore.com/cancel",
"errorUrl": "https://yourstore.com/error",
"successUrl": "https://yourstore.com/success",
"notifyUrl": "https://yourstore.com/notify",
"isTest": false,
"hashCheck": "YOUR_GENERATED_HASH"
}'
var client = new RestClient("https://api.ozow.com/postpaymentrequest");
var request = new RestRequest(Method.POST);
request.AddHeader("Accept", "application/json");
request.AddHeader("ApiKey", "YOUR_API_KEY");
request.AddHeader("Content-Type", "application/json");
var data = new
{
siteCode = "YOUR_SITE_CODE",
countryCode = "ZA",
currencyCode = "ZAR",
amount = "100.00",
transactionReference = "ORDER-001",
bankReference = "ABC123",
cancelUrl = "https://yourstore.com/cancel",
errorUrl = "https://yourstore.com/error",
successUrl = "https://yourstore.com/success",
notifyUrl = "https://yourstore.com/notify",
isTest = false,
hashCheck = "YOUR_GENERATED_HASH",
};
request.AddParameter(
"application/json",
JsonConvert.SerializeObject(data),
ParameterType.RequestBody
);
IRestResponse response = client.Execute(request);
Console.WriteLine(response.Content);
<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.ozow.com/postpaymentrequest",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
"siteCode" => "YOUR_SITE_CODE",
"countryCode" => "ZA",
"currencyCode" => "ZAR",
"amount" => "100.00",
"transactionReference" => "ORDER-001",
"bankReference" => "ABC123",
"cancelUrl" => "https://yourstore.com/cancel",
"errorUrl" => "https://yourstore.com/error",
"successUrl" => "https://yourstore.com/success",
"notifyUrl" => "https://yourstore.com/notify",
"isTest" => false,
"hashCheck" => "YOUR_GENERATED_HASH",
]),
CURLOPT_HTTPHEADER => [
"Accept: application/json",
"ApiKey: YOUR_API_KEY",
"Content-Type: application/json",
],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
?>
const data = {
siteCode: "YOUR_SITE_CODE",
countryCode: "ZA",
currencyCode: "ZAR",
amount: "100.00",
transactionReference: "ORDER-001",
bankReference: "ABC123",
cancelUrl: "https://yourstore.com/cancel",
errorUrl: "https://yourstore.com/error",
successUrl: "https://yourstore.com/success",
notifyUrl: "https://yourstore.com/notify",
isTest: false,
hashCheck: "YOUR_GENERATED_HASH",
};
fetch("https://api.ozow.com/postpaymentrequest", {
method: "POST",
headers: {
"Accept": "application/json",
"ApiKey": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
.then((response) => response.text())
.then((data) => console.log(data));
import requests
import json
data = {
"siteCode": "YOUR_SITE_CODE",
"countryCode": "ZA",
"currencyCode": "ZAR",
"amount": "100.00",
"transactionReference": "ORDER-001",
"bankReference": "ABC123",
"cancelUrl": "https://yourstore.com/cancel",
"errorUrl": "https://yourstore.com/error",
"successUrl": "https://yourstore.com/success",
"notifyUrl": "https://yourstore.com/notify",
"isTest": False,
"hashCheck": "YOUR_GENERATED_HASH",
}
response = requests.post(
"https://api.ozow.com/postpaymentrequest",
headers={
"Accept": "application/json",
"ApiKey": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json=data,
)
print(response.text)
Key request fields
| Field | Type | Required | Description |
|---|---|---|---|
site |
string | Yes | Your Ozow site code |
country |
string | Yes | Must be ZA |
currency |
string | Yes | Must be ZAR |
amount |
string | Yes | Payment amount |
transaction |
string | Yes | Your internal order reference |
bank |
string | Yes | The reference that appears on your bank statement for the payment |
cancel |
string | Yes | URL to redirect the customer to if they cancel |
error |
string | Yes | URL to redirect the customer to if an error occurs |
success |
string | Yes | URL to redirect the customer to on successful payment |
notify |
string | Yes | URL Ozow posts the notification response to |
is |
boolean | Yes | Set to false for live payments |
hash |
string | Yes | The SHA512 hash generated in Step 1 |
For the full list of request fields see Post Payment Request.
Successful response
{
"paymentRequestId": "00000000-0000-0000-0000-000000000000",
"url": "https://pay.ozow.com/00000000-0000-0000-0000-000000000000/Secure",
"errorMessage": null
}
Important
A rejected request is also an HTTP 200. A failed hash check, a bank
reference that is too long, or a site code that is not active come back with status 200,
a null url, and the reason in errorMessage. Treat the request as accepted only when
url is populated and errorMessage is null.
Rejected response
{
"paymentRequestId": null,
"url": null,
"errorMessage": "The HashCheck value has failed"
}
Step 3: Redirect the customer
Redirect your customer's browser to the url returned in the response. Ozow displays the payment
page where the customer selects their preferred payment method and completes the payment.
Once the customer completes, cancels, or encounters an error, Ozow redirects them to the applicable
URL you specified in the request; successUrl, cancelUrl, or errorUrl.
Important
Do not use the redirect to successUrl alone as confirmation that a payment was
successful. Always confirm payment status via the verified notification response or an API status
check.
Note
Pay by BankPay by Bank The payer authorises the payment inside their own banking app or online banking, and the funds move from their bank account. No card is involved and no card details are entered. is available by default. Additional payment methods such as Capitec PayCapitec Pay Capitec's own payment method. The payer gives a cellphone, account or ID number rather than card details, and approves the payment in the Capitec app, so no card number and no banking login is ever entered at checkout. It gets its own button rather than sitting inside the bank list, and it requires Customer Identity Verification.Capitec, Buy Now Pay Later, and PayShap RequestPayShap Request The request side of PayShap. Rather than the payer pushing money, the payee asks for it: the payer receives a request and approves it in their own banking app, and the funds move once they do. Enabled by Ozow on request rather than by default.payshap.co.za are enabled by Ozow on request. Once activated, they appear automatically on the payment page with no additional integration work required.
Step 4: Handle the notification response
Ozow posts a notification to your notifyUrl when a transaction completes. The notification is sent
form-encoded with mime-type application/x-www-form-urlencoded.
Example notification
SiteCode=TSTSTE0001&TransactionId=c02dc1c9-a117-45cf-b375-d304cf434a52&TransactionReference=ORDER-001&Amount=100.00&Status=Complete&CurrencyCode=ZAR&IsTest=False&StatusMessage=&Hash=11fa4b11...&SubStatus=Unclassified
Verifying the notification hash
You must verify every notification before acting on it:
- Concatenate the fields below in this order, skipping any that are empty
- Append your private key to the concatenated string
- Convert the entire string to lowercase
- Generate a SHA512 hash of the lowercase string
- Compare your generated hash to the
Hashvalue in the notification
Notification hash field order
| Position | Field |
|---|---|
| 1 | Site |
| 2 | Transaction |
| 3 | Transaction |
| 4 | Amount, with two decimal places |
| 5 | Status |
| 6 | Optional1 |
| 7 | Optional2 |
| 8 | Optional3 |
| 9 | Optional4 |
| 10 | Optional5 |
| 11 | Currency |
| 12 | Is |
| 13 | Status |
| 14 | Your private key |
Important
This is not the same order as the request hash, and it is not the order the
fields appear in the notification body. CurrencyCode comes after the five optional fields here,
and SubStatus and Hash are not part of the hash at all. Concatenate in the order above, not in
the order you read them off the request.
Important
Never update an order status without first verifying the notification hash. Log and alert on verification failures: do not silently discard them.
Step 5: Confirm the transaction outcome
After verifying the notification hash, check the transaction status and update your order.
Transaction statuses
| Status | Description |
|---|---|
Complete |
Payment completed successfully |
Cancelled |
Customer cancelled the payment |
Error |
An error occurred: check Sub for detail |
Pending |
Payment is under review: do not credit until resolved |
SubStatus values
When a payment fails, the SubStatus field provides more detail. See Transaction and settlement
statuses for the full SubStatus list.
You can also confirm transaction status directly via the API at any time:
By transaction reference
GET https://api.ozow.com/GetTransactionByReference?siteCode={siteCode}&transactionReference={transactionReference}
ApiKey: YOUR_API_KEY
By transaction ID
GET https://api.ozow.com/GetTransaction?siteCode={siteCode}&transactionId={transactionId}
ApiKey: YOUR_API_KEY
Note
Handle duplicate notifications 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. Ozow may send the same notification more than once. Processing the same notification twice must not result in double-crediting an order.
Step 6: Cancel a payment
The Payments API does not support programmatic cancellation of a payment request via API. A payment
can only be cancelled by the customer on the Ozow payment page. If the customer cancels, Ozow
redirects them to your cancelUrl.
Optional features
Standalone button
A standalone button lets you surface a specific Ozow payment method as a dedicated button on your checkout page, taking the customer directly to that payment method.
Important
Do not display a standalone button for a payment method until you have received confirmation from Ozow that your account has been enabled for that payment method.
Digital wallets
Apple Pay and Google Pay cannot be offered as standalone buttons using this method. If you want to offer these as dedicated payment options at checkout, use the Wallet SDK.
Implementation
Include the SelectedBankId field in your payment request. The SelectedBankId for each payment
method is available on the relevant Payment products page.
Hash field order
selectedBankId is field 18 in the concatenation order. Add it between
isTest (field 17) and hashCheck (field 29) in the concatenated string. Only include it if you
are using it.
{
"siteCode": "YOUR_SITE_CODE",
"countryCode": "ZA",
"currencyCode": "ZAR",
"amount": "100.00",
"transactionReference": "ORDER-001",
"bankReference": "ABC123",
"cancelUrl": "https://yourstore.com/cancel",
"errorUrl": "https://yourstore.com/error",
"successUrl": "https://yourstore.com/success",
"notifyUrl": "https://yourstore.com/notify",
"isTest": false,
"selectedBankId": "YOUR_BANK_ID",
"hashCheck": "YOUR_GENERATED_HASH"
}
Customer Identity Verification
If your business operates in a high-risk industry, you are required to implement Customer Identity VerificationCustomer Identity Verification Checking that the payment instrument belongs to the natural person making the payment. Ozow requires it for merchants it has classified as high-risk, on Pay by Bank, Absa Pay, Capitec Pay, Nedbank Direct EFT, FNB Payment Requests and PayShap Request, and can disable those methods where it is not implemented correctly. before going live with Bank API payment methods.
To implement it in Payments API, pass the verified South African ID number or foreign passport
number in the customerIdentifier field of the payment request:
{
"siteCode": "YOUR_SITE_CODE",
"countryCode": "ZA",
"currencyCode": "ZAR",
"amount": "100.00",
"transactionReference": "ORDER-001",
"bankReference": "ABC123",
"cancelUrl": "https://yourstore.com/cancel",
"errorUrl": "https://yourstore.com/error",
"successUrl": "https://yourstore.com/success",
"notifyUrl": "https://yourstore.com/notify",
"isTest": false,
"customerIdentifier": "0000000000000",
"hashCheck": "YOUR_GENERATED_HASH"
}
Hash field order
customerIdentifier is field 27 in the concatenation order. Add it
between variableAmountMax (field 26) and customerCellphoneNumber (field 28) in the
concatenated string. If you are not using the fields between isTest and customerIdentifier,
skip them; only include fields that have a value.
For full details on Customer Identity Verification requirements see Customer Identity Verification.
Next steps
- Review the Building a secure integration checklist before going live
- Test your integration using Payin test cases
- Switch your base URL from staging to production when you are ready to go live
- See the Payments API reference for the full technical specification
In the API reference
4 entries
Last updated