Ozow Hub
On this page12 sections
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.
    View package

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

  1. Create a payment request with POST /postpaymentrequest, with NotifyUrl set
  2. Complete the payment with a valid payment method
  3. Verify that your endpoint receives the notification
  4. Verify that the hash validates
  5. Verify that the order is updated
  6. Verify that the customer reaches your SuccessUrl

Expected outcomes

  • Status is Complete
  • Order credited and fulfilled once
  • Customer redirected to SuccessUrl

Test 2: Cancelled payment

Verify that a cancellation does not credit the order.

Steps

  1. Create a payment request
  2. Cancel the payment on the Ozow payment page
  3. Verify that your endpoint receives the notification
  4. Verify that the order is not credited
  5. Verify that the customer reaches your CancelUrl

Expected outcomes

  • Status is Cancelled
  • Order not credited
  • Customer redirected to CancelUrl

Test 3: Failed payment

Verify that a failure does not credit the order.

Steps

  1. Create a payment request
  2. Attempt a payment that fails
  3. Verify that your endpoint receives the notification
  4. Verify that the order is not credited
  5. Verify that the customer reaches your ErrorUrl

Expected outcomes

  • Status is Error, with the detail in StatusMessage
  • Order not credited
  • Customer redirected to ErrorUrl

Test 4: Hash verification

Verify that verification actually rejects something.

Steps

  1. Complete a successful test payment and keep the notification body
  2. Verify it with your implementation and confirm it passes
  3. Change one field, Amount or Status, leaving Hash as it was, and replay it
  4. 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

  1. Complete a successful test payment
  2. Deliver the same notification to your endpoint a second time
  3. 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

  1. Complete a test payment and note the transaction reference
  2. Call GET /GetTransactionByReference
  3. 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

  1. Build the standalone button with the correct SelectedBankId
  2. Create a payment request with that field set
  3. 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

  1. Create a payment request with a verified customer identity in customerIdentifier
  2. Verify that the page offers only payment methods linked to that identity
  3. Verify that unlinked payment methods are hidden
  4. 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