Start a Project
All guides

Payments

Ozow Instant EFT setup for South African stores.

Ozow pioneered instant EFT in South Africa — the buyer authenticates inside their own internet banking session with FNB, Absa, Standard Bank, Nedbank, Capitec, Investec or Tyme, and Ozow confirms the clearing to you in near real time. It is not a card processor, it is a bank-to-bank rail, which means lower fees but zero chargeback protection. Most SA Shopify stores offer Ozow alongside a card gateway rather than as the only option. This guide covers the SiteCode plus HashCheck flow, SHA512 hash generation, and the notify/redirect split.

Updated 15 April 2026 · 11 min read · any · Joshua Kaplan

Prerequisites

  • Ozow merchant account with FICA and bank verification complete
  • SiteCode, PrivateKey and ApiKey from the Ozow dashboard (distinct test and live values)
  • Publicly reachable HTTPS notify URL
  • SA business bank account linked for settlement
  • Awareness that Ozow only works for buyers with SA bank accounts

Step 1. Create a merchant profile and fetch credentials

Sign up at ozow.com, complete FICA (ID, proof of address, bank letter, CIPC for companies). Once approved, the dashboard exposes your SiteCode, PrivateKey and ApiKey. Generate a separate test site for development — never develop against live.

Step 2. Understand the hash order

Ozow verifies requests with a SHA512 hash over a specific concatenation of fields — in a fixed order — followed by your PrivateKey, then lowercased. Get the order wrong and every request fails silently with a generic error.

ozow-hash.ts
import crypto from 'node:crypto';

// Field order per Ozow spec — do not change
const FIELD_ORDER = [
  'SiteCode',
  'CountryCode',
  'CurrencyCode',
  'Amount',
  'TransactionReference',
  'BankReference',
  'Customer',
  'CancelUrl',
  'ErrorUrl',
  'SuccessUrl',
  'NotifyUrl',
  'IsTest',
] as const;

export function ozowHashCheck(
  payload: Record<string, string | number | boolean>,
  privateKey: string,
) {
  const concat =
    FIELD_ORDER.map((k) =>
      payload[k] === undefined || payload[k] === null ? '' : String(payload[k]),
    ).join('') + privateKey;

  return crypto
    .createHash('sha512')
    .update(concat.toLowerCase())
    .digest('hex');
}

Step 3. Build the Ozow payment request

POST the form to pay.ozow.com. Amount is in Rands with two decimals. IsTest should be true during development and false in production. TransactionReference must be unique — use your Shopify order name plus a retry counter.

ozow-redirect.html
<form method="post" action="https://pay.ozow.com">
  <input type="hidden" name="SiteCode" value="YOUR_SITE_CODE" />
  <input type="hidden" name="CountryCode" value="ZA" />
  <input type="hidden" name="CurrencyCode" value="ZAR" />
  <input type="hidden" name="Amount" value="499.00" />
  <input type="hidden" name="TransactionReference" value="SHOPIFY-1001-1" />
  <input type="hidden" name="BankReference" value="ORDER1001" />
  <input type="hidden" name="Customer" value="customer@example.co.za" />
  <input type="hidden" name="CancelUrl" value="https://yourstore.co.za/ozow/cancel" />
  <input type="hidden" name="ErrorUrl" value="https://yourstore.co.za/ozow/error" />
  <input type="hidden" name="SuccessUrl" value="https://yourstore.co.za/ozow/success" />
  <input type="hidden" name="NotifyUrl" value="https://yourstore.co.za/ozow/notify" />
  <input type="hidden" name="IsTest" value="true" />
  <input type="hidden" name="HashCheck" value="__computed_sha512__" />
  <button type="submit">Pay with Ozow</button>
</form>

Step 4. Handle the notify callback server-side

Ozow POSTs to NotifyUrl on final status. Ignore the SuccessUrl for order fulfillment — users bounce. Verify the inbound HashCheck using the same SHA512 pattern but with the notify-specific field order, then fulfill the order only on Status = "Complete".

ozow-notify.ts
const NOTIFY_ORDER = [
  'SiteCode',
  'TransactionId',
  'TransactionReference',
  'Amount',
  'Status',
  'Optional1',
  'Optional2',
  'Optional3',
  'Optional4',
  'Optional5',
  'CurrencyCode',
  'IsTest',
  'StatusMessage',
] as const;

export async function handleOzowNotify(req: Request, privateKey: string) {
  const body = await req.formData();
  const data = Object.fromEntries(body.entries()) as Record<string, string>;
  const received = (data['Hash'] || '').toLowerCase();

  const concat =
    NOTIFY_ORDER.map((k) => data[k] ?? '').join('') + privateKey;
  const expected = crypto
    .createHash('sha512')
    .update(concat.toLowerCase())
    .digest('hex');

  if (expected !== received) {
    return new Response('bad hash', { status: 400 });
  }

  if (data.Status === 'Complete') {
    await markOrderPaid(data.TransactionReference, data.TransactionId);
  }
  return new Response('OK', { status: 200 });
}

Step 5. Test against the Ozow test environment

With IsTest = true and your test SiteCode, Ozow routes you to a sandbox bank picker. They publish test credentials for simulated success, failure and abandonment. Run all three plus a duplicate TransactionReference to confirm idempotency.

Step 6. Integrate as a Shopify payment method

Like Payfast/Yoco, Ozow on non-Plus Shopify is a Manual Payment Method with a cart-redirect bridge. The Ozow Shopify app simplifies this — install from the App Store and point it at your live credentials.

Step 7. Go live and reconcile daily

Flip IsTest to false, swap SiteCode and PrivateKey to live values, and reconcile the Ozow merchant dashboard against Shopify orders every morning for the first two weeks. EFT clearing edge cases (bank timeouts) are the main cause of "paid but not marked paid" discrepancies.

SA gotchas

  • Ozow only works if the buyer has a South African bank account with a supported bank. International buyers cannot use it — always offer a card option alongside.
  • There is no chargeback mechanism on Instant EFT. If you sell high-ticket goods and ship before reconciling, you inherit the fraud risk completely.
  • Approximate fees are ~1.5% (verify current rates in the dashboard — Ozow has volume tiers). Cheaper than card but the no-chargeback trade-off is real.
  • Ozow does not support recurring billing — it is a one-shot bank authorisation each time. Use Stitch for pay-by-bank recurring.
  • Hash case-sensitivity: the final hash must be lowercased BEFORE hashing, then the hex digest itself is compared lowercased. Getting either step wrong causes silent failures.
  • The NotifyUrl is authoritative, not the SuccessUrl redirect. Buyers close tabs — you will lose orders if you fulfill off the return query string.

Frequently asked questions

Is Ozow POPIA-compliant?

Ozow is a regulated SA payment initiation provider. They process the bank login flow on their own infrastructure, so your store never sees banking credentials. You still need to cover them as an Operator in your POPIA privacy notice.

Does Ozow work outside South Africa?

Ozow has expanded into limited other African markets, but for practical purposes on a SA Shopify store, treat it as SA-bank-only. International card buyers should be routed to Payfast, Peach, Yoco or Stripe.

Does Ozow support 3DS2?

3DS2 does not apply — Ozow is not a card rail. The authentication happens inside the buyer's own internet banking session, which is inherently strong customer authentication.

How fast is settlement?

Ozow settles into your linked bank account typically T+1 business day for established merchants, sometimes same-day for intrabank transfers.

Can I refund an Ozow transaction?

Yes via the Ozow dashboard — you initiate a bank-to-bank refund to the original account. It is not instant; expect 1–2 business days for the buyer to see the reversal.