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.

  • Take a paymentEverything needed to take a payment end to end with One API, from credentials through the hosted page to the webhook that confirms it, and the test cases that prove each outcome before you go live.
    View package

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.

Building on the Payments API instead? Use Payin test cases: Payments API; 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 One API payment tells you its outcome on a transaction.complete 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., and also on the standard Ozow notification when you set notifyUrl on the payment request. Configure both and both fire, which is what Test 5 exists for.

The webhook carries one envelope whatever happened, and data.status is one of four values:

data.status The transaction was
Successful completed
Incomplete created and not taken further
Pending pending, or under investigation
Error anything else, including cancelled, abandoned and voided

There is no Cancelled to test for. A cancellation arrives as Error, and data.reason says which kind of failure it was. A handler that switches on Cancelled never runs that branch.

A handler that passes every test below

The tests are all the same handler seen from different angles: verify, then read the status, then update the order once. This is that handler, written against svix 2.x.

import { Webhook } from "svix";

// From Get Webhook Secret. Keep it out of source control.
const webhook = new Webhook(process.env.OZOW_WEBHOOK_SECRET);

app.post(
  "/ozow/webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    try {
      // Verify against the raw body. Parsing first and re-serialising changes
      // the bytes and the signature will not match. Test 4.
      webhook.verify(req.body, {
        "svix-id": req.headers["svix-id"],
        "svix-timestamp": req.headers["svix-timestamp"],
        "svix-signature": req.headers["svix-signature"],
      });
    } catch {
      // Rejected, logged, and never acted on.
      return res.status(400).send("invalid signature");
    }

    // `verify` returns nothing on svix 2.x: it throws on a bad signature and
    // that is the whole result. Parse after it, never before. On svix 1.x it
    // returned the parsed body, so a handler carried over from that version
    // reads `undefined` here and fails after it has already acknowledged.
    const event = JSON.parse(req.body);

    // Acknowledge first. Ozow retries anything that is not a 2xx, and a slow
    // database is not a reason to be sent the same event again.
    res.sendStatus(200);

    const { type, data } = event;
    if (type !== "transaction.complete") return;

    // The same event can arrive more than once, and a payment can also notify
    // twice when `notifyUrl` is set alongside the webhook. Test 5.
    if (!claimOnce(data.id)) return;

    switch (data.status) {
      case "Successful":
        fulfilOrder(data.id);
        break;
      case "Pending":
        // Not an outcome. Leave the order alone and wait for the next event.
        break;
      case "Incomplete":
      case "Error":
        // Everything that is not a payment: cancelled, abandoned, voided, failed.
        failOrder(data.id, data.reason);
        break;
      default:
        // A value this code has never seen. Do not guess what it means.
        alertOps(`unknown status ${data.status} on ${data.id}`);
    }
  },
);

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. Deciding by "have I seen this id" in memory does not survive two instances.

Important

Update the order from this handler, never from the browser returning to your success URL. 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 with POST /payments
  2. Complete the payment on the Ozow payment page with a valid payment method
  3. Verify that your endpoint receives the webhook
  4. Verify that the Svix signature validates
  5. Verify that the order is updated
  6. Verify that the customer reaches your success URL

Expected outcomes

  • type is transaction.complete and data.status is Successful
  • Order credited and fulfilled once
  • Customer redirected to the success URL

Test 2: Cancelled payment

Verify that a cancellation does not credit the order.

Steps

  1. Create a payment
  2. Cancel it on the Ozow payment page
  3. Verify that your endpoint receives the webhook
  4. Verify that the order is not credited
  5. Verify that the customer reaches your cancel URL

Expected outcomes

  • data.status is Error, not Cancelled, with the detail in data.reason
  • Order not credited
  • Customer redirected to the cancel URL

Test 3: Failed payment

Verify that a failure does not credit the order.

Steps

  1. Create a payment
  2. Attempt a payment that fails
  3. Verify that your endpoint receives the webhook
  4. Verify that the order is not credited
  5. Verify that the customer reaches your error URL

Expected outcomes

  • data.status is Error, with the reason in data.reason
  • Order not credited
  • Customer redirected to the error URL

Test 4: Signature verification

Verify that verification actually rejects something.

Steps

  1. Complete a successful test payment and keep the delivery
  2. Verify it with your implementation and confirm it passes
  3. Change one byte of the body, or one character of svix-signature, and replay it
  4. Confirm the tampered delivery fails verification and is not processed

Expected outcomes

  • The genuine delivery passes
  • The tampered delivery is rejected, logged, and updates nothing
  • Verification runs against the raw body, not a re-serialised object

Test 5: The same outcome twice

Verify that one payment updates one order once.

Note

Ozow retries a delivery your endpoint did not acknowledge, and a payment with both a webhook and notifyUrl reports twice by design. Both look like a duplicate to your handler.

Steps

  1. Complete a successful test payment
  2. Deliver the same event to your endpoint a second time
  3. If you have set notifyUrl as well, confirm what that notification does to the same order

Expected outcomes

  • The second delivery 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 payment id
  2. Call GET /payments/{id}/transactions
  3. Compare the status with the outcome you were sent

Expected outcomes

  • The call returns the transaction
  • Its status matches the payment's outcome

Note

This endpoint returns the transaction's own status, which is the full set on the statuses page, not the four the webhook maps to. A payment the webhook reports as Complete reads Successful here. Compare without case.


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 institutionId
  2. Create a payment 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 with a verified customer identity in payer.identity
  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 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

4 entries

Last updated