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.
On this page14 sections
- Before you start
- Environments
- How redirect works
- Core integration
- Step 1: Obtain an access token
- Step 2: Create a payment request
- Step 3: Redirect the customer
- Step 4: Handle the webhook notification
- Step 5: Confirm the transaction outcome
- Step 6: Cancel a payment
- Optional features
- Standalone button
- Customer Identity Verification
- Next steps
Build with AI 2 packages
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.
- Take a paymentEverything needed to take a payment end to end with One API, from credentials through the hosted page to the webhook that confirms it, and the test cases that prove each outcome before you go live.
- 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 One API. Your system creates a payment request, redirects the customer to Ozow's secure hosted payment page, and receives a webhookWebhook 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. notification when the payment is complete.
This guide uses One API: Ozow's recommended API for all new integrations. It uses OAuth 2.0OAuth 2.0 The authorisation framework behind the token endpoint. Ozow uses the client credentials flow: your server exchanges a client ID and secret for a short-lived access token, and sends that token rather than the secret on every subsequent call.RFC 6749 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 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, you're
on the Payments API: see Redirect to Ozow under
Legacy integrations, or Migrating to One API.
Before you start
- You have completed Prerequisites and onboarding
- You have a Client ID and Client Secret from the One API Clients section of your Ozow Dashboard
- You have your 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 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:/ |
https:/ |
dash.ozow.com |
| Staging | https:/ |
https:/ |
stagingdash.ozow.com |
How redirect works
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.
POST https://one.ozow.com/v1/token
Content-Type: application/x-www-form-urlencoded
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"
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
$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;
?>
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();
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
{
"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:
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.
POST https://one.ozow.com/v1/payments
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
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"
}'
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
$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;
?>
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();
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 |
|---|---|---|---|
site |
string | Yes | Your Ozow site code |
amount. |
string | Yes | Must be ZAR |
amount. |
number | Yes | Payment amount |
merchant |
string | Yes | Your internal order reference |
beneficiary |
string | Usually | The reference that appears on your bank statement for the payment. Letters and numbers only |
expire |
string | Yes | Payment request expiry in RFC 3339RFC 3339 A profile of ISO 8601 for timestamps on the internet, and what most APIs mean when they say a field is an ISO date.IETF format |
return |
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.
Successful response
{
"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 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.
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 to deliver webhooks.
There are two ways to be told, and you choose them independently.
| Webhooks | notify |
|
|---|---|---|
| How you turn it on | The Ozow Dashboard or the webhook endpoints | Set notify 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 notify |
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 idempotentIdempotency 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, 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 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, select your client, and manage webhooks from there
- Via the API: List Webhook Subscriptions to see what you already have, then Create Webhook Subscription 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. |
A transaction has completed: check the status field to determine if it was successful or resulted in an error |
refund. |
A refund has completed: check the status field for the refund result |
subscription. |
A customer approved a subscription consent ⚠️ Beta, subject to change |
subscription. |
A consent was not approved ⚠️ Beta, subject to change |
subscription. |
An individual collection succeeded ⚠️ Beta, subject to change |
subscription. |
An individual collection failed ⚠️ Beta, subject to change |
subscription. |
A subscription took all its scheduled occurrences ⚠️ Beta, subject to change |
subscription. |
A subscription was cancelled ⚠️ Beta, subject to change. One l, unlike Cancelled elsewhere |
subscription. |
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. data is
WebhookEventData when the subscription asked for thin,
TransactionCompleteFullData for a full
subscription to transaction.complete, and
RefundCompleteFullData for a full subscription
to refund.complete. Every value in a full payload is a string, the amount and the flags
included.
{
"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: the webhook maps them down to four.
status |
Sent when the transaction is |
|---|---|
Successful |
Complete |
Incomplete |
Created |
Pending |
Pending or Pending |
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 |
Base64Base64 A way of writing binary data using ordinary text characters, so it can travel inside JSON or a URL. It is an encoding, not encryption: anyone can decode it.Wikipedia encoded signature |
Verify a webhook signature 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.
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.
You can also check transaction status directly via the API at any time:
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.
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 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 PayShapPayShap South Africa's rapid payments service, run by the banks. Low-value payments clear in seconds, and the recipient can be identified by a ShapID instead of by an account number.payshap.co.za.
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.
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 page.
{
"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 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 One API, pass the payer.identity object in the payment request:
{
"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. |
string | said for South African ID, passport for foreign passport |
payer. |
string | ISO 3166 Alpha-2ISO 3166-1 alpha-2 The two-letter country codes published by the International Organization for Standardization, such as ZA for South Africa and GB for the United Kingdom. Always uppercase.Wikipedia country code, ZA for South Africa |
payer. |
string | The verified ID or passport number |
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 One API reference for the full technical specification
In the API reference
11 entries
- POST
/tokenGenerate Authentication Token One API - POST
/paymentsRequest Payment One API - GET
/payments/{id}/transactionsList Transactions for Payment One API - POST
/payments/{id}/cancelCancel Payment Request One API - GET
/webhooksList Webhook Subscriptions One API - POST
/webhooksCreate Webhook Subscription One API - GET
/webhooks/{id}/secretGet Webhook Secret One API - POST Ozow sends your notification URLTransaction completed One API
- SchemaWebhookEnvelope One API
- SchemaWebhookEventData One API
- SchemaTransactionCompleteFullData One API
Last updated