Ozow Hub
On this page11 sections
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.

  • 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.
    View package
  • Migrate refunds from the Payments API to One APIEverything needed to move an existing refunds integration onto One API, with the legacy guide and its One API counterpart side by side.
    View package

This guide maps 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. money in and refunds integration from the Payments API to One API. It covers what changes, what does not, and the order to make the changes in.

One API is the current integration path. The Payments API continues to process live traffic and is not being switched off on a fixed date, but it does not receive new features; new features are added to One API first.

Before you start

  • You have a working Payments API integration using Redirect to Ozow with Payments API, Refund a payment with Payments API, or both
  • You have access to One API clients in the Ozow Portal, and you have created a Client ID and Client Secret
  • 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., in the format XXX-XXX-XXX. See Find your site code if you're not sure where to find it
  • You can run a staging integration against One API before touching production

What does not change

  • Your site code stays the same
  • The payment methods available to your customers are the same products on both APIs
  • Your floatFloat The balance held with Ozow that payouts and refunds are paid out of. Both draw on it, and neither will process while it is empty. Payins do not need one, so if you only take payments you never meet it., settlementSettlement Ozow paying the money you have collected into your bank account. Payins arrive at Ozow first and are settled to you on a schedule, so what a customer paid you today and what has been settled to you today are different amounts. schedule, and Ozow Portal reporting are shared across both APIs
  • 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. is the only supported currency on either API

What changes

Concept Payments API One API
Authentication API key plus 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 signed with your private key, per request 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 Client Credentials, a bearer tokenBearer token An access token sent in the Authorization header as Authorization: Bearer <token>. Anyone holding the token can use it, which is why it belongs on your server and never in a browser or a mobile app.RFC 6750 from /v1/token
Request shape Flat form fields, mostly PascalCase in notifications, camelCase in requests Nested JSON resources, camelCase throughout
Creating a payment POST /postpaymentrequest returns a url directly POST /v1/payments returns a Payment resource with a redirectUrl and a set of links
Payment outcome The payment request and its outcome are one resource, reported by notification A Payment is the checkout session; each attempt against it is a separate Transaction, retrieved via GET /payments/{id}/transactions
Cancelling before completion Not possible via API, only the customer can abandon the payment page POST /payments/{id}/cancel
Outcome delivery A form-encoded POST to notifyUrl, authenticated with a hash you verify yourself A signed 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. delivered via Svix, authenticated with svix-id, svix-timestamp, and svix-signature
Refunds Submitted as a batch array to /secure/refunds/submit, one hash check per item POST /transactions/{id}/refunds for a single transaction, or POST /refunds for a batch, both 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 via an Idempotency-Key header
Cancelling a refund Not possible via API POST /refunds/{id}/cancel
Standalone payment method button selectedBankId field in the payment request institutionId field in the payment request
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. Flat customerIdentifier field Nested payer.identity object with type, country, and identifier
Retrying a failed request safely Not supported, a resubmitted request is a new payment request Idempotency-Key header on POST and PUT requests: replaying the same key returns the original result instead of creating a duplicate

Step 1: Replace hash-based authentication with OAuth

The Payments API signs every request with a SHA512 hash built from your private key. One API instead issues a short-lived bearer token from your Client ID and Client Secret.

Remove your hash-generation code entirely, there is no request hash in One API. Replace it with a token request:

POST Generate Authentication Token Reference
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"

A token carries the scopes you asked for and no others, and each scope is refused on the endpoints outside it. Request the scope the calling code needs rather than one token for the whole integration.

What the code is doing Scope Where
Creating a payment, cancelling it, reading its transactions payments Steps 2, 3 and 5
Managing the webhook endpoint and reading its signing secret webhooks Step 4
Issuing and cancelling refunds refunds Step 6

A payments token is refused with 403 Forbidden on the webhook and refund endpoints, so the three parts of your integration hold three tokens. Cache them separately. Asking for a scope your client has not been granted is refused at the token request itself.

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. Cache the token and request a new one before it expires, rather than requesting one per payment. See Step 2: Obtain an access token for the full flow and code samples in C#, PHP, JavaScript, and Python.

Important

Your private key has no equivalent in One API and is not used for anything. Do not send it, and retire it from your secrets store once the migration is complete and the Payments API integration is decommissioned.


Step 2: Move from flat fields to nested resources

The Payments API request is a single flat object. One API groups related fields, most visibly amount becomes an object with currency and value.

Payments API

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

One API

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

Field mapping

Payments API One API Notes
siteCode siteCode Unchanged
countryCode region Same 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 code, renamed and moved to the top level. Optional on One API, and defaults to ZA when you omit it
currencyCode amount.currency Moves inside amount, and must be ZAR
amount (string) amount.value (number) One API takes a numeric value, not a pre-formatted string
transactionReference merchantReference Same purpose, renamed. Still the reference the payer sees on their own statement, and still yours to keep unique
bankReference beneficiaryReference Same purpose, renamed: the reference that appears on your bank statement, for recon. One API is stricter, letters and numbers only where the Payments API also allowed spaces, dashes and punctuation
cancelUrl, errorUrl, successUrl returnUrl One API redirects to a single returnUrl regardless of outcome; determine the outcome from a verified webhook or the transactions endpoint, not from which URL fired
notifyUrl notifyUrl Still accepted, still optional, and still per payment request. Move to webhooks instead: they are configured once and signed, and Step 4 is that change
expiryDateUtc expireAt Now required on every payment request, where the Payments API let you omit it. ISO 8601ISO 8601 The international standard for writing dates and times, such as 2026-03-14T09:30:00Z. Unambiguous about ordering and time zone, which local formats are not.Wikipedia (2026-12-31T23:59:59Z) rather than yyyy-MM-dd HH:mm
isTest Not a request field Test and live are separated by environment (stagingone.ozow.com vs one.ozow.com), not by a flag on the request
hashCheck Not a request field Removed entirely, see Step 1

