# 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.

Source: https://hub.ozow.com/integration-methods/apis/deprecated-integrations/redirect-to-ozow/

This guide walks you through a redirect payin 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](https://hub.ozow.com/integration-methods/apis/payin/redirect-to-ozow.md) instead. This guide exists to support merchants
> already integrated on Payments API.

## Before you start

- You have completed [Prerequisites and onboarding](https://hub.ozow.com/getting-started/prerequisites-and-onboarding.md)
- You have your API key, private key, and site code from your [Ozow Dashboard](https://dash.ozow.com)
- Your notification URL, success URL, cancel URL, and error URL are set up and publicly accessible
  via HTTPS

## Environments

| Environment | API endpoint | Dashboard |
|---|---|---|
| Production | `https://api.ozow.com/postpaymentrequest` | [dash.ozow.com](https://dash.ozow.com) |
| Staging | `https://stagingapi.ozow.com/postpaymentrequest` | [stagingdash.ozow.com](https://stagingdash.ozow.com) |

## How redirect works

```mermaid
sequenceDiagram
    participant C as Customer
    participant M as Your system
    participant O as Payments API
    participant P as Ozow payment page

    C->>M: Reaches checkout
    M->>O: POST /postpaymentrequest
    O-->>M: Returns payment URL
    M->>C: Redirects customer to payment URL
    C->>P: Completes payment
    O-->>M: Sends notification response to NotifyUrl
    M->>M: Verifies notification hash
    M-->>C: Redirects customer to SuccessUrl, CancelUrl, or ErrorUrl
```

---

## Core integration

### Step 1: Generate the hash check

The Payments API uses SHA512 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.

1. Concatenate the post variables (excluding `HashCheck` and `Token`) in the order they appear in
   the request fields table below
2. Append your private key to the concatenated string
3. Convert the entire string to lowercase
4. 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 | ZAR |
| Amount | 25.00 |
| TransactionReference | 123 |
| BankReference | ABC123 |
| CancelUrl | `http://demo.ozow.com/cancel.aspx` |
| ErrorUrl | `http://demo.ozow.com/error.aspx` |
| SuccessUrl | `http://demo.ozow.com/success.aspx` |
| NotifyUrl | `http://demo.ozow.com/notify.aspx` |
| IsTest | false |

The concatenated string before hashing would be:

```text
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:

```text
4a2e7db32f76747b0f434edeb62e8b3ebb04125025feae622bc09092296bc965cec49a8cbeeef0e21f3c4c04249de69dd0b330a5b7da21c51a95d360d03f54ba
```

**Code examples**

**C#**

```csharp
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**

```php
<?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();
?>
```

**JavaScript**

```javascript
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();
```

**Python**

```python
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 | `siteCode` | Yes |
| 2 | `countryCode` | Yes |
| 3 | `currencyCode` | Yes |
| 4 | `amount` | Yes |
| 5 | `transactionReference` | Yes |
| 6 | `bankReference` | Yes |
| 7 | `optional1` | No |
| 8 | `optional2` | No |
| 9 | `optional3` | No |
| 10 | `optional4` | No |
| 11 | `optional5` | No |
| 12 | `customer` | No |
| 13 | `cancelUrl` | No |
| 14 | `errorUrl` | No |
| 15 | `successUrl` | No |
| 16 | `notifyUrl` | No |
| 17 | `isTest` | Yes |
| 18 | `selectedBankId` | No |
| 19 | `bankAccountNumber` | No |
| 20 | `branchCode` | No |
| 21 | `bankAccountName` | No |
| 22 | `payeeDisplayName` | No |
| 23 | `expiryDateUtc` | No |
| 24 | `allowVariableAmount` | No |
| 25 | `variableAmountMin` | No |
| 26 | `variableAmountMax` | No |
| 27 | `customerIdentifier` | No |
| 28 | `customerCellphoneNumber` | No |
| 29 | `hashCheck` | 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.

```endpoint
POST https://api.ozow.com/postpaymentrequest
ApiKey: YOUR_API_KEY
Content-Type: application/json
Accept: application/json
```

**cURL**

```bash
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"
  }'
```

**C#**

```csharp
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**

```php
<?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;
?>
```

**JavaScript**

```javascript
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));
```

**Python**

```python
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 |
|---|---|---|---|
| `siteCode` | string | Yes | Your Ozow site code |
| `countryCode` | string | Yes | Must be `ZA` |
| `currencyCode` | string | Yes | Must be `ZAR` |
| `amount` | string | Yes | Payment amount |
| `transactionReference` | string | Yes | Your internal order reference |
| `bankReference` | string | Yes | The reference that appears on your bank statement for the payment |
| `cancelUrl` | string | Yes | URL to redirect the customer to if they cancel |
| `errorUrl` | string | Yes | URL to redirect the customer to if an error occurs |
| `successUrl` | string | Yes | URL to redirect the customer to on successful payment |
| `notifyUrl` | string | Yes | URL Ozow posts the notification response to |
| `isTest` | boolean | Yes | Set to `false` for live payments |
| `hashCheck` | string | Yes | The SHA512 hash generated in Step 1 |

For the full list of request fields see [Post Payment Request](https://hub.ozow.com/api-reference/payments-api/post-post-payment-request.md).

**Successful response**

```json
{
  "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**

```json
{
  "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 Bank is available by default. Additional payment methods such as Capitec Pay,
> Buy Now Pay Later, and PayShap Request 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**

```text
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:

1. Concatenate the fields below in this order, skipping any that are empty
2. Append your private key to the concatenated string
3. Convert the entire string to lowercase
4. Generate a SHA512 hash of the lowercase string
5. Compare your generated hash to the `Hash` value in the notification

**Notification hash field order**

| Position | Field |
|---|---|
| 1 | `SiteCode` |
| 2 | `TransactionId` |
| 3 | `TransactionReference` |
| 4 | `Amount`, with two decimal places |
| 5 | `Status` |
| 6 | `Optional1` |
| 7 | `Optional2` |
| 8 | `Optional3` |
| 9 | `Optional4` |
| 10 | `Optional5` |
| 11 | `CurrencyCode` |
| 12 | `IsTest` |
| 13 | `StatusMessage` |
| 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.

```mermaid
flowchart LR
    A[Receive notification] --> B{Verify hash}
    B -->|Invalid| C[Log and alert - do not process]
    B -->|Valid| D{Check Status field}
    D -->|Complete| E[Credit order and fulfil]
    D -->|Cancelled| F[Return customer to checkout]
    D -->|Error| G[Do not credit - check SubStatus]
```

**Transaction statuses**

| Status | Description |
|---|---|
| `Complete` | Payment completed successfully |
| `Cancelled` | Customer cancelled the payment |
| `Error` | An error occurred: check `SubStatus` for detail |
| `PendingInvestigation` | 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](https://hub.ozow.com/integration-methods/statuses.md) for the full SubStatus list.

You can also confirm transaction status directly via the API at any time:

**By transaction reference**

```endpoint
GET https://api.ozow.com/GetTransactionByReference?siteCode={siteCode}&transactionReference={transactionReference}
ApiKey: YOUR_API_KEY
```

**By transaction ID**

```endpoint
GET https://api.ozow.com/GetTransaction?siteCode={siteCode}&transactionId={transactionId}
ApiKey: YOUR_API_KEY
```

> ℹ️ **Note**: Handle duplicate notifications idempotently. 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](https://hub.ozow.com/integration-methods/apis/payin/embedded-wallet.md).

**Implementation**

Include the `SelectedBankId` field in your payment request. The `SelectedBankId` for each payment
method is available on the relevant [Payment products](https://hub.ozow.com/payment-products.md) 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.

```json
{
  "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
Verification 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:

```json
{
  "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](https://hub.ozow.com/integration-methods/apis/payin/identity-verification.md).

---

## Next steps

- Review the [Building a secure
  integration](https://hub.ozow.com/getting-started/building-a-secure-integration.md) checklist before going
  live
- Test your integration using [Payin test cases](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/payin-test-cases-payments-api.md)
- Switch your base URL from staging to production when you are ready to go live
- See the [Payments API reference](https://hub.ozow.com/api-reference/payments-api.md) for the full technical specification