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 page12 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
- What this guide does not cover
- 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. 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. 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 payment methods are added to One API first.
Before you start
- You have a working Payments API integration using Redirect: Payments API, Refunds: Payments API, or both
- You have a Client ID and Client Secret from the One API Clients section of your Ozow Dashboard
- You can run a staging integration against One API before touching production
Note
Only users with administrator privileges in the Ozow Dashboard can access the One API Clients section.
What does not change
- 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. stays the same
- The payment methods available to your customers, 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., 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, 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 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 Dashboard 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 |
| Recurring payments | Not available | Available, see Recurring payments: One API |
| Embedded checkout | Not available, redirect only | Available: iframe, modal, and Wallet SDK |
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 1: 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 Dashboard: navigate to One API Clients, select your client, and manage webhooks from there
- Via the API: see the webhook endpoints. These need a token
with the
webhooksscope, and so does Get Webhook Secret below. Your payin token does not reach them
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.
See Step 4: Handle the webhook notification for the full webhook setup and verification process.
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 payin 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
Do not attempt a single cutover. Payments API and One API are separate systems, and a payment created on one is not visible on the other.
- Build and test the full One API flow in staging: token, payment creation, webhook delivery, and refunds if you use them
- Deploy the One API integration behind a flag or a new code path, without removing the Payments API path yet
- Route new payments to One API while existing in-flight Payments API payments finish on the old flow
- Keep your Payments API webhook handler live until every payment created before the cutover has resolved, a payment can still complete or time out for some time after creation
- Once no in-flight Payments API payments remain, decommission the old endpoint calls, retire the private key, and remove the hash-generation code
Important
Test in the staging environment before switching production traffic. Staging credentials and endpoints are entirely separate between the two APIs, a Payments API staging site code does not carry over to One API.
What this guide does not cover
If your Payments API integration uses more than payin and refunds, the rest moves too, and each part has its own guide rather than a step here:
| On the Payments API | Where it goes |
|---|---|
/, / |
Reconcile settlements |
/, / |
Send a payout |
/ |
No One API equivalent, contact support before you migrate |
Migrate payin first. The others are separate integrations against separate endpoints, and each can move on its own schedule.
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 Payin test cases
- See Redirect: One API and Refunds: One API for the complete guides
- Contact support@ozow.com if you need your One API Client ID and Client Secret issued or your webhook endpoint configured
In the API reference
6 entries
Last updated