For the full request and response schema see Create a payment.


Step 3: Update how you read the response and redirect the customer

Payments API response

{
  "paymentRequestId": "00000000-0000-0000-0000-000000000000",
  "url": "https://pay.ozow.com/00000000-0000-0000-0000-000000000000/Secure",
  "errorMessage": null
}

One API 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"
}

Redirect the customer to redirectUrl instead of url. Store id instead of paymentRequestId, you use it to check transactions or cancel the payment later.

Important

Persist that id against your order, before you redirect. On One API the id is the handle to the payment: Get a payment, List its transactions and Cancel it all key off it, where the Payments API keyed off the reference you chose. Store it with the order rather than for the life of the request.

Important

The Payments API returns a rejected request as HTTP 200 with a null url and a reason in errorMessage. One API uses HTTP status codes for this instead, a validation failure comes back as 400 Bad Request with an Error body. Update your error handling to check the status code rather than inspecting the response body for a null URL.

A Payment created but never completed by the customer settles into one of two PaymentStatus values, Created or Expired. This is a separate concept from the transaction outcome, see Step 5.


Step 4: Move from a per-request notifyUrl to a webhook subscription

The Payments API takes a notifyUrl on every payment request and posts a form-encoded notification to it. One API configures a webhook endpoint once, and delivers signed events to it for every subsequent payment.

Set up your webhook endpoint in one of two ways:

  • Via the Ozow Portal
  • Via the API

See Step 5: Handle the webhook notification for detailed setup steps.

Subscribe to transaction.complete to receive the same outcome your Payments API notifyUrl delivers. Choose the full message type if you need transaction details in the payload, or thin if you will call the API separately for details.

Replace your hash verification with signature verification. Every webhook carries svix-id, svix-timestamp and svix-signature headers, and Verify a webhook signature has the five steps and a working verifier in four languages. Retrieve your webhook secret with Get Webhook Secret.

Important

Never process a webhook notification without first verifying its signature, in exactly the way you never processed a Payments API notification without first verifying its hash. Log and alert on verification failures.


Step 5: Re-map transaction statuses

The Payments API reports one flat status on the notification. One API separates the Payment (the checkout session) from each Transaction attempted against it, and the transaction carries the outcome you act on.

Payments API notification statuses

Status Description
Complete Payment completed successfully
Cancelled Customer cancelled the payment
Error An error occurred: check SubStatus for detail
Abandoned Customer left the payment page without completing
Pending The outcome is not known yet and is reposted to your notifyUrl once it is. If you do not use a notifyUrl you receive PendingInvestigation instead
PendingInvestigation Payment is under review: do not credit until resolved

One API transaction statuses

Status Description
Successful The transaction completed, credit the order
Incomplete The customer did not complete the payment attempt
Error The transaction failed. reason carries the detail
Pending The transaction is still in progress
Refunded The transaction completed and has since been refunded

These are not the same enumeration and the values do not line up one to one. Refunded has no Payments API equivalent, because a refund was reported as a separate resource entirely.

Audit every status comparison before you cut over. Error and Pending keep their spelling across both APIs, but Complete becomes Successful. An equality check against Complete therefore returns false for every One API transaction without raising an error, leaving paid orders unfulfilled. Treat Successful as the only status that means the order is paid.

Important

Never update an order status without first verifying the webhook signature, the same rule as the Payments API notification hash.


Step 6: Rebuild refunds on the new endpoints

If you issue refunds, the request and authentication both change alongside the 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. flow.

  • Request a token with the refunds scope, the same call as Step 1 with a different scope. This replaces the Payments API's separate /token bearer flow for refunds, and it is a different token from the one your payin code holds
  • Replace the batch array to /secure/refunds/submit with either POST /transactions/{id}/refunds for a single refund or POST /refunds for a batch, see Refunds: One API
  • Add an Idempotency-Key header to every refund request, retrying a failed submission with the same key returns the original result instead of issuing a second refund
  • Drop your per-item hash check, refund requests are authenticated by the bearer token alone
  • refundReason becomes reason, and is still required
  • isRtc becomes realTimePayment
  • Refund statuses keep their spelling but shrink from nine values to six: Pending, Submitted, Complete, Failed, Cancelled and Returned. PendingInvestigation, Invalid and Error do not exist on One API, so any branch you have for those three needs somewhere else to go

Migrating without downtime

One API and Payments API use the same underlying data, so any payment created through Payments API is also available through One API. You don't need to move or re-create existing payments when you switch, and you can look up and refund payments created on Payments API using One API.

  1. Build and test the full One API flow. Before you change anything in production, test every part of your integration on One API: getting an access token, creating payments, receiving webhooks, and processing refunds if you use them.
  2. Deploy One API alongside Payments API. Release your One API integration behind a feature flag or on a new code path. Don't remove your Payments API code yet.
  3. Send new payments to One API. Switch new payments over to One API. Any payments already in progress on Payments API will finish on the old flow.
  4. Keep your Payments API webhook handler running. A payment can still complete or time out for some time after it was created. Keep the handler live until every payment created before the switch has reached a final status.
  5. Remove Payments API. Once no Payments API payments are still in progress, remove the old endpoint calls and the hash-generation code, and retire your private key. You'll still be able to look up and refund older payments through One API.

Test before you switch all production traffic.

If you need staging credentials and endpoints, request them from support@ozow.com.


Next steps

In the API reference

6 entries

Last updated