# Redirect to Ozow

> Build a redirect payin with One API. Create a payment request, send the customer to Ozow's hosted page, and confirm the result from the webhook.

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

This guide walks you through a redirect payin integration using One API. Your system creates a
payment request, redirects the customer to Ozow's secure hosted payment page, and receives a webhook
notification when the payment is complete.

> ℹ️ This guide uses **One API**: Ozow's recommended API for all new integrations. It uses OAuth 2.0
> for authentication, and new payment methods and features are released here first.
>
> Already integrated? If your integration posts to `api.ozow.com` and builds a SHA512 hash, you're
> on the Payments API: see [Redirect to Ozow](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/redirect-to-ozow.md) under
> Legacy integrations, or [Migrating to One API](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/migrating-to-one-api.md).

## Before you start

- You have completed [Prerequisites and onboarding](https://hub.ozow.com/getting-started/prerequisites-and-onboarding.md)
- You have a Client ID and Client Secret from the One API Clients section of your [Ozow Dashboard](https://dash.ozow.com/MerchantAdmin/OneAPI/Clients)
- You have your site code from the Site section of your Dashboard
- Your webhook endpoint is set up and publicly accessible via HTTPS

> ℹ️ **Note**: Only users with administrator privileges in the Ozow Dashboard can access the One API
> Clients section.

## Environments

| Environment | Token endpoint | API base URL | Dashboard |
|---|---|---|---|
| Production | `https://one.ozow.com/v1/token` | `https://one.ozow.com/v1` | [dash.ozow.com](https://dash.ozow.com) |
| Staging | `https://stagingone.ozow.com/v1/token` | `https://stagingone.ozow.com/v1` | [stagingdash.ozow.com](https://stagingdash.ozow.com) |

## How redirect works

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

    C->>M: Reaches checkout
    M->>O: POST /v1/payments
    O-->>M: Returns redirectUrl
    M->>C: Redirects customer to redirectUrl
    C->>P: Completes payment
    O-->>M: Sends webhook notification
    M->>M: Verifies webhook signature
    M-->>C: Updates order and redirects to returnUrl
```

---

## Core integration

### Step 1: Obtain an access token

One API uses OAuth 2.0 Client Credentials authentication. You need an access token before making any
API calls.

```endpoint
POST https://one.ozow.com/v1/token
Content-Type: application/x-www-form-urlencoded
```

**cURL**

```bash
curl -X POST "https://one.ozow.com/v1/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "scope=payments" \
  -d "grant_type=client_credentials"
```

**C#**

```csharp
var client = new HttpClient();
var content = new FormUrlEncodedContent(
    new[]
    {
        new KeyValuePair<string, string>("client_id", "YOUR_CLIENT_ID"),
        new KeyValuePair<string, string>("client_secret", "YOUR_CLIENT_SECRET"),
        new KeyValuePair<string, string>("scope", "payments"),
        new KeyValuePair<string, string>("grant_type", "client_credentials"),
    }
);
var response = await client.PostAsync("https://one.ozow.com/v1/token", content);
var result = await response.Content.ReadAsStringAsync();
```

**PHP**

```php
<?php
$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_URL => "https://one.ozow.com/v1/token",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        "client_id" => "YOUR_CLIENT_ID",
        "client_secret" => "YOUR_CLIENT_SECRET",
        "scope" => "payments",
        "grant_type" => "client_credentials",
    ]),
    CURLOPT_HTTPHEADER => ["Content-Type: application/x-www-form-urlencoded"],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
?>
```

**JavaScript**

```javascript
const response = await fetch("https://one.ozow.com/v1/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    client_id: "YOUR_CLIENT_ID",
    client_secret: "YOUR_CLIENT_SECRET",
    scope: "payments",
    grant_type: "client_credentials",
  }),
});
const data = await response.json();
```

**Python**

```python
import requests

