Migrating to One API
Map a redirect payin and refunds integration from the Payments API to One API: what changes, what does not, and the order to make the changes in.
On this page11 sections
- Before you start
- What does not change
- What changes
- Step 1: Replace hash-based authentication with OAuth
- Step 2: Move from flat fields to nested resources
- Step 3: Update how you read the response and redirect the customer
- Step 4: Move from a per-request notifyUrl to a webhook subscription
- Step 5: Re-map transaction statuses
- Step 6: Rebuild refunds on the new endpoints
- Migrating without downtime
- 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.
- 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.
- 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.
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
- If you don't have access to your One API Clients, contact support@ozow.com or your account manager
- If you need help creating your Client ID and Client Secret, see Set up your 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.00is 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 / |
| Request shape | Flat form fields, mostly Pascal in notifications, camel in requests |
Nested JSON resources, camel throughout |
| Creating a payment | POST / returns a url directly |
POST / returns a Payment resource with a redirect 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 / |
| Cancelling before completion | Not possible via API, only the customer can abandon the payment page | POST / |
| Outcome delivery | A form-encoded POST to notify, 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 /, one hash check per item |
POST / for a single transaction, or POST / 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 / |
| Standalone payment method button | selected field in the payment request |
institution 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 customer field |
Nested payer. 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 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 |
|---|---|---|
site |
site |
Unchanged |
country |
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 |
currency |
amount. |
Moves inside amount, and must be ZAR |
amount (string) |
amount. (number) |
One API takes a numeric value, not a pre-formatted string |
transaction |
merchant |
Same purpose, renamed. Still the reference the payer sees on their own statement, and still yours to keep unique |
bank |
beneficiary |
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 |
cancel, error, success |
return |
One API redirects to a single return regardless of outcome; determine the outcome from a verified webhook or the transactions endpoint, not from which URL fired |
notify |
notify |
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 |
expiry |
expire |
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-14. Unambiguous about ordering and time zone, which local formats are not.Wikipedia (2026-12-31) rather than yyyy-MM-dd HH:mm |
is |
Not a request field | Test and live are separated by environment (stagingone. vs one.), not by a flag on the request |
hash |
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 Sub for detail |
Abandoned |
Customer left the payment page without completing |
Pending |
The outcome is not known yet and is reposted to your notify once it is. If you do not use a notify you receive Pending instead |
Pending |
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
refundsscope, the same call as Step 1 with a differentscope. This replaces the Payments API's separate/tokenbearer flow for refunds, and it is a different token from the one your payin code holds - Replace the batch array to
/secure/refunds/submitwith eitherPOST /transactions/{id}/refundsfor a single refund orPOST /refundsfor a batch, see Refunds: One API - Add an
Idempotency-Keyheader 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
refundReasonbecomesreason, and is still requiredisRtcbecomesrealTimePayment- Refund statuses keep their spelling but shrink from nine values to six:
Pending,Submitted,Complete,Failed,CancelledandReturned.PendingInvestigation,InvalidandErrordo 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.
- 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.
- 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.
- 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.
- 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.
- 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
- Review the Building a secure integration checklist, the webhook and credential handling sections apply to One API's model
- Test your migrated integration using Money in test cases
- See Redirect to Ozow with One API and Refund a payment with One API for the complete guides
In the API reference
6 entries
Last updated