Payin test cases
The payments to run before you go live on the Payments API, what each one delivers, and a notification handler that survives all of them.
On this page12 sections
- What arrives, and what it says
- A handler that passes every test below
- Core test cases
- Test 1: Successful payment
- Test 2: Cancelled payment
- Test 3: Failed payment
- Test 4: Hash verification
- Test 5: The same notification twice
- Test 6: Transaction status check
- Optional test cases
- Test 7: Standalone button
- Test 8: Customer Identity Verification
Build with AI 1 package
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.
- Embed checkout in your own pageEverything needed to keep the customer on your site while they pay, as an iframe, a modal, or the Wallet SDK for Apple Pay and Google Pay, with the notification that actually confirms the payment.
The Payments API is deprecated.
This guide applies to you 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. Your integration keeps working and remains supported. New
payment methods and features are released on the One API only, and all new integrations use it,
no new merchants are onboarded onto the Payments API. No end-of-life date has been set. We
recommend planning a move when you next have development capacity. See Migrating to One
API.
These test cases are recommended but not mandatory for go-live. Run at least the core ones before you accept real payments. The optional ones apply only if you have built the feature they test.
They cover the Payments API, which is what the embedded iframe, embedded modal and embedded wallet checkouts are built on as well as the legacy 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.. Building on One API? Use payin test cases; the statuses and the field names are different.
Testing environment
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. integrations can be tested directly in production. Any test transactions settle into your configured bank account. To test without moving real money, staging credentials are available on request: contact your account manager or support@ozow.com.
What arrives, and what it says
A Payments API payment tells you its outcome on the
transaction notification: a form encoded
POST to the NotifyUrl on the payment request, authenticated by its Hash field. Set no
NotifyUrl and nothing is delivered.
Status is the transaction's own status, not a mapped one: Complete, Cancelled, Error,
Abandoned, PendingInvestigation and the rest of the set on the statuses
page.
Important
The notification is an unauthenticated POST to a URL anyone can call. The
Hash is what makes it Ozow's. A handler that trusts the body without checking it can be told
that any order is paid.
A handler that passes every test below
import crypto from "node:crypto";
// The fields, in this order. Empty ones are skipped.
const HASH_FIELDS = [
"SiteCode",
"TransactionId",
"TransactionReference",
"Amount",
"Status",
"Optional1",
"Optional2",
"Optional3",
"Optional4",
"Optional5",
"CurrencyCode",
"IsTest",
"StatusMessage",
];
function isFromOzow(body) {
const parts = HASH_FIELDS.map((field) => body[field] ?? "").filter(
(value) => value !== "",
);
// Private key appended, then the whole string lowercased, then SHA512.
const check = crypto
.createHash("sha512")
.update((parts.join("") + process.env.OZOW_PRIVATE_KEY).toLowerCase())
.digest("hex");
// Fixed-time compare: a plain === leaks the answer one character at a time.
const sent = Buffer.from(String(body.Hash ?? "").toLowerCase());
const ours = Buffer.from(check);
return sent.length === ours.length && crypto.timingSafeEqual(sent, ours);
}
app.post(
"/ozow/notify",
express.urlencoded({ extended: false }),
(req, res) => {
// Test 4. Rejected, logged, and never acted on.
if (!isFromOzow(req.body)) return res.status(400).send("invalid hash");
res.sendStatus(200);
const { TransactionId, Status, StatusMessage } = req.body;
// Test 5. The same notification can arrive more than once.
if (!claimOnce(TransactionId)) return;
switch (Status) {
case "Complete":
fulfilOrder(TransactionId);
break;
case "Pending":
case "PendingInvestigation":
// Not an outcome. Leave the order alone.
break;
case "Cancelled":
case "Error":
case "Abandoned":
failOrder(TransactionId, StatusMessage);
break;
default:
// A value this code has never seen. Do not guess what it means.
alertOps(`unknown status ${Status} on ${TransactionId}`);
}
},
);
Amount is hashed with two decimal places, exactly as it was sent. claimOnce is whatever makes
the update happen once in your system: a unique constraint on the transaction id, a row lock, or a
conditional write.
Important
Update the order from this handler, never from the browser returning to your success URL, and never from an SDK event. A customer who closes the tab still has to end up with the right order state.
Core test cases
Test 1: Successful payment
Verify that your integration handles a completed payment end to end.
Steps
- Create a payment request with
POST /postpaymentrequest, withNotifyUrlset - Complete the payment with a valid payment method
- Verify that your endpoint receives the notification
- Verify that the hash validates
- Verify that the order is updated
- Verify that the customer reaches your
SuccessUrl
Expected outcomes
StatusisComplete- Order credited and fulfilled once
- Customer redirected to
SuccessUrl
Test 2: Cancelled payment
Verify that a cancellation does not credit the order.
Steps
- Create a payment request
- Cancel the payment on the Ozow payment page
- Verify that your endpoint receives the notification
- Verify that the order is not credited
- Verify that the customer reaches your
CancelUrl
Expected outcomes
StatusisCancelled- Order not credited
- Customer redirected to
CancelUrl
Test 3: Failed payment
Verify that a failure does not credit the order.
Steps
- Create a payment request
- Attempt a payment that fails
- Verify that your endpoint receives the notification
- Verify that the order is not credited
- Verify that the customer reaches your
ErrorUrl
Expected outcomes
StatusisError, with the detail inStatusMessage- Order not credited
- Customer redirected to
ErrorUrl
Test 4: Hash verification
Verify that verification actually rejects something.
Steps
- Complete a successful test payment and keep the notification body
- Verify it with your implementation and confirm it passes
- Change one field,
AmountorStatus, leavingHashas it was, and replay it - Confirm the tampered notification fails verification and is not processed
Expected outcomes
- The genuine notification passes
- The tampered notification is rejected, logged, and updates nothing
- Empty fields are skipped, and the string is lowercased before hashing
Test 5: The same notification twice
Verify that one payment updates one order once.
Note
Ozow may send the same notification more than once. Your handler has to survive it.
Steps
- Complete a successful test payment
- Deliver the same notification to your endpoint a second time
- Verify that your handler processes it idempotentlyIdempotency 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
Expected outcomes
- The second notification is recognised and changes nothing
- The order is credited once
- Nothing throws
Test 6: Transaction status check
Verify that you can ask, rather than wait.
Steps
- Complete a test payment and note the transaction reference
- Call
GET /GetTransactionByReference - Compare the status with the outcome you were sent
Expected outcomes
- The call returns the transaction
- Its status matches the notification
Optional test cases
Run these only if you have built the feature.
Test 7: Standalone button
Verify that a button opens the payment method it names.
Steps
- Build the standalone button with the correct
SelectedBankId - Create a payment request with that field set
- Verify that the Ozow payment page opens on that payment method
Expected outcomes
- The named payment method is shown
- The customer is routed to it without choosing again
Test 8: Customer Identity Verification
Run this only if your business is in a high-risk industry and 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. is required.
Steps
- Create a payment request with a verified customer identity in
customerIdentifier - Verify that the page offers only payment methods linked to that identity
- Verify that unlinked payment methods are hidden
- Create a payment request with no identity and verify which methods are offered
Expected outcomes
- Only linked payment methods are offered when an identity is passed
- The behaviour without an identity is what you expect for your account
In the API reference
3 entries
Last updated