response = requests.post(
    "https://one.ozow.com/v1/token",
    data={
        "client_id": "YOUR_CLIENT_ID",
        "client_secret": "YOUR_CLIENT_SECRET",
        "scope": "payments",
        "grant_type": "client_credentials",
    },
)
data = response.json()
```

**Successful response**

```json
{
  "access_token": "mF_9.B5f-4.1JqM",
  "token_type": "Bearer",
  "expires_in": "14400",
  "scope": "payments"
}
```

Store the `access_token` and include it in the `Authorization` header of all subsequent requests:

```http
Authorization: Bearer YOUR_ACCESS_TOKEN
```

Tokens expire after the number of seconds in `expires_in`, which is 14400, four hours. Read
that field rather than hard coding the number: a change to the lifetime reaches you in the
response before it reaches this page. If authentication fails, the API returns
`401 Unauthorized`.

---

### Step 2: Create a payment request

Create a payment request for your customer's order.

```endpoint
POST https://one.ozow.com/v1/payments
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
```

**cURL**

```bash
curl -X POST "https://one.ozow.com/v1/payments" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "siteCode": "YOUR_SITE_CODE",
    "amount": {
      "currency": "ZAR",
      "value": 100.00
    },
    "merchantReference": "ORDER-001",
    "beneficiaryReference": "ONLINESHOP002",
    "expireAt": "2026-12-31T23:59:59Z",
    "returnUrl": "https://yourstore.com/order-complete"
  }'
```

**C#**

```csharp
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer",
    "YOUR_ACCESS_TOKEN"
);

var payload = new
{
    siteCode = "YOUR_SITE_CODE",
    amount = new { currency = "ZAR", value = 100.00 },
    merchantReference = "ORDER-001",
    beneficiaryReference = "ONLINESHOP002",
    expireAt = "2026-12-31T23:59:59Z",
    returnUrl = "https://yourstore.com/order-complete",
};

var response = await client.PostAsync(
    "https://one.ozow.com/v1/payments",
    new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
);
var result = await response.Content.ReadAsStringAsync();
```

**PHP**

```php
<?php
$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_URL => "https://one.ozow.com/v1/payments",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode([
        "siteCode" => "YOUR_SITE_CODE",
        "amount" => ["currency" => "ZAR", "value" => 100.00],
        "merchantReference" => "ORDER-001",
        "beneficiaryReference" => "ONLINESHOP002",
        "expireAt" => "2026-12-31T23:59:59Z",
        "returnUrl" => "https://yourstore.com/order-complete",
    ]),
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer YOUR_ACCESS_TOKEN",
        "Content-Type: application/json",
    ],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
?>
```

**JavaScript**

```javascript
const response = await fetch("https://one.ozow.com/v1/payments", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_ACCESS_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    siteCode: "YOUR_SITE_CODE",
    amount: { currency: "ZAR", value: 100.00 },
    merchantReference: "ORDER-001",
    beneficiaryReference: "ONLINESHOP002",
    expireAt: "2026-12-31T23:59:59Z",
    returnUrl: "https://yourstore.com/order-complete",
  }),
});
const data = await response.json();
```

**Python**

```python
import requests

response = requests.post(
    "https://one.ozow.com/v1/payments",
    headers={
        "Authorization": "Bearer YOUR_ACCESS_TOKEN",
        "Content-Type": "application/json",
    },
    json={
        "siteCode": "YOUR_SITE_CODE",
        "amount": {"currency": "ZAR", "value": 100.00},
        "merchantReference": "ORDER-001",
        "beneficiaryReference": "ONLINESHOP002",
        "expireAt": "2026-12-31T23:59:59Z",
        "returnUrl": "https://yourstore.com/order-complete",
    },
)
data = response.json()
```

**Key request fields**

| Field | Type | Required | Description |
|---|---|---|---|
| `siteCode` | string | Yes | Your Ozow site code |
| `amount.currency` | string | Yes | Must be `ZAR` |
| `amount.value` | number | Yes | Payment amount |
| `merchantReference` | string | Yes | Your internal order reference |
| `beneficiaryReference` | string | Usually | The reference that appears on your bank statement for the payment. Letters and numbers only |
| `expireAt` | string | Yes | Payment request expiry in RFC 3339 format |
| `returnUrl` | string | Yes | URL to redirect the customer to after payment |

> ⚠️ **Important**: Send `beneficiaryReference`. The contract marks it optional because a site can be
> configured either way, and most are configured to require it. Leaving it out of a site that wants
> it answers `400` with `Error occurred creating merchant request (Parameter 'Bank reference
> missing')`, which does not name the field it means.

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

**Successful response**

```json
{
  "links": {
    "self": "https://one.ozow.com/v1/payments/497f6eca-6276-4993-bfeb-53cbbbba6f08",
    "cancel": "https://one.ozow.com/v1/payments/497f6eca-6276-4993-bfeb-53cbbbba6f08/cancel",
    "transactions": "https://one.ozow.com/v1/payments/497f6eca-6276-4993-bfeb-53cbbbba6f08/transactions"
  },
  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  "status": "Created",
  "redirectUrl": "https://pay.ozow.com/497f6eca-6276-4993-bfeb-53cbbbba6f08/secure"
}
```

---

### Step 3: Redirect the customer

Redirect your customer's browser to the `redirectUrl` from the response. Ozow displays the payment
page where the customer selects their preferred payment method and completes the payment.

**Send them to the URL you were given, and do not build one.** Its shape is Ozow's to change, and a
URL assembled from the payment id is a URL that stops working without notice.

`status` on the response is `Created`. Compare it without case.

Once the customer completes or cancels the payment, Ozow redirects them back to your `returnUrl`.

> ℹ️ **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.

> ⚠️ **Important**: Do not use the customer's return to your `returnUrl` as confirmation that a
> payment was successful. Always confirm payment status via a verified webhook notification or an
> API status check.

---

### Step 4: Handle the webhook notification

Ozow sends a webhook notification to your endpoint when a transaction completes. One API uses
[Svix](https://www.svix.com/) to deliver webhooks.

**There are two ways to be told, and you choose them independently.**

| | Webhooks | `notifyUrl` |
|---|---|---|
| How you turn it on | The Ozow Dashboard or the webhook endpoints | Set `notifyUrl` on the payment request |
| What arrives | The events below, signed with Svix headers | The same notification the Payments API sends on a payin, with the same payload and the same hash |
| When it fires | Every transaction | Every payment where you set `notifyUrl` |

Webhooks are the recommended path and the rest of this step covers them. `notifyUrl` is optional
and exists so that an integration already handling the Payments API notification keeps working:
point it at your existing handler and the payload and hash are the ones it already verifies.

> ⚠️ **Important**: If you configure both, **both fire for the same payment**. Your handlers must
> be idempotent, keyed on the merchant reference or the transaction ID, or one payment marks an
> order paid twice.

If you set neither, nothing is delivered and you must poll
[Get Transactions](https://hub.ozow.com/api-reference/one-api/get-payments-id-transactions.md) instead, which is slower and
which Step 5 covers.

**Setting up your webhook endpoint**

Set up your webhook endpoint in one of two ways:

- **Via the Ozow Dashboard**: navigate to [One API
  Clients](https://dash.ozow.com/MerchantAdmin/OneAPI/Clients), select your client, and manage
  webhooks from there
- **Via the API**: [List Webhook Subscriptions](https://hub.ozow.com/api-reference/one-api/get-webhooks.md) to see what you
  already have, then [Create Webhook Subscription](https://hub.ozow.com/api-reference/one-api/post-webhooks.md) for what you do
  not

> ⚠️ **Important**: List before you create. Creating a subscription that duplicates one you already
> have delivers every event twice, and the second delivery is indistinguishable from the first, so
> a handler that is not idempotent processes the same payment again. A deploy script that creates a
> subscription on every run is the usual way this happens.

**Available webhook events**

| Event | Description |
|---|---|
| [`transaction.complete`](https://hub.ozow.com/api-reference/one-api/webhooks/transaction-complete.md) | A transaction has completed: check the `status` field to determine if it was successful or resulted in an error |
| [`refund.complete`](https://hub.ozow.com/api-reference/one-api/webhooks/refund-complete.md) | A refund has completed: check the `status` field for the refund result |
| [`subscription.authorization.success`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-authorization-success.md) | A customer approved a subscription consent ⚠️ Beta, subject to change |
| [`subscription.authorization.failed`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-authorization-failed.md) | A consent was not approved ⚠️ Beta, subject to change |
| [`subscription.transaction.success`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-transaction-success.md) | An individual collection succeeded ⚠️ Beta, subject to change |
| [`subscription.transaction.failed`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-transaction-failed.md) | An individual collection failed ⚠️ Beta, subject to change |
| [`subscription.completed`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-completed.md) | A subscription took all its scheduled occurrences ⚠️ Beta, subject to change |
| [`subscription.canceled`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-canceled.md) | A subscription was cancelled ⚠️ Beta, subject to change. One `l`, unlike `Cancelled` elsewhere |
| [`subscription.expired`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-expired.md) | An authorisation lapsed before the subscription became active ⚠️ Beta, subject to change |

Subscribe to the name exactly as written. An event name the service does not
know is rejected, so a subscription to something close is a subscription that
never fires.

**Message types**

When setting up your webhook subscription you can choose how much data is included in each notification:

| Message type | What it includes |
|---|---|
| `thin` | `id`, `status` and `reason` |
| `full` | The transaction's fields, in the same shape the Payments API notification uses |

Choose `full` if you need transaction details in the webhook payload. Choose `thin` if you only need
the status and will query the API for details separately.

> ⚠️ **Important**: `full` is implemented for `transaction.complete` and `refund.complete` only. A
> subscription event registered as `full` delivers nothing.

**What arrives**

Every delivery has the same [envelope](https://hub.ozow.com/api-reference/one-api/schemas/webhook-envelope.md). `data` is
[`WebhookEventData`](https://hub.ozow.com/api-reference/one-api/schemas/webhook-event-data.md) when the subscription asked for `thin`,
[`TransactionCompleteFullData`](https://hub.ozow.com/api-reference/one-api/schemas/transaction-complete-full-data.md) for a `full`
subscription to `transaction.complete`, and
[`RefundCompleteFullData`](https://hub.ozow.com/api-reference/one-api/schemas/refund-complete-full-data.md) for a `full` subscription
to `refund.complete`. Every value in a `full` payload is a string, the amount and the flags
included.

```json
{
  "type": "transaction.complete",
  "timestamp": "2026-03-14T09:30:00Z",
  "data": {
    "id": "00000000-0000-0000-0000-000000000000",
    "status": "Successful",
    "reason": null
  }
}
```

`id` is the transaction, refund or subscription the event is about. `reason` carries the status
message and is null when there is nothing to say.

**The `status` values for `transaction.complete`**

These are not the transaction statuses on the
[statuses page](https://hub.ozow.com/integration-methods/statuses.md): the webhook maps them down to four.

| `status` | Sent when the transaction is |
|---|---|
| `Successful` | `Complete` |
| `Incomplete` | `Created` |
| `Pending` | `Pending` or `PendingInvestigation` |
| `Error` | anything else, including `Cancelled`, `Abandoned`, `Voided` and `Unknown` |

**A cancelled payment arrives as `Error`, not as `Cancelled`.** `reason` tells you which it was.
Switch on these four and read `reason` for the detail; do not expect the status names the
transaction itself carries.

For `refund.complete`, `status` is `Pending`, `Failed`, `Complete`, `Submitted`, `Cancelled`,
`Returned` or `Invalid`. Those are the `thin` values. A `full` subscription carries the refund's own
status in `Status`, unmapped, so `PendingInvestigation` and `Error` arrive as themselves rather than
as `Pending` and `Failed`. Handle both if you are on `full`.

**Verifying the webhook signature**

Every webhook notification includes Svix signature headers. You must verify the signature before
acting on any notification.

| Header | Description |
|---|---|
| `svix-id` | Unique message identifier, the same if the webhook is resent after a failure |
| `svix-timestamp` | Timestamp in seconds since epoch |
| `svix-signature` | Base64 encoded signature |

[Verify a webhook signature](https://hub.ozow.com/integration-methods/apis/payin/verify-a-webhook.md) has the five steps of the
check and a working verifier in `csharp`, `php`, `python` and `javascript`, with or
without the Svix library. To retrieve the secret for your webhook, use
[Get Webhook Secret](https://hub.ozow.com/api-reference/one-api/get-webhooks-id-secret.md).

> ⚠️ **Important**: Never process a webhook notification without first verifying its signature. Log
> and alert on verification failures: do not silently discard them.

---

### Step 5: Confirm the transaction outcome

After verifying the webhook, check the transaction status and update your order.

```mermaid
flowchart LR
    A[Receive webhook] --> B{Verify signature}
    B -->|Invalid| C[Log and alert - do not process]
    B -->|Valid| D{Check transaction status}
    D -->|Complete| E[Credit order and fulfil]
    D -->|Cancelled| F[Return customer to checkout]
    D -->|Error| G[Do not credit - investigate]
```

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

```endpoint
GET https://one.ozow.com/v1/payments/{id}/transactions
Authorization: Bearer YOUR_ACCESS_TOKEN
```

Replace `{id}` with the payment ID returned in Step 2.

> ℹ️ **Note**: Handle duplicate webhook 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

If your customer abandons checkout or you need to cancel an order before the customer completes
payment, you can cancel the payment request using the cancel link returned in the payment response.

```endpoint
POST https://one.ozow.com/v1/payments/{id}/cancel
Authorization: Bearer YOUR_ACCESS_TOKEN
```

A successfully cancelled payment will no longer be accessible to the customer via the `redirectUrl`.
If the customer attempts to use the link after cancellation they will see an error.

> ⚠️ **Important**: You can only cancel a payment that has not yet been completed. Do not attempt to
> cancel a payment with a `complete` status: use the refunds flow instead. See the [One API
> reference](https://hub.ozow.com/api-reference/one-api.md) for details.

---

## Optional features

### Standalone button

A standalone button lets you surface a specific Ozow payment method as a dedicated button on your
checkout page. Instead of showing a generic payment page where the customer selects their payment
method, a standalone button takes the customer directly to a specific payment method; for example
Capitec Pay, Buy Now Pay Later, or PayShap.

Adding a standalone button to your checkout removes an extra step for the customer, increases
awareness of specific payment methods, and can improve conversion rates.

> ⚠️ **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 `institutionId` field in your payment request. When a valid `institutionId` is included,
the customer is taken directly to that payment method. The `institutionId` for each payment method
is available on the relevant [Payment products](https://hub.ozow.com/payment-products.md) page.

```json
{
  "siteCode": "YOUR_SITE_CODE",
  "region": "ZA",
  "amount": {
    "currency": "ZAR",
    "value": 100.00
  },
  "merchantReference": "ORDER-001",
  "expireAt": "2026-12-31T23:59:59Z",
  "returnUrl": "https://yourstore.com/order-complete",
  "institutionId": "YOUR_INSTITUTION_ID"
}
```

---

### 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 One API, pass the `payer.identity` object in the payment request:

```json
{
  "siteCode": "YOUR_SITE_CODE",
  "amount": { "currency": "ZAR", "value": 100.00 },
  "merchantReference": "ORDER-001",
  "expireAt": "2026-12-31T23:59:59Z",
  "returnUrl": "https://yourstore.com/order-complete",
  "payer": {
    "id": "CUSTOMER-123",
    "name": "Firstname Lastname",
    "identity": {
      "type": "said",
      "country": "ZA",
      "identifier": "0000000000000"
    }
  }
}
```

**Identity fields**

| Field | Type | Description |
|---|---|---|
| `payer.identity.type` | string | `said` for South African ID, `passport` for foreign passport |
| `payer.identity.country` | string | ISO 3166 Alpha-2 country code, `ZA` for South Africa |
| `payer.identity.identifier` | string | The verified ID or passport number |

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/testing/payin-test-cases-one-api.md)
- Switch your base URL from staging to production when you are ready to go live
- See the [One API reference](https://hub.ozow.com/api-reference/one-api.md) for the full technical specification