# Migrate a payin from the Payments API to One API

> Everything needed to move an existing redirect payin onto One API, with the legacy guide and its One API counterpart side by side.

Both sides are implemented against: the legacy redirect and its One API
counterpart. Start with the migration guide, which maps the field names across,
then read the two redirect guides beside each other.

Six changes, in the order the guide takes them: the hash gives way to a bearer
token, flat fields become nested resources, the response carries a URL to
redirect to rather than a form to post, the per-request `notifyUrl` becomes one
webhook subscription with a Svix signature, the statuses re-map, and refunds
move. **Refunds are a separate package, `migrate-refunds-to-one-api`.**

`SelectedBankId` on the Payments API is `institutionId` on One API. The same
UUID identifies the same bank on both.

Test and live are separated by environment, not by a flag on the request.

## What this was built from

- Ozow Hub, commit `e0b2a572`
- `one-api` version 1.0, OpenAPI document: https://hub.ozow.com/api-reference/specs/one-api.yaml
- `payments-api` version 1.0, OpenAPI document: https://hub.ozow.com/api-reference/specs/payments-api.yaml
- Build against `https://one.ozow.com/v1` for `one-api`
- Build against `https://api.ozow.com` for `payments-api`
- 8 pages, 16 operations, inlined in full below
- The same package as links: https://hub.ozow.com/bundles/migrate-a-payin-to-one-api.md

---

# Implement against these

Every field name, order and format below is exact. Copy them as written.

---

# 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.

Source: https://hub.ozow.com/integration-methods/apis/deprecated-integrations/migrating-to-one-api/

This guide maps a redirect payin 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](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/redirect-to-ozow.md),
  [Refunds: Payments API](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/refund-a-payment.md), or both
- You have a Client ID and Client Secret from the One API Clients section of your [Ozow Dashboard](https://dash.ozow.com/MerchantAdmin/OneAPI/Clients)
- 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 code stays the same
- The payment methods available to your customers, Pay by Bank, Capitec Pay, Buy Now Pay Later,
  PayShap Request, are the same products on both APIs
- Your float, settlement schedule, and Ozow Dashboard reporting are shared across both APIs
- ZAR is the only supported currency on either API

## What changes

| Concept | Payments API | One API |
|---|---|---|
| Authentication | API key plus a SHA512 hash signed with your private key, per request | OAuth 2.0 Client Credentials, a bearer token from `/v1/token` |
| Request shape | Flat form fields, mostly `PascalCase` in notifications, `camelCase` in requests | Nested JSON resources, `camelCase` throughout |
| Creating a payment | `POST /postpaymentrequest` returns a `url` directly | `POST /v1/payments` returns a `Payment` resource with a `redirectUrl` 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 /payments/{id}/transactions` |
| Cancelling before completion | Not possible via API, only the customer can abandon the payment page | `POST /payments/{id}/cancel` |
| Outcome delivery | A form-encoded POST to `notifyUrl`, authenticated with a hash you verify yourself | A signed webhook delivered via [Svix](https://www.svix.com/), authenticated with `svix-id`, `svix-timestamp`, and `svix-signature` |
| Refunds | Submitted as a batch array to `/secure/refunds/submit`, one hash check per item | `POST /transactions/{id}/refunds` for a single transaction, or `POST /refunds` for a batch, both idempotent via an `Idempotency-Key` header |
| Cancelling a refund | Not possible via API | `POST /refunds/{id}/cancel` |
| Standalone payment method button | `selectedBankId` field in the payment request | `institutionId` field in the payment request |
| Customer Identity Verification | Flat `customerIdentifier` field | Nested `payer.identity` 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](https://hub.ozow.com/integration-methods/apis/recurring-payments/set-up-recurring-payments.md) |
| Embedded checkout | Not available, redirect only | Available: [iframe](https://hub.ozow.com/integration-methods/apis/payin/embedded-iframe.md), [modal](https://hub.ozow.com/integration-methods/apis/payin/embedded-modal.md), and [Wallet SDK](https://hub.ozow.com/integration-methods/apis/payin/embedded-wallet.md) |

---

## 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:

```endpoint
POST https://one.ozow.com/v1/token
Content-Type: application/x-www-form-urlencoded
```

```bash
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](https://hub.ozow.com/integration-methods/apis/payin/redirect-to-ozow.md#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**

```json
{
  "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**

```json
{
  "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 |
|---|---|---|
| `siteCode` | `siteCode` | Unchanged |
| `countryCode` | `region` | Same ISO 3166-alpha-2 code, renamed and moved to the top level. Optional on One API, and defaults to `ZA` when you omit it |
| `currencyCode` | `amount.currency` | Moves inside `amount`, and must be `ZAR` |
| `amount` (string) | `amount.value` (number) | One API takes a numeric value, not a pre-formatted string |
| `transactionReference` | `merchantReference` | Same purpose, renamed. Still the reference the payer sees on their own statement, and still yours to keep unique |
| `bankReference` | `beneficiaryReference` | 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 |
| `cancelUrl`, `errorUrl`, `successUrl` | `returnUrl` | One API redirects to a single `returnUrl` regardless of outcome; determine the outcome from a verified webhook or the transactions endpoint, not from which URL fired |
| `notifyUrl` | `notifyUrl` | 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 |
| `expiryDateUtc` | `expireAt` | **Now required on every payment request**, where the Payments API let you omit it. ISO 8601 (`2026-12-31T23:59:59Z`) rather than `yyyy-MM-dd HH:mm` |
| `isTest` | Not a request field | Test and live are separated by environment (`stagingone.ozow.com` vs `one.ozow.com`), not by a flag on the request |
| `hashCheck` | Not a request field | Removed entirely, see Step 1 |

For the full request and response schema see [Create a payment](https://hub.ozow.com/api-reference/one-api/post-payments.md).

---

## Step 3: Update how you read the response and redirect the customer

**Payments API response**

```json
{
  "paymentRequestId": "00000000-0000-0000-0000-000000000000",
  "url": "https://pay.ozow.com/00000000-0000-0000-0000-000000000000/Secure",
  "errorMessage": null
}
```

**One API response**

```json
{
  "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](https://hub.ozow.com/api-reference/one-api/get-payments-id.md), [List its
> transactions](https://hub.ozow.com/api-reference/one-api/get-payments-id-transactions.md) and [Cancel
> it](https://hub.ozow.com/api-reference/one-api/post-payments-id-cancel.md) 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](https://dash.ozow.com/MerchantAdmin/OneAPI/Clients), select your client, and manage
  webhooks from there
- **Via the API**: see the [webhook endpoints](https://hub.ozow.com/api-reference/one-api/tags/webhooks.md). These need a token
  with the `webhooks` scope, and so does [Get Webhook
  Secret](https://hub.ozow.com/api-reference/one-api/get-webhooks-id-secret.md) below. Your payin token does not reach them

**Subscribe to [`transaction.complete`](https://hub.ozow.com/api-reference/one-api/webhooks/transaction-complete.md)** 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](https://hub.ozow.com/integration-methods/apis/payin/verify-a-webhook.md) has the five steps and a
working verifier in four languages. Retrieve your webhook secret with
[Get Webhook Secret](https://hub.ozow.com/api-reference/one-api/get-webhooks-id-secret.md).

> ⚠️ **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](https://hub.ozow.com/integration-methods/apis/payin/redirect-to-ozow.md#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 `SubStatus` for detail |
| `Abandoned` | Customer left the payment page without completing |
| `Pending` | The outcome is not known yet and is reposted to your `notifyUrl` once it is. If you do not use a `notifyUrl` you receive `PendingInvestigation` instead |
| `PendingInvestigation` | 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 `refunds` scope, the same call as Step 1 with a different `scope`. This
  replaces the Payments API's separate `/token` bearer flow for refunds, and it is a different token
  from the one your payin code holds
- Replace the batch array to `/secure/refunds/submit` with either `POST /transactions/{id}/refunds`
  for a single refund or `POST /refunds` for a batch, see [Refunds: One
  API](https://hub.ozow.com/integration-methods/apis/refunds/refund-a-payment.md)
- Add an `Idempotency-Key` header 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
- `refundReason` becomes `reason`, and is still required
- `isRtc` becomes `realTimePayment`
- Refund statuses keep their spelling but shrink from nine values to six: `Pending`, `Submitted`,
  `Complete`, `Failed`, `Cancelled` and `Returned`. `PendingInvestigation`, `Invalid` and `Error`
  do 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.

1. Build and test the full One API flow in staging: token, payment creation, webhook delivery, and
   refunds if you use them
2. Deploy the One API integration behind a flag or a new code path, without removing the Payments
   API path yet
3. Route new payments to One API while existing in-flight Payments API payments finish on the old flow
4. 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
5. 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 |
|---|---|
| `/secure/settlements`, `/secure/settlements/getsitesettlements` | [Reconcile settlements](https://hub.ozow.com/integration-methods/apis/settlements/reconcile-settlements.md) |
| `/secure/banktransfer/single`, `/secure/banktransfer/multiple` | [Send a payout](https://hub.ozow.com/integration-methods/apis/payout/send-a-payout.md) |
| `/secure/bulkpaymentrequests/create` | 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](https://hub.ozow.com/getting-started/building-a-secure-integration.md) checklist, the webhook and
  credential handling sections apply to One API's model
- Test your migrated integration using [Payin test cases](https://hub.ozow.com/integration-methods/testing/payin-test-cases-one-api.md)
- See [Redirect: One API](https://hub.ozow.com/integration-methods/apis/payin/redirect-to-ozow.md) and [Refunds: One
  API](https://hub.ozow.com/integration-methods/apis/refunds/refund-a-payment.md) for the complete guides
- Contact [support@ozow.com](mailto:support@ozow.com) if you need your One API Client ID and Client
  Secret issued or your webhook endpoint configured

---

# Redirect to Ozow

> Build a redirect payin on the Payments API, the legacy path. Post the payment, redirect the customer, and handle the notification response.

Source: https://hub.ozow.com/integration-methods/apis/deprecated-integrations/redirect-to-ozow/

This guide walks you through a redirect payin integration using the Payments API. Your system posts
payment information to Ozow, redirects the customer to the Ozow-hosted payment page, and receives a
notification response when the payment is complete.

> ⚠️ **Legacy integration: use One API for new integrations**: Payments API is a legacy integration
> path and will not receive new features. If you are starting a new payin integration, use
> [Redirect: One API](https://hub.ozow.com/integration-methods/apis/payin/redirect-to-ozow.md) instead. This guide exists to support merchants
> already integrated on Payments API.

## Before you start

- You have completed [Prerequisites and onboarding](https://hub.ozow.com/getting-started/prerequisites-and-onboarding.md)
- You have your API key, private key, and site code from your [Ozow Dashboard](https://dash.ozow.com)
- Your notification URL, success URL, cancel URL, and error URL are set up and publicly accessible
  via HTTPS

## Environments

| Environment | API endpoint | Dashboard |
|---|---|---|
| Production | `https://api.ozow.com/postpaymentrequest` | [dash.ozow.com](https://dash.ozow.com) |
| Staging | `https://stagingapi.ozow.com/postpaymentrequest` | [stagingdash.ozow.com](https://stagingdash.ozow.com) |

## How redirect works

```mermaid
sequenceDiagram
    participant C as Customer
    participant M as Your system
    participant O as Payments API
    participant P as Ozow payment page

    C->>M: Reaches checkout
    M->>O: POST /postpaymentrequest
    O-->>M: Returns payment URL
    M->>C: Redirects customer to payment URL
    C->>P: Completes payment
    O-->>M: Sends notification response to NotifyUrl
    M->>M: Verifies notification hash
    M-->>C: Redirects customer to SuccessUrl, CancelUrl, or ErrorUrl
```

---

## Core integration

### Step 1: Generate the hash check

The Payments API uses SHA512 hash-based authentication. Before posting a payment request, you must
generate a hash check to sign the request.

**How to generate the hash check**

> ⚠️ **Critical, field order matters**: The fields must be concatenated in exactly the order listed
> in the request fields table below. Using the wrong order is the most common cause of hash check
> failures. Only include fields that have a value, empty or unused fields must be excluded from the
> hash entirely, not included as empty strings.

1. Concatenate the post variables (excluding `HashCheck` and `Token`) in the order they appear in
   the request fields table below
2. Append your private key to the concatenated string
3. Convert the entire string to lowercase
4. Generate a SHA512 hash of the lowercase string

> ℹ️ **Note**: Boolean values must be represented as the strings `true` or `false` in the
> concatenated string. Some languages may convert booleans to `0` or `1` which will result in a
> failed hash check.

> ⚠️ **Important**: The amount must be formatted with exactly two decimal places, `100.00` rather
> than `100` or `100.0`. Most languages drop a trailing zero when a number is converted to a string,
> so format the amount before you concatenate it. This is the most common cause of a failed hash
> check.

**Hash check example**

Given the following values:

| Field | Value |
|---|---|
| SiteCode | TSTSTE0001 |
| CountryCode | ZA |
| CurrencyCode | ZAR |
| Amount | 25.00 |
| TransactionReference | 123 |
| BankReference | ABC123 |
| CancelUrl | `http://demo.ozow.com/cancel.aspx` |
| ErrorUrl | `http://demo.ozow.com/error.aspx` |
| SuccessUrl | `http://demo.ozow.com/success.aspx` |
| NotifyUrl | `http://demo.ozow.com/notify.aspx` |
| IsTest | false |

The concatenated string before hashing would be:

```text
tstste0001zazar25.00123abc123http://demo.ozow.com/cancel.aspxhttp://demo.ozow.com/error.aspxhttp://demo.ozow.com/success.aspxhttp://demo.ozow.com/notify.aspxfalse[your private key]
```

Resulting hash:

```text
4a2e7db32f76747b0f434edeb62e8b3ebb04125025feae622bc09092296bc965cec49a8cbeeef0e21f3c4c04249de69dd0b330a5b7da21c51a95d360d03f54ba
```

**Code examples**

**C#**

```csharp
using System.Globalization;
using System.Security.Cryptography;
using System.Text;

void GenerateRequestHash()
{
    string siteCode = "YOUR_SITE_CODE";
    string countryCode = "ZA";
    string currencyCode = "ZAR";
    string amount = 100.00M.ToString("0.00", CultureInfo.InvariantCulture);
    string transactionReference = "ORDER-001";
    string bankReference = "ABC123";
    string cancelUrl = "https://yourstore.com/cancel";
    string errorUrl = "https://yourstore.com/error";
    string successUrl = "https://yourstore.com/success";
    string notifyUrl = "https://yourstore.com/notify";
    string privateKey = "YOUR_PRIVATE_KEY";
    bool isTest = false;

    string inputString = string.Concat(
            siteCode,
            countryCode,
            currencyCode,
            amount,
            transactionReference,
            bankReference,
            cancelUrl,
            errorUrl,
            successUrl,
            notifyUrl,
            isTest,
            privateKey
        )
        .ToLower();

    using SHA512 sha512 = new SHA512CryptoServiceProvider();
    var bytes = sha512.ComputeHash(Encoding.UTF8.GetBytes(inputString));
    var hash = BitConverter.ToString(bytes).Replace("-", "").ToLower();
    Console.WriteLine($"HashCheck: {hash}");
}
```

**PHP**

```php
<?php
function generateRequestHash()
{
    $siteCode = "YOUR_SITE_CODE";
    $countryCode = "ZA";
    $currencyCode = "ZAR";
    $amount = number_format(100.00, 2, ".", "");
    $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";
    $privateKey = "YOUR_PRIVATE_KEY";
    $isTest = "false";

    $inputString = strtolower(
        $siteCode .
            $countryCode .
            $currencyCode .
            $amount .
            $transactionReference .
            $bankReference .
            $cancelUrl .
            $errorUrl .
            $successUrl .
            $notifyUrl .
            $isTest .
            $privateKey,
    );

    echo "HashCheck: " . hash("sha512", $inputString) . "\n";
}
generateRequestHash();
?>
```

**JavaScript**

```javascript
const crypto = require("crypto");

function generateRequestHash() {
  const siteCode = "YOUR_SITE_CODE";
  const countryCode = "ZA";
  const currencyCode = "ZAR";
  const amount = (100.00).toFixed(2);
  const transactionReference = "ORDER-001";
  const bankReference = "ABC123";
  const cancelUrl = "https://yourstore.com/cancel";
  const errorUrl = "https://yourstore.com/error";
  const successUrl = "https://yourstore.com/success";
  const notifyUrl = "https://yourstore.com/notify";
  const privateKey = "YOUR_PRIVATE_KEY";
  const isTest = false;

  const inputString =
    `${siteCode}${countryCode}${currencyCode}${amount}${transactionReference}${bankReference}${cancelUrl}${errorUrl}${successUrl}${notifyUrl}${isTest}${privateKey}`.toLowerCase();

  const hash = crypto.createHash("sha512").update(inputString).digest("hex");
  console.log(`HashCheck: ${hash}`);
}
generateRequestHash();
```

**Python**

```python
import hashlib

def generate_request_hash():
    site_code = "YOUR_SITE_CODE"
    country_code = "ZA"
    currency_code = "ZAR"
    amount = f"{100.00:.2f}"
    transaction_reference = "ORDER-001"
    bank_reference = "ABC123"
    cancel_url = "https://yourstore.com/cancel"
    error_url = "https://yourstore.com/error"
    success_url = "https://yourstore.com/success"
    notify_url = "https://yourstore.com/notify"
    private_key = "YOUR_PRIVATE_KEY"
    is_test = False

    input_string = (
        str(site_code)
        + str(country_code)
        + str(currency_code)
        + amount
        + str(transaction_reference)
        + str(bank_reference)
        + str(cancel_url)
        + str(error_url)
        + str(success_url)
        + str(notify_url)
        + str(is_test)
        + str(private_key)
    ).lower()

    hash_result = hashlib.sha512(input_string.encode()).hexdigest()
    print(f"HashCheck: {hash_result}")

generate_request_hash()
```

**Complete field concatenation order**

Only include fields that have a value. Empty or unused fields must be excluded.

| Position | Field | Required |
|---|---|---|
| 1 | `siteCode` | Yes |
| 2 | `countryCode` | Yes |
| 3 | `currencyCode` | Yes |
| 4 | `amount` | Yes |
| 5 | `transactionReference` | Yes |
| 6 | `bankReference` | Yes |
| 7 | `optional1` | No |
| 8 | `optional2` | No |
| 9 | `optional3` | No |
| 10 | `optional4` | No |
| 11 | `optional5` | No |
| 12 | `customer` | No |
| 13 | `cancelUrl` | No |
| 14 | `errorUrl` | No |
| 15 | `successUrl` | No |
| 16 | `notifyUrl` | No |
| 17 | `isTest` | Yes |
| 18 | `selectedBankId` | No |
| 19 | `bankAccountNumber` | No |
| 20 | `branchCode` | No |
| 21 | `bankAccountName` | No |
| 22 | `payeeDisplayName` | No |
| 23 | `expiryDateUtc` | No |
| 24 | `allowVariableAmount` | No |
| 25 | `variableAmountMin` | No |
| 26 | `variableAmountMax` | No |
| 27 | `customerIdentifier` | No |
| 28 | `customerCellphoneNumber` | No |
| 29 | `hashCheck` | Yes: do not include in hash |

---

### Step 2: Create a payment request

Post the payment request to the Ozow API to generate a payment URL.

```endpoint
POST https://api.ozow.com/postpaymentrequest
ApiKey: YOUR_API_KEY
Content-Type: application/json
Accept: application/json
```

**cURL**

```bash
curl -X POST "https://api.ozow.com/postpaymentrequest" \
  -H "Accept: application/json" \
  -H "ApiKey: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "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"
  }'
```

**C#**

```csharp
var client = new RestClient("https://api.ozow.com/postpaymentrequest");
var request = new RestRequest(Method.POST);
request.AddHeader("Accept", "application/json");
request.AddHeader("ApiKey", "YOUR_API_KEY");
request.AddHeader("Content-Type", "application/json");

var data = new
{
    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",
};

request.AddParameter(
    "application/json",
    JsonConvert.SerializeObject(data),
    ParameterType.RequestBody
);
IRestResponse response = client.Execute(request);
Console.WriteLine(response.Content);
```

**PHP**

```php
<?php
$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_URL => "https://api.ozow.com/postpaymentrequest",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode([
        "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",
    ]),
    CURLOPT_HTTPHEADER => [
        "Accept: application/json",
        "ApiKey: YOUR_API_KEY",
        "Content-Type: application/json",
    ],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
?>
```

**JavaScript**

```javascript
const data = {
  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",
};

fetch("https://api.ozow.com/postpaymentrequest", {
  method: "POST",
  headers: {
    "Accept": "application/json",
    "ApiKey": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
})
  .then((response) => response.text())
  .then((data) => console.log(data));
```

**Python**

```python
import requests
import json

data = {
    "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",
}

response = requests.post(
    "https://api.ozow.com/postpaymentrequest",
    headers={
        "Accept": "application/json",
        "ApiKey": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json=data,
)
print(response.text)
```

**Key request fields**

| Field | Type | Required | Description |
|---|---|---|---|
| `siteCode` | string | Yes | Your Ozow site code |
| `countryCode` | string | Yes | Must be `ZA` |
| `currencyCode` | string | Yes | Must be `ZAR` |
| `amount` | string | Yes | Payment amount |
| `transactionReference` | string | Yes | Your internal order reference |
| `bankReference` | string | Yes | The reference that appears on your bank statement for the payment |
| `cancelUrl` | string | Yes | URL to redirect the customer to if they cancel |
| `errorUrl` | string | Yes | URL to redirect the customer to if an error occurs |
| `successUrl` | string | Yes | URL to redirect the customer to on successful payment |
| `notifyUrl` | string | Yes | URL Ozow posts the notification response to |
| `isTest` | boolean | Yes | Set to `false` for live payments |
| `hashCheck` | string | Yes | The SHA512 hash generated in Step 1 |

For the full list of request fields see [Post Payment Request](https://hub.ozow.com/api-reference/payments-api/post-post-payment-request.md).

**Successful response**

```json
{
  "paymentRequestId": "00000000-0000-0000-0000-000000000000",
  "url": "https://pay.ozow.com/00000000-0000-0000-0000-000000000000/Secure",
  "errorMessage": null
}
```

> ⚠️ **Important**: A rejected request is also an HTTP 200. A failed hash check, a bank
> reference that is too long, or a site code that is not active come back with status 200,
> a null `url`, and the reason in `errorMessage`. Treat the request as accepted only when
> `url` is populated and `errorMessage` is null.

**Rejected response**

```json
{
  "paymentRequestId": null,
  "url": null,
  "errorMessage": "The HashCheck value has failed"
}
```

---

### Step 3: Redirect the customer

Redirect your customer's browser to the `url` returned in the response. Ozow displays the payment
page where the customer selects their preferred payment method and completes the payment.

Once the customer completes, cancels, or encounters an error, Ozow redirects them to the applicable
URL you specified in the request; `successUrl`, `cancelUrl`, or `errorUrl`.

> ⚠️ **Important**: Do not use the redirect to `successUrl` alone as confirmation that a payment was
> successful. Always confirm payment status via the verified notification response or an API status
> check.

> ℹ️ **Note**: Pay by Bank is available by default. Additional payment methods such as Capitec Pay,
> Buy Now Pay Later, and PayShap Request are enabled by Ozow on request. Once activated, they appear
> automatically on the payment page with no additional integration work required.

---

### Step 4: Handle the notification response

Ozow posts a notification to your `notifyUrl` when a transaction completes. The notification is sent
form-encoded with mime-type `application/x-www-form-urlencoded`.

**Example notification**

```text
SiteCode=TSTSTE0001&TransactionId=c02dc1c9-a117-45cf-b375-d304cf434a52&TransactionReference=ORDER-001&Amount=100.00&Status=Complete&CurrencyCode=ZAR&IsTest=False&StatusMessage=&Hash=11fa4b11...&SubStatus=Unclassified
```

**Verifying the notification hash**

You must verify every notification before acting on it:

1. Concatenate the fields below in this order, skipping any that are empty
2. Append your private key to the concatenated string
3. Convert the entire string to lowercase
4. Generate a SHA512 hash of the lowercase string
5. Compare your generated hash to the `Hash` value in the notification

**Notification hash field order**

| Position | Field |
|---|---|
| 1 | `SiteCode` |
| 2 | `TransactionId` |
| 3 | `TransactionReference` |
| 4 | `Amount`, with two decimal places |
| 5 | `Status` |
| 6 | `Optional1` |
| 7 | `Optional2` |
| 8 | `Optional3` |
| 9 | `Optional4` |
| 10 | `Optional5` |
| 11 | `CurrencyCode` |
| 12 | `IsTest` |
| 13 | `StatusMessage` |
| 14 | Your private key |

> ⚠️ **Important**: This is not the same order as the request hash, and it is not the order the
> fields appear in the notification body. `CurrencyCode` comes after the five optional fields here,
> and `SubStatus` and `Hash` are not part of the hash at all. Concatenate in the order above, not in
> the order you read them off the request.

> ⚠️ **Important**: Never update an order status without first verifying the notification hash. Log
> and alert on verification failures: do not silently discard them.

---

### Step 5: Confirm the transaction outcome

After verifying the notification hash, check the transaction status and update your order.

```mermaid
flowchart LR
    A[Receive notification] --> B{Verify hash}
    B -->|Invalid| C[Log and alert - do not process]
    B -->|Valid| D{Check Status field}
    D -->|Complete| E[Credit order and fulfil]
    D -->|Cancelled| F[Return customer to checkout]
    D -->|Error| G[Do not credit - check SubStatus]
```

**Transaction statuses**

| Status | Description |
|---|---|
| `Complete` | Payment completed successfully |
| `Cancelled` | Customer cancelled the payment |
| `Error` | An error occurred: check `SubStatus` for detail |
| `PendingInvestigation` | Payment is under review: do not credit until resolved |

**SubStatus values**

When a payment fails, the `SubStatus` field provides more detail. See [Transaction and settlement
statuses](https://hub.ozow.com/integration-methods/statuses.md) for the full SubStatus list.

You can also confirm transaction status directly via the API at any time:

**By transaction reference**

```endpoint
GET https://api.ozow.com/GetTransactionByReference?siteCode={siteCode}&transactionReference={transactionReference}
ApiKey: YOUR_API_KEY
```

**By transaction ID**

```endpoint
GET https://api.ozow.com/GetTransaction?siteCode={siteCode}&transactionId={transactionId}
ApiKey: YOUR_API_KEY
```

> ℹ️ **Note**: Handle duplicate notifications idempotently. Ozow may send the same notification more
> than once. Processing the same notification twice must not result in double-crediting an order.

---

### Step 6: Cancel a payment

The Payments API does not support programmatic cancellation of a payment request via API. A payment
can only be cancelled by the customer on the Ozow payment page. If the customer cancels, Ozow
redirects them to your `cancelUrl`.

---

## Optional features

### Standalone button

A standalone button lets you surface a specific Ozow payment method as a dedicated button on your
checkout page, taking the customer directly to that payment method.

> ⚠️ **Important**: Do not display a standalone button for a payment method until you have received
> confirmation from Ozow that your account has been enabled for that payment method.

> ℹ️ **Digital wallets**: Apple Pay and Google Pay cannot be offered as standalone buttons using
> this method. If you want to offer these as dedicated payment options at checkout, use the [Wallet
> SDK](https://hub.ozow.com/integration-methods/apis/payin/embedded-wallet.md).

**Implementation**

Include the `SelectedBankId` field in your payment request. The `SelectedBankId` for each payment
method is available on the relevant [Payment products](https://hub.ozow.com/payment-products.md) page.

> ℹ️ **Hash field order**: `selectedBankId` is field 18 in the concatenation order. Add it between
> `isTest` (field 17) and `hashCheck` (field 29) in the concatenated string. Only include it if you
> are using it.

```json
{
  "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,
  "selectedBankId": "YOUR_BANK_ID",
  "hashCheck": "YOUR_GENERATED_HASH"
}
```

---

### Customer Identity Verification

If your business operates in a high-risk industry, you are required to implement Customer Identity
Verification before going live with Bank API payment methods.

To implement it in Payments API, pass the verified South African ID number or foreign passport
number in the `customerIdentifier` field of the payment request:

```json
{
  "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,
  "customerIdentifier": "0000000000000",
  "hashCheck": "YOUR_GENERATED_HASH"
}
```

> ℹ️ **Hash field order**: `customerIdentifier` is field 27 in the concatenation order. Add it
> between `variableAmountMax` (field 26) and `customerCellphoneNumber` (field 28) in the
> concatenated string. If you are not using the fields between `isTest` and `customerIdentifier`,
> skip them; only include fields that have a value.

For full details on Customer Identity Verification requirements see [Customer Identity Verification](https://hub.ozow.com/integration-methods/apis/payin/identity-verification.md).

---

## Next steps

- Review the [Building a secure
  integration](https://hub.ozow.com/getting-started/building-a-secure-integration.md) checklist before going
  live
- Test your integration using [Payin test cases](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/payin-test-cases-payments-api.md)
- Switch your base URL from staging to production when you are ready to go live
- See the [Payments API reference](https://hub.ozow.com/api-reference/payments-api.md) for the full technical specification

---

# Redirect to Ozow

> Build a redirect payin with One API. Create a payment request, send the customer to Ozow's hosted page, and confirm the result from the webhook.

Source: https://hub.ozow.com/integration-methods/apis/payin/redirect-to-ozow/

This guide walks you through a redirect payin integration using One API. Your system creates a
payment request, redirects the customer to Ozow's secure hosted payment page, and receives a webhook
notification when the payment is complete.

> ℹ️ This guide uses **One API**: Ozow's recommended API for all new integrations. It uses OAuth 2.0
> for authentication, and new payment methods and features are released here first.
>
> Already integrated? If your integration posts to `api.ozow.com` and builds a SHA512 hash, you're
> on the Payments API: see [Redirect to Ozow](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/redirect-to-ozow.md) under
> Legacy integrations, or [Migrating to One API](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/migrating-to-one-api.md).

## Before you start

- You have completed [Prerequisites and onboarding](https://hub.ozow.com/getting-started/prerequisites-and-onboarding.md)
- You have a Client ID and Client Secret from the One API Clients section of your [Ozow Dashboard](https://dash.ozow.com/MerchantAdmin/OneAPI/Clients)
- You have your site code from the Site section of your Dashboard
- Your webhook endpoint is set up and publicly accessible via HTTPS

> ℹ️ **Note**: Only users with administrator privileges in the Ozow Dashboard can access the One API
> Clients section.

## Environments

| Environment | Token endpoint | API base URL | Dashboard |
|---|---|---|---|
| Production | `https://one.ozow.com/v1/token` | `https://one.ozow.com/v1` | [dash.ozow.com](https://dash.ozow.com) |
| Staging | `https://stagingone.ozow.com/v1/token` | `https://stagingone.ozow.com/v1` | [stagingdash.ozow.com](https://stagingdash.ozow.com) |

## How redirect works

```mermaid
sequenceDiagram
    participant C as Customer
    participant M as Your system
    participant O as One API
    participant P as Ozow payment page

    C->>M: Reaches checkout
    M->>O: POST /v1/payments
    O-->>M: Returns redirectUrl
    M->>C: Redirects customer to redirectUrl
    C->>P: Completes payment
    O-->>M: Sends webhook notification
    M->>M: Verifies webhook signature
    M-->>C: Updates order and redirects to returnUrl
```

---

## Core integration

### Step 1: Obtain an access token

One API uses OAuth 2.0 Client Credentials authentication. You need an access token before making any
API calls.

```endpoint
POST https://one.ozow.com/v1/token
Content-Type: application/x-www-form-urlencoded
```

**cURL**

```bash
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"
```

**C#**

```csharp
var client = new HttpClient();
var content = new FormUrlEncodedContent(
    new[]
    {
        new KeyValuePair<string, string>("client_id", "YOUR_CLIENT_ID"),
        new KeyValuePair<string, string>("client_secret", "YOUR_CLIENT_SECRET"),
        new KeyValuePair<string, string>("scope", "payments"),
        new KeyValuePair<string, string>("grant_type", "client_credentials"),
    }
);
var response = await client.PostAsync("https://one.ozow.com/v1/token", content);
var result = await response.Content.ReadAsStringAsync();
```

**PHP**

```php
<?php
$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_URL => "https://one.ozow.com/v1/token",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        "client_id" => "YOUR_CLIENT_ID",
        "client_secret" => "YOUR_CLIENT_SECRET",
        "scope" => "payments",
        "grant_type" => "client_credentials",
    ]),
    CURLOPT_HTTPHEADER => ["Content-Type: application/x-www-form-urlencoded"],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
?>
```

**JavaScript**

```javascript
const response = await fetch("https://one.ozow.com/v1/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    client_id: "YOUR_CLIENT_ID",
    client_secret: "YOUR_CLIENT_SECRET",
    scope: "payments",
    grant_type: "client_credentials",
  }),
});
const data = await response.json();
```

**Python**

```python
import requests

response = requests.post(
    "https://one.ozow.com/v1/token",
    data={
        "client_id": "YOUR_CLIENT_ID",
        "client_secret": "YOUR_CLIENT_SECRET",
        "scope": "payments",
        "grant_type": "client_credentials",
    },
)
data = response.json()
```

**Successful response**

```json
{
  "access_token": "mF_9.B5f-4.1JqM",
  "token_type": "Bearer",
  "expires_in": "14400",
  "scope": "payments"
}
```

Store the `access_token` and include it in the `Authorization` header of all subsequent requests:

```http
Authorization: Bearer YOUR_ACCESS_TOKEN
```

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. If authentication fails, the API returns
`401 Unauthorized`.

---

### Step 2: Create a payment request

Create a payment request for your customer's order.

```endpoint
POST https://one.ozow.com/v1/payments
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
```

**cURL**

```bash
curl -X POST "https://one.ozow.com/v1/payments" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "siteCode": "YOUR_SITE_CODE",
    "amount": {
      "currency": "ZAR",
      "value": 100.00
    },
    "merchantReference": "ORDER-001",
    "beneficiaryReference": "ONLINESHOP002",
    "expireAt": "2026-12-31T23:59:59Z",
    "returnUrl": "https://yourstore.com/order-complete"
  }'
```

**C#**

```csharp
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer",
    "YOUR_ACCESS_TOKEN"
);

var payload = new
{
    siteCode = "YOUR_SITE_CODE",
    amount = new { currency = "ZAR", value = 100.00 },
    merchantReference = "ORDER-001",
    beneficiaryReference = "ONLINESHOP002",
    expireAt = "2026-12-31T23:59:59Z",
    returnUrl = "https://yourstore.com/order-complete",
};

var response = await client.PostAsync(
    "https://one.ozow.com/v1/payments",
    new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
);
var result = await response.Content.ReadAsStringAsync();
```

**PHP**

```php
<?php
$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_URL => "https://one.ozow.com/v1/payments",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode([
        "siteCode" => "YOUR_SITE_CODE",
        "amount" => ["currency" => "ZAR", "value" => 100.00],
        "merchantReference" => "ORDER-001",
        "beneficiaryReference" => "ONLINESHOP002",
        "expireAt" => "2026-12-31T23:59:59Z",
        "returnUrl" => "https://yourstore.com/order-complete",
    ]),
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer YOUR_ACCESS_TOKEN",
        "Content-Type: application/json",
    ],
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;
?>
```

**JavaScript**

```javascript
const response = await fetch("https://one.ozow.com/v1/payments", {
  method: "POST",
  headers: {
    "Authorization": "Bearer YOUR_ACCESS_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    siteCode: "YOUR_SITE_CODE",
    amount: { currency: "ZAR", value: 100.00 },
    merchantReference: "ORDER-001",
    beneficiaryReference: "ONLINESHOP002",
    expireAt: "2026-12-31T23:59:59Z",
    returnUrl: "https://yourstore.com/order-complete",
  }),
});
const data = await response.json();
```

**Python**

```python
import requests

response = requests.post(
    "https://one.ozow.com/v1/payments",
    headers={
        "Authorization": "Bearer YOUR_ACCESS_TOKEN",
        "Content-Type": "application/json",
    },
    json={
        "siteCode": "YOUR_SITE_CODE",
        "amount": {"currency": "ZAR", "value": 100.00},
        "merchantReference": "ORDER-001",
        "beneficiaryReference": "ONLINESHOP002",
        "expireAt": "2026-12-31T23:59:59Z",
        "returnUrl": "https://yourstore.com/order-complete",
    },
)
data = response.json()
```

**Key request fields**

| Field | Type | Required | Description |
|---|---|---|---|
| `siteCode` | string | Yes | Your Ozow site code |
| `amount.currency` | string | Yes | Must be `ZAR` |
| `amount.value` | number | Yes | Payment amount |
| `merchantReference` | string | Yes | Your internal order reference |
| `beneficiaryReference` | string | Usually | The reference that appears on your bank statement for the payment. Letters and numbers only |
| `expireAt` | string | Yes | Payment request expiry in RFC 3339 format |
| `returnUrl` | string | Yes | URL to redirect the customer to after payment |

> ⚠️ **Important**: Send `beneficiaryReference`. The contract marks it optional because a site can be
> configured either way, and most are configured to require it. Leaving it out of a site that wants
> it answers `400` with `Error occurred creating merchant request (Parameter 'Bank reference
> missing')`, which does not name the field it means.

For the full list of request fields see [Create a payment](https://hub.ozow.com/api-reference/one-api/post-payments.md).

**Successful response**

```json
{
  "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"
}
```

---

### Step 3: Redirect the customer

Redirect your customer's browser to the `redirectUrl` from the response. Ozow displays the payment
page where the customer selects their preferred payment method and completes the payment.

**Send them to the URL you were given, and do not build one.** Its shape is Ozow's to change, and a
URL assembled from the payment id is a URL that stops working without notice.

`status` on the response is `Created`. Compare it without case.

Once the customer completes or cancels the payment, Ozow redirects them back to your `returnUrl`.

> ℹ️ **Note**: Pay by Bank is available by default. Additional payment methods such as Capitec Pay,
> Buy Now Pay Later, and PayShap Request are enabled by Ozow on request. Once activated, they appear
> automatically on the payment page with no additional integration work required.

> ⚠️ **Important**: Do not use the customer's return to your `returnUrl` as confirmation that a
> payment was successful. Always confirm payment status via a verified webhook notification or an
> API status check.

---

### Step 4: Handle the webhook notification

Ozow sends a webhook notification to your endpoint when a transaction completes. One API uses
[Svix](https://www.svix.com/) to deliver webhooks.

**There are two ways to be told, and you choose them independently.**

| | Webhooks | `notifyUrl` |
|---|---|---|
| How you turn it on | The Ozow Dashboard or the webhook endpoints | Set `notifyUrl` on the payment request |
| What arrives | The events below, signed with Svix headers | The same notification the Payments API sends on a payin, with the same payload and the same hash |
| When it fires | Every transaction | Every payment where you set `notifyUrl` |

Webhooks are the recommended path and the rest of this step covers them. `notifyUrl` is optional
and exists so that an integration already handling the Payments API notification keeps working:
point it at your existing handler and the payload and hash are the ones it already verifies.

> ⚠️ **Important**: If you configure both, **both fire for the same payment**. Your handlers must
> be idempotent, keyed on the merchant reference or the transaction ID, or one payment marks an
> order paid twice.

If you set neither, nothing is delivered and you must poll
[Get Transactions](https://hub.ozow.com/api-reference/one-api/get-payments-id-transactions.md) instead, which is slower and
which Step 5 covers.

**Setting up your webhook endpoint**

Set up your webhook endpoint in one of two ways:

- **Via the Ozow Dashboard**: navigate to [One API
  Clients](https://dash.ozow.com/MerchantAdmin/OneAPI/Clients), select your client, and manage
  webhooks from there
- **Via the API**: [List Webhook Subscriptions](https://hub.ozow.com/api-reference/one-api/get-webhooks.md) to see what you
  already have, then [Create Webhook Subscription](https://hub.ozow.com/api-reference/one-api/post-webhooks.md) for what you do
  not

> ⚠️ **Important**: List before you create. Creating a subscription that duplicates one you already
> have delivers every event twice, and the second delivery is indistinguishable from the first, so
> a handler that is not idempotent processes the same payment again. A deploy script that creates a
> subscription on every run is the usual way this happens.

**Available webhook events**

| Event | Description |
|---|---|
| [`transaction.complete`](https://hub.ozow.com/api-reference/one-api/webhooks/transaction-complete.md) | A transaction has completed: check the `status` field to determine if it was successful or resulted in an error |
| [`refund.complete`](https://hub.ozow.com/api-reference/one-api/webhooks/refund-complete.md) | A refund has completed: check the `status` field for the refund result |
| [`subscription.authorization.success`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-authorization-success.md) | A customer approved a subscription consent ⚠️ Beta, subject to change |
| [`subscription.authorization.failed`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-authorization-failed.md) | A consent was not approved ⚠️ Beta, subject to change |
| [`subscription.transaction.success`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-transaction-success.md) | An individual collection succeeded ⚠️ Beta, subject to change |
| [`subscription.transaction.failed`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-transaction-failed.md) | An individual collection failed ⚠️ Beta, subject to change |
| [`subscription.completed`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-completed.md) | A subscription took all its scheduled occurrences ⚠️ Beta, subject to change |
| [`subscription.canceled`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-canceled.md) | A subscription was cancelled ⚠️ Beta, subject to change. One `l`, unlike `Cancelled` elsewhere |
| [`subscription.expired`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-expired.md) | An authorisation lapsed before the subscription became active ⚠️ Beta, subject to change |

Subscribe to the name exactly as written. An event name the service does not
know is rejected, so a subscription to something close is a subscription that
never fires.

**Message types**

When setting up your webhook subscription you can choose how much data is included in each notification:

| Message type | What it includes |
|---|---|
| `thin` | `id`, `status` and `reason` |
| `full` | The transaction's fields, in the same shape the Payments API notification uses |

Choose `full` if you need transaction details in the webhook payload. Choose `thin` if you only need
the status and will query the API for details separately.

> ⚠️ **Important**: `full` is implemented for `transaction.complete` and `refund.complete` only. A
> subscription event registered as `full` delivers nothing.

**What arrives**

Every delivery has the same [envelope](https://hub.ozow.com/api-reference/one-api/schemas/webhook-envelope.md). `data` is
[`WebhookEventData`](https://hub.ozow.com/api-reference/one-api/schemas/webhook-event-data.md) when the subscription asked for `thin`,
[`TransactionCompleteFullData`](https://hub.ozow.com/api-reference/one-api/schemas/transaction-complete-full-data.md) for a `full`
subscription to `transaction.complete`, and
[`RefundCompleteFullData`](https://hub.ozow.com/api-reference/one-api/schemas/refund-complete-full-data.md) for a `full` subscription
to `refund.complete`. Every value in a `full` payload is a string, the amount and the flags
included.

```json
{
  "type": "transaction.complete",
  "timestamp": "2026-03-14T09:30:00Z",
  "data": {
    "id": "00000000-0000-0000-0000-000000000000",
    "status": "Successful",
    "reason": null
  }
}
```

`id` is the transaction, refund or subscription the event is about. `reason` carries the status
message and is null when there is nothing to say.

**The `status` values for `transaction.complete`**

These are not the transaction statuses on the
[statuses page](https://hub.ozow.com/integration-methods/statuses.md): the webhook maps them down to four.

| `status` | Sent when the transaction is |
|---|---|
| `Successful` | `Complete` |
| `Incomplete` | `Created` |
| `Pending` | `Pending` or `PendingInvestigation` |
| `Error` | anything else, including `Cancelled`, `Abandoned`, `Voided` and `Unknown` |

**A cancelled payment arrives as `Error`, not as `Cancelled`.** `reason` tells you which it was.
Switch on these four and read `reason` for the detail; do not expect the status names the
transaction itself carries.

For `refund.complete`, `status` is `Pending`, `Failed`, `Complete`, `Submitted`, `Cancelled`,
`Returned` or `Invalid`. Those are the `thin` values. A `full` subscription carries the refund's own
status in `Status`, unmapped, so `PendingInvestigation` and `Error` arrive as themselves rather than
as `Pending` and `Failed`. Handle both if you are on `full`.

**Verifying the webhook signature**

Every webhook notification includes Svix signature headers. You must verify the signature before
acting on any notification.

| Header | Description |
|---|---|
| `svix-id` | Unique message identifier, the same if the webhook is resent after a failure |
| `svix-timestamp` | Timestamp in seconds since epoch |
| `svix-signature` | Base64 encoded signature |

[Verify a webhook signature](https://hub.ozow.com/integration-methods/apis/payin/verify-a-webhook.md) has the five steps of the
check and a working verifier in `csharp`, `php`, `python` and `javascript`, with or
without the Svix library. To retrieve the secret for your webhook, use
[Get Webhook Secret](https://hub.ozow.com/api-reference/one-api/get-webhooks-id-secret.md).

> ⚠️ **Important**: Never process a webhook notification without first verifying its signature. Log
> and alert on verification failures: do not silently discard them.

---

### Step 5: Confirm the transaction outcome

After verifying the webhook, check the transaction status and update your order.

```mermaid
flowchart LR
    A[Receive webhook] --> B{Verify signature}
    B -->|Invalid| C[Log and alert - do not process]
    B -->|Valid| D{Check transaction status}
    D -->|Complete| E[Credit order and fulfil]
    D -->|Cancelled| F[Return customer to checkout]
    D -->|Error| G[Do not credit - investigate]
```

You can also check transaction status directly via the API at any time:

```endpoint
GET https://one.ozow.com/v1/payments/{id}/transactions
Authorization: Bearer YOUR_ACCESS_TOKEN
```

Replace `{id}` with the payment ID returned in Step 2.

> ℹ️ **Note**: Handle duplicate webhook notifications idempotently. Ozow may send the same
> notification more than once. Processing the same notification twice must not result in
> double-crediting an order.

---

### Step 6: Cancel a payment

If your customer abandons checkout or you need to cancel an order before the customer completes
payment, you can cancel the payment request using the cancel link returned in the payment response.

```endpoint
POST https://one.ozow.com/v1/payments/{id}/cancel
Authorization: Bearer YOUR_ACCESS_TOKEN
```

A successfully cancelled payment will no longer be accessible to the customer via the `redirectUrl`.
If the customer attempts to use the link after cancellation they will see an error.

> ⚠️ **Important**: You can only cancel a payment that has not yet been completed. Do not attempt to
> cancel a payment with a `complete` status: use the refunds flow instead. See the [One API
> reference](https://hub.ozow.com/api-reference/one-api.md) for details.

---

## Optional features

### Standalone button

A standalone button lets you surface a specific Ozow payment method as a dedicated button on your
checkout page. Instead of showing a generic payment page where the customer selects their payment
method, a standalone button takes the customer directly to a specific payment method; for example
Capitec Pay, Buy Now Pay Later, or PayShap.

Adding a standalone button to your checkout removes an extra step for the customer, increases
awareness of specific payment methods, and can improve conversion rates.

> ⚠️ **Important**: Do not display a standalone button for a payment method until you have received
> confirmation from Ozow that your account has been enabled for that payment method.

> ℹ️ **Digital wallets**: Apple Pay and Google Pay cannot be offered as standalone buttons using
> this method. If you want to offer these as dedicated payment options at checkout, use the [Wallet
> SDK](https://hub.ozow.com/integration-methods/apis/payin/embedded-wallet.md).

**Implementation**

Include the `institutionId` field in your payment request. When a valid `institutionId` is included,
the customer is taken directly to that payment method. The `institutionId` for each payment method
is available on the relevant [Payment products](https://hub.ozow.com/payment-products.md) page.

```json
{
  "siteCode": "YOUR_SITE_CODE",
  "region": "ZA",
  "amount": {
    "currency": "ZAR",
    "value": 100.00
  },
  "merchantReference": "ORDER-001",
  "expireAt": "2026-12-31T23:59:59Z",
  "returnUrl": "https://yourstore.com/order-complete",
  "institutionId": "YOUR_INSTITUTION_ID"
}
```

---

### Customer Identity Verification

If your business operates in a high-risk industry, you are required to implement Customer Identity
Verification before going live with Bank API payment methods.

To implement it in One API, pass the `payer.identity` object in the payment request:

```json
{
  "siteCode": "YOUR_SITE_CODE",
  "amount": { "currency": "ZAR", "value": 100.00 },
  "merchantReference": "ORDER-001",
  "expireAt": "2026-12-31T23:59:59Z",
  "returnUrl": "https://yourstore.com/order-complete",
  "payer": {
    "id": "CUSTOMER-123",
    "name": "Firstname Lastname",
    "identity": {
      "type": "said",
      "country": "ZA",
      "identifier": "0000000000000"
    }
  }
}
```

**Identity fields**

| Field | Type | Description |
|---|---|---|
| `payer.identity.type` | string | `said` for South African ID, `passport` for foreign passport |
| `payer.identity.country` | string | ISO 3166 Alpha-2 country code, `ZA` for South Africa |
| `payer.identity.identifier` | string | The verified ID or passport number |

For full details on Customer Identity Verification requirements see [Customer Identity Verification](https://hub.ozow.com/integration-methods/apis/payin/identity-verification.md).

---

## Next steps

- Review the [Building a secure
  integration](https://hub.ozow.com/getting-started/building-a-secure-integration.md) checklist before going
  live
- Test your integration using [Payin test cases](https://hub.ozow.com/integration-methods/testing/payin-test-cases-one-api.md)
- Switch your base URL from staging to production when you are ready to go live
- See the [One API reference](https://hub.ozow.com/api-reference/one-api.md) for the full technical specification

---

# Verify a webhook signature

> The signature on a One API webhook, the five steps that check it, and a working implementation in four languages.

Source: https://hub.ozow.com/integration-methods/apis/payin/verify-a-webhook/

Your webhook URL is public. Anyone who finds it can post a `transaction.complete` to it claiming a
payment succeeded, and the only thing separating that from a real delivery is the signature. Check
it before you read a single field of the body.

This page covers the **One API** webhook signature. The Payments API notification is authenticated
differently, with a `Hash` field over the payload: see the
[hash calculator](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/hash-calculator.md) for that one.

> ⚠️ **Important**: Verify against the exact bytes you received. The signature covers the body
> byte for byte, and a framework that parses the request as JSON and hands you the object has
> already thrown those bytes away: serialising it back reorders keys and changes whitespace, and
> nothing matches. Reach for the raw body explicitly. In Express that is `express.raw()`, in
> ASP.NET Core `EnableBuffering` and reading the stream yourself, in Flask `request.get_data()`,
> in Laravel `$request->getContent()`.

## Use the library where you can

Ozow delivers webhooks through [Svix](https://www.svix.com/), and Svix publishes a verification
library for most languages. It gets the constant-time comparison, the replay window and the
signature list right, and it is the shortest path to a correct handler.

```javascript
// svix 2.x. `verify` throws on a bad signature and returns nothing.
import { Webhook } from "svix";

const webhook = new Webhook(process.env.OZOW_WEBHOOK_SECRET);

webhook.verify(rawBody, {
  "svix-id": headers["svix-id"],
  "svix-timestamp": headers["svix-timestamp"],
  "svix-signature": headers["svix-signature"],
});
const event = JSON.parse(rawBody);
```

> ⚠️ **Pin the major version, and read its signature before you upgrade**: `verify` returned the
> parsed body on `svix` 1.x and returns nothing on 2.x. A handler that keeps `const event =
> webhook.verify(...)` across that upgrade reads `undefined`, fails after it has already replied
> `200`, and looks from our side like a delivery that succeeded.

The rest of this page is what that library does, for when you would rather not add one.

## The algorithm

Every delivery carries three headers:

| Header | What it holds |
|---|---|
| `svix-id` | The message identifier, unchanged across retries of the same event |
| `svix-timestamp` | When the delivery was signed, in seconds since the epoch |
| `svix-signature` | One or more signatures, space separated, each written `v1,<base64>` |

Five steps, and all five are load bearing:

1. **Require all three headers.** A delivery missing any of them is not one of ours.
2. **Check the timestamp is within five minutes of now**, in either direction. Without this, a
   signature captured once stays valid forever and a recorded delivery can be replayed at will.
3. **Turn the secret into key bytes.** The secret from
   [Get Webhook Secret](https://hub.ozow.com/api-reference/one-api/get-webhooks-id-secret.md) arrives as `whsec_` followed by
   Base64. Strip the prefix and Base64-decode the rest. The key is those raw bytes, not the string.
4. **Build the signed string and take its HMAC.** The string is the message id, the timestamp and
   the raw body joined by full stops: `{svix-id}.{svix-timestamp}.{body}`. HMAC-SHA256 it with the
   key bytes and Base64-encode the digest.
5. **Compare against every `v1` signature in the header**, with a constant-time comparison. The
   header can carry more than one while a secret is being rotated, and a match on any of them is a
   pass. Ignore any entry whose version is not `v1`.

## A verifier

Each of these returns true only if the delivery is genuine, current and intact. None of them needs
a dependency beyond the standard library.

**C#**

```csharp
using System.Security.Cryptography;
using System.Text;

const int ToleranceSeconds = 300;

static bool IsFromOzow(
    string secret,
    string svixId,
    string svixTimestamp,
    string svixSignature,
    string rawBody
)
{
    if (!long.TryParse(svixTimestamp, out var sent))
        return false;
    if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - sent) > ToleranceSeconds)
        return false;

    var key = Convert.FromBase64String(
        secret.StartsWith("whsec_", StringComparison.Ordinal) ? secret["whsec_".Length..] : secret
    );

    using var hmac = new HMACSHA256(key);
    var digest = hmac.ComputeHash(Encoding.UTF8.GetBytes($"{svixId}.{sent}.{rawBody}"));
    var expected = Encoding.UTF8.GetBytes(Convert.ToBase64String(digest));

    foreach (var candidate in svixSignature.Split(' '))
    {
        var parts = candidate.Split(',', 2);
        if (parts.Length != 2 || parts[0] != "v1")
            continue;
        // Returns false on a length mismatch rather than throwing, and takes the
        // same time whether the first byte differs or the last one does.
        if (CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(parts[1]), expected))
            return true;
    }

    return false;
}
```

**PHP**

```php
<?php

const TOLERANCE_SECONDS = 300;

function isFromOzow(
    string $secret,
    string $svixId,
    string $svixTimestamp,
    string $svixSignature,
    string $rawBody,
): bool {
    if (!ctype_digit($svixTimestamp)) {
        return false;
    }

    $sent = (int) $svixTimestamp;
    if (abs(time() - $sent) > TOLERANCE_SECONDS) {
        return false;
    }

    $key = base64_decode(
        str_starts_with($secret, "whsec_") ? substr($secret, 6) : $secret,
        true,
    );
    if ($key === false) {
        return false;
    }

    $expected = base64_encode(
        hash_hmac("sha256", "{$svixId}.{$sent}.{$rawBody}", $key, true),
    );

    foreach (explode(" ", $svixSignature) as $candidate) {
        [$version, $signature] = array_pad(
            explode(",", $candidate, 2),
            2,
            null,
        );
        if ($version !== "v1" || $signature === null) {
            continue;
        }
        // hash_equals, never ===. A plain comparison returns early on the first
        // differing byte and leaks how much of a guess was right.
        if (hash_equals($expected, $signature)) {
            return true;
        }
    }

    return false;
}
```

**JavaScript**

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

/** `rawBody` is a Buffer, straight off the request. Never a re-serialised object. */
function isFromOzow(secret, headers, rawBody) {
  const id = headers["svix-id"];
  const timestamp = headers["svix-timestamp"];
  const signature = headers["svix-signature"];
  if (!id || !timestamp || !signature) return false;

  const sent = Number(timestamp);
  if (!Number.isInteger(sent)) return false;
  if (Math.abs(Date.now() / 1000 - sent) > TOLERANCE_SECONDS) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = Buffer.from(
    createHmac("sha256", key)
      .update(`${id}.${sent}.`)
      .update(rawBody)
      .digest("base64"),
  );

  return signature.split(" ").some((candidate) => {
    const [version, value] = candidate.split(",");
    if (version !== "v1" || !value) return false;
    const given = Buffer.from(value);
    // timingSafeEqual throws on a length mismatch, so the lengths are checked first.
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}
```

**Python**

```python
import base64
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300

def is_from_ozow(
    secret: str,
    svix_id: str,
    svix_timestamp: str,
    svix_signature: str,
    raw_body: bytes,
) -> bool:
    try:
        sent = int(svix_timestamp)
    except ValueError:
        return False

    if abs(time.time() - sent) > TOLERANCE_SECONDS:
        return False

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{svix_id}.{sent}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()

    for candidate in svix_signature.split(" "):
        version, _, signature = candidate.partition(",")
        if version != "v1":
            continue
        if hmac.compare_digest(expected, signature):
            return True

    return False
```

## What goes wrong

The first two announce themselves the moment you test. The last three pass every test you are
likely to write and fail in production, which is why they are worth reading twice.

| Mistake | What happens |
|---|---|
| **The secret used as a string** | The key is the Base64-decoded bytes after `whsec_`. Using the characters gives a digest that never matches anything. |
| **The body parsed before it is verified** | Re-serialising changes the bytes. Nothing matches, on every delivery. |
| **No timestamp check** | Every signature stays valid forever. One captured delivery can be replayed for as long as the secret lives. |
| **`==` instead of a constant-time compare** | The comparison returns as soon as two bytes differ, and how long it took says how much of a guess was right. |
| **Only the first signature checked** | `svix-signature` carries both the old and the new signature while a secret is rotated. A verifier that reads one of them starts rejecting real deliveries mid-rotation. |

## Check your verifier

Start offline. Svix publishes a signature you can check against without sending anything, which
separates a wrong implementation from a wrong endpoint before either can confuse the other:

```text
secret     whsec_plJ3nmyCDGBKInavdOK15jsl
body       {"event_type":"ping","data":{"success":true}}
svix-id    msg_loFOjxBNrRLzqYUf
timestamp  1731705121
signature  v1,rAvfW3dJ/X/qxhsaXPOyyCGmRKsaKWcsNccKXlIktD0=
```

Feed those five values to your verifier and it must produce that signature. The timestamp is from
2024, so a verifier that checks the replay window rejects the delivery even when the signature is
right: check the signature it computed rather than the answer it returned, or hold the clock at
`1731705121` for the test.

Then send yourself a real delivery and confirm all four of these, in this order:

1. **An untouched delivery passes.** Anything else and the rest of the list means nothing.
2. **One changed byte of the body fails.** Change a digit of the amount and replay it.
3. **One changed character of `svix-signature` fails.**
4. **The same delivery replayed an hour later fails.** If it passes, step 2 of the algorithm is
   missing.

A verifier that rejects a delivery must log it and alert. A signature that does not match is either
a bug of ours or somebody probing your endpoint, and both are worth a person looking at them. Never
discard one quietly.

---

# Background

Context for the above. Nothing here is implemented against.

---

# Prerequisites and onboarding

> What to have in place before you write any code: a merchant account, Dashboard access, your credentials, and payout eligibility if you need it.

Source: https://hub.ozow.com/getting-started/prerequisites-and-onboarding/

Before you start integrating Ozow, make sure you have everything in place. This page covers what you
need before writing a single line of code.

## 1. Register as an Ozow merchant

You need an active Ozow merchant account before you can integrate. If you don't have one yet,
[join our merchant family](https://ozow.com/merchants) or speak to your account manager to get set up.
 If you signed up through a commercial manager or already have an account, you can skip this step and
 log in to the [Ozow Dashboard](https://dash.ozow.com) directly.

If you need assistance with your account, contact [support@ozow.com](mailto:support@ozow.com) or
reach out to your account manager.

## 2. Access the Ozow Dashboard

Once your merchant account is active, you can log in to your [Ozow Dashboard](https://dash.ozow.com).
The Dashboard is where you'll find everything you need to begin your integration.

## 3. Retrieve your credentials

Which credentials you need depends on the API you are integrating against. Collect the row for
yours and ignore the rest.

| | One API | Payments API | Payouts API |
|---|---|---|---|
| Client ID and Client Secret | **Yes** | No | No |
| API key | No | Yes | Yes, a **different** key |
| Private key | No | Yes, to sign the hash | Yes, to sign the hash |
| Where to find them | One API Clients | Merchant Details and Site | Issued once payouts are approved |

**On One API, the Client ID and Client Secret are all you need.** You exchange them for an access
token. There is no API key to send and no hash to compute.

**On the Payments API and the Payouts API you need both keys, and they do different things.** The
API key goes in the `ApiKey` header. The private key is never sent: you use it to compute the
`hashCheck` field on the request, and again to verify the hash on a notification.

**The Payouts API takes its own API key, not the one the Payments API takes.** Sending the Payments
API key to a payout endpoint is rejected.

Your **site code** identifies which of your sites a request belongs to and is in the Site section of
the Dashboard. Every path needs one. It is not a secret.

> ℹ️ **One API clients and payout API keys are scoped per site or per merchant.** A key issued for
> one site does not work for another, so check which you have been given before you assume it covers
> your whole account.

> ⚠️ **Security note**: Keep your credentials secure at all times. Never expose them in client-side
> code, public repositories, or logs. Ozow does not publish credentials publicly and will never ask
> you to share them in an unsecured channel.

## 4. Understand your project setup

When you log in to the Ozow Dashboard, you'll see your merchant account. Within your account, you
can have one or more sites; each representing a separate website, merchant, or integration point.

Each site has its own unique site code, and your site code and API credentials work together to
identify which site a payment belongs to. Payment requests and transactions are always tied to a
specific site, so it's important to use the correct site code for the integration you're building.

```mermaid
graph TD
    A[Ozow Dashboard] --> B[Site 1\nsite code: ABC-001]
    A --> C[Site 2\nsite code: ABC-002]
    A --> D[Site 3\nsite code: ABC-003]
    B --> E[Payments & transactions\ntied to Site 1]
    C --> F[Payments & transactions\ntied to Site 2]
    D --> G[Payments & transactions\ntied to Site 3]
```

## 5. Integrating payouts? Check your eligibility first

If you intend to integrate payouts, you must be approved by Ozow's onboarding team before you can
begin. Payout credentials are not issued until this approval is in place; you will not be able to
start a payout integration without them.

Contact your account manager or [support@ozow.com](mailto:support@ozow.com) to request payout eligibility.

> ℹ️ **Note**: Payin credentials are issued automatically as part of standard merchant onboarding.
> Payout credentials require a separate approval process before they are issued.

## 6. Choose your integration path

Once your credentials are in place, you're ready to choose how you'll integrate. Head to
[choose your integration](https://hub.ozow.com/integration-methods.md) to understand your
options and choose the right path for your use case.

If you're new to Ozow and want to get to your first payment as quickly as possible, go straight to
the [quick start guide](https://hub.ozow.com/getting-started/quick-start.md).

---

# Hash calculator

> Work out the hashCheck a request carries, and see the exact string it is computed from.

Source: https://hub.ozow.com/integration-methods/apis/deprecated-integrations/hash-calculator/

Every request that moves money carries a `hashCheck`, and a request whose hash does not match is
rejected. The rejection does not say which field was wrong, so this shows you the string the hash is
computed from, one field at a time.

It works in both directions. **Build it** fills in the fields you send and shows the string they
concatenate to. **Check mine** goes the other way: give it what your own code produced, and it says
where that leaves the documented string and names the mistake that would explain it.

> ⚠️ **Important**: Neither of those needs your private key, and the page asks for one only if you
> choose the last of three options. It builds the string with a placeholder in place of the key,
> which is all you need: a hash is determined entirely by the string it is computed from, so if your
> string matches this one, your hash matches too. Comparing strings finds every mistake except a
> wrong key, because every other mistake changes the string before the key is reached.

> ⚠️ **Before you paste a key anywhere**: check the address bar. This page is the only one on this
> site that will ever ask for one, it holds it in the tab and nowhere else, and it still asks you to
> try the string comparison first. A page imitating this one would ask sooner and explain less.

[Hash calculator](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/hash-calculator/), a tool on this page.

## What usually goes wrong

The key is rarely the problem. In order of how often they occur:

| Cause | What it looks like |
|---|---|
| **Field order** | The fields are concatenated in a fixed order, not the order your object happens to serialise in. Reordering them changes the hash. |
| **Amount format** | A payin writes the amount with two decimals, `100.00`. A payout writes it in cents, `10000`. |
| **A blank optional field** | An empty field contributes nothing at all. It does not contribute a placeholder, a space or the word `null`. |
| **Lowercasing** | Payin, payout and verification hashes lowercase the whole concatenated string, including the key. Refunds do not: see below. |
| **The key itself** | Test and production keys differ. A hash built with the wrong one fails in exactly the same way as a hash built in the wrong order. |

## Three things the notification hashes do differently

- **The payout notification takes `customerMerchantReference`**, where the payout request takes
  `customerBankReference`. Check which one you are reading before you hash it.
- **The payout notification's two statuses go in as integers**, not as their names.
- **A voucher payout appends the voucher pin after the key**, so the key is not last for that one hash.

The refund notification also arrives with the account number already masked, so verify it with the
masked value you received rather than the number you sent.

## Refunds do not lowercase

Payin, payout and payout verification hashes lowercase the entire concatenated string before
hashing. **A refund hash does not.**

Apply the payin rule to a refund and your hash is rejected the moment your refund reason or notify
URL contains a capital letter. Build the refund string exactly as your values are, with no
lowercasing.

## Which direction the hash goes

A **request** hash is one you build and send. Ozow rejects the request if it does not match.

A **notification** hash is one you check on something Ozow sent you, and **its field order is not
the same as the request's**. Reusing a request's order to verify an incoming notification rejects
every notification you receive, which is the single most expensive way to get this wrong: your
integration looks fine until money starts moving.

## One API does not use a hash

One API authenticates every call with an OAuth 2.0 bearer token, and its requests carry no
`hashCheck` field.

Its webhooks are not verified with a hash either. One API delivers them through
[Svix](https://www.svix.com/), which signs each one with an HMAC over the `svix-id`,
`svix-timestamp` and body. Verify it with the secret from the Get Webhook Secret endpoint, using the
Svix libraries. [Redirect to Ozow](https://hub.ozow.com/integration-methods/apis/payin/redirect-to-ozow.md) covers the
headers and how to check them.

Use this page for the Payments API and the Payouts API.

## Where each hash is used

| Request | Concatenates | Key |
|---|---|---|
| Payments API payin request | 32 fields, from `siteCode` to `tokenProfileId` | Your private key |
| Payments API payin notification | 13 fields, from `siteCode` to `statusMessage` | Your private key |
| Payments API refund request | `transactionId`, `amount`, `refundReason`, `notifyUrl` | Your private key |
| Payments API refund notification | 8 fields, from `refundId` to `statusMessage` | Your private key |
| Payouts API payout request | 11 fields, from `siteCode` to `identityType` | Your API key |
| Payouts API payout verification | The payout fields, with `payoutId` in front | Your API key |
| Payouts API payout notification | 6 fields, from `payoutId` to `payoutStatus.subStatus` | Your API key |

The payin hash is described in full in [Redirect to
Ozow](https://hub.ozow.com/integration-methods/apis/payin/redirect-to-ozow.md), and the payout hash in [Send a
payout](https://hub.ozow.com/integration-methods/apis/payout/send-a-payout.md).

> ℹ️ **Note**: Compute the hash on your server, never in browser JavaScript. A hash built in the
> browser needs the private key in the browser, and anything that reaches the client can be read by
> anyone holding the client.

---

# Payment method identifiers

> The UUID for each payment method Ozow supports, for the fields that take one.

Source: https://hub.ozow.com/integration-methods/apis/payin/payment-method-ids/

Ozow identifies each payment method by a UUID. The same identifier works across the APIs,
under different field names: `institutionId` on One API, `SelectedBankId` on the
Payments API.

Send one to take a customer straight to that method, skipping the screen where they
choose. Leave the field out and the customer chooses on the Ozow payment page, which is
what most integrations do.

**Standalone button** marks the ones you can put your own button behind, as
[Choose a checkout experience](https://hub.ozow.com/integration-methods/apis/payin.md)
describes. The rest identify a bank within Pay by Bank rather than a method a customer
would recognise as its own button.

| Payment method | Identifier | Standalone button |
|---|---|---|
| Absa | `3284A0AD-BA78-4838-8C2B-102981286A2B` | Deprecated |
| Absa Pay | `8F0B5AD2-2A44-4FF2-B052-D4E1E426587D` | Yes |
| African Bank | `33A0840B-0CF4-4B8C-86E0-EC6C4BE8C60E` | Yes |
| Bidvest Bank Grow | `E022DFC8-FF4A-4425-A074-C65D07E8F09C` | Yes |
| Buy Now Pay Later | `643C3DCF-9FC3-47BB-A11A-A390B5680E2F` | Yes |
| Capitec Pay | `913999FA-3A32-4E3D-82F0-A1DF7E9E4F7B` | Yes |
| Card | `3B1ED354-46E8-465D-9213-8C7A8E5663CE` | Yes |
| Crypto | `43FDB792-3B88-4D36-A13D-42B7661E9F76` | Yes |
| FNB | `4816019C-3314-4C80-8B6B-B2CD16DCC4EC` | Yes |
| FNB Pay | `23D34554-5727-4BE9-9276-9DDD20431E2B` | Yes |
| GoTyme Bank | `28FCC8FA-985B-480B-82FD-7D09BC19C9D0` | Deprecated |
| Investec | `4B45BE85-B616-4BD1-9027-F8FCF8F9AF7B` | Yes |
| Nedbank | `D3889DF6-CDAC-4861-9D64-2B100FB7ED07` | Yes |
| Nedbank Direct EFT | `8FD134F9-4D3F-4F54-9B1F-0AE2E356CF24` | Yes |
| PayShap Request | `EEC08676-46EB-4F80-AF56-CAA5A6623880` | Yes |
| Standard Bank | `AD7D8DA4-1723-4066-94BB-6662D845E483` | Yes |
| Voucher | `42F71BF8-0E09-43D5-A6EB-4F7370CB5B20` | Yes |

> ℹ️ **Note**: Not every identifier is enabled on every account. Pay by Bank is available by
> default; Capitec Pay, Buy Now Pay Later, card, crypto and PayShap Request are enabled by
> Ozow on request. Sending an identifier your account is not enabled for will result in an error.

## Where these are used

| API | Field | Where |
|---|---|---|
| One API | `institutionId` | `POST /payments`, at the top level of the request |
| One API | `details.institutionId` | `POST /payments/{id}/transactions`, when `paymentType` is `ozowredirect` |
| One API | `institutionId` | Inside a bank account object: a refund's `paidTo`, a settlement's `bank`, and a redirect's `beneficiary` and `verifiedBankAccount` |
| Payments API | `SelectedBankId` | [Redirect to Ozow](https://hub.ozow.com/integration-methods/apis/deprecated-integrations/redirect-to-ozow.md), on the payment request |

> ⚠️ **Important**: Field names are case sensitive, and a name that differs by a character
> is a name the API does not recognise. The rejection does not say which field was wrong,
> so copy the names from the reference page for the operation you are calling rather than
> retyping them.

Bank availability changes. Banks go down for infrastructure work and updates, and Ozow
notifies you where it can, so treat a bank being reachable as something to handle rather
than assume.

---

# Transaction and settlement statuses

> Every payin, payout, refund and settlement status, which are final, and what to do about each.

Source: https://hub.ozow.com/integration-methods/statuses/

Every payment through Ozow moves through a series of statuses. This page lists all of them, tells
you which ones are final, and tells you what to do about each.

> 🚨 **Read this first.** `Pending` and `Complete` appear on a payin, on a settlement and on a
> refund. `Complete` on a payin is a customer's payment succeeding, on a settlement it is the
> payment out to you confirmed by Ozow's bank, and on a refund it is your customer having their
> money back. Refunds spell theirs as integers rather than strings. A status value on its own tells
> you nothing. Always check which object it belongs to before you act on it.

## How the lifecycles relate

A payment has more than one lifecycle. The transaction completing and the money reaching your bank
account are two separate events with two separate status vocabularies.

```mermaid
flowchart TB
    subgraph in["Money in"]
        direction LR
        A["Customer<br/>pays"] --> B["Payin<br/>Complete"]
        B --> C["Settlement<br/>Complete"]
        C --> D["Your bank<br/>account"]
    end
    subgraph out["Money out"]
        direction LR
        T["Your bank<br/>account"] -. "top up" .-> E["Your<br/>float"]
        E --> F["PayoutComplete"]
        E --> H["Refund"]
        F --> G["Recipient's<br/>bank account"]
        H --> G
    end
    in ~~~ out
```

**A completed transaction is not settled money.** A payin status of `Complete` means the customer's
payment succeeded and the funds *will* be settled to you. The settlement status tells you whether
that has actually happened. Don't use payin status to reconcile your bank account. Every other final
payin status settles nothing.

**Refunds and payouts draw on your float, not on the original transaction.** A refund needs a funded
float balance even though it's returning money the customer already paid, because that money has
already been settled to you. See [Float
top-up](https://hub.ozow.com/payment-products/settlements-and-float/float-top-up.md).

## Look up a status

[Status decoder](https://hub.ozow.com/integration-methods/statuses/), a tool on this page.

The tables below list every status in full.

## How to read a status

### Final vs non-final

A **final** status will not change on its own. A **non-final** status will be followed by another
update, and you must wait for it rather than acting.

Getting this wrong is the most common integration bug in payments. Never release goods, mark an
order paid, or notify a customer on a non-final status.

> ⚠️ One exception: payin `PendingInvestigation` is final in the sense that no automatic update is
> coming, but Ozow Support can change it to `Complete` or `Error` after a manual check. Treat it as
> needing human action, not as a settled outcome.

### Status and sub-status

Payouts return a status and, in most cases, a sub-status. **Branch on the sub-status whenever one is
present.**

**The API sends the code, not the name.** `subStatus` on a payout status response is a number, so
`405` is what arrives where this page says `PayoutProcessingError_InvalidAccountNumber`. Both are
listed. The name is for reading; the code is what your `switch` matches.

This matters because a parent status can be non-final while the sub-status under it is final.
`PayoutReceived` is non-final, but `Payout_ValidationFailed` beneath it is a final failure. If you
only read the parent status, you will wait forever for a payout that has already failed.

### Never infer status from the browser redirect

The customer's browser returning to your success page is not proof of payment. Statuses arrive on
your notification or webhook URL, and you can query them with the status check API. See [Building a
secure integration](https://hub.ozow.com/getting-started/building-a-secure-integration.md).

---

## Payin statuses

```mermaid
stateDiagram-v2
    direction LR

    state "Not paid" as NotPaid {
        Cancelled
        Abandoned
        Voided
        Error
    }

    [*] --> Created
    Created --> PendingInvestigation
    Created --> Pending
    Created --> Complete
    Created --> NotPaid

    PendingInvestigation --> Complete
    PendingInvestigation --> NotPaid

    Pending --> Complete
    Pending --> NotPaid
```

Which of the four unpaid outcomes you can get depends on where the payment was when it stopped.
`Voided` follows only `Created`. From `Pending` the only unpaid outcome is `Error`, and from
`PendingInvestigation` it is `Cancelled`, `Abandoned` or `Error`.

**Non-final:** `Created`, `Pending`
**Final:** `Complete`, `PendingInvestigation`, `Cancelled`, `Abandoned`, `Voided`, `Error`

**The `One API` column is the value `GET /payments/{id}/transactions` returns for that status.**

| Status | One API | Final? | Settles? | What it means | What to do |
|---|---|---|---|---|---|
| `Created` | `Incomplete` | No | No | The transaction has been created and the customer has opened the payment page, but hasn't completed it. | Wait for a final status. |
| `Complete` | `Successful` | Yes | **Yes** | The payment succeeded and the funds will be settled to you. | Nothing. Fulfil the order. |
| `Cancelled` | `Error` | Yes | No | The transaction was cancelled: either the customer pressed cancel, or the payment failed Ozow's verification with the bank. `StatusMessage` says which. | Nothing. If they still want to pay, ask them to start a new transaction. |
| `Abandoned` | `Error` | Yes | No | The customer started the transaction but didn't finish it, typically they closed the Ozow payment page. | Nothing. If they still want to pay, ask them to start a new transaction. |
| `Voided` | `Error` | Yes | No | The transaction was invalidated and won't be processed. Happens when the customer changes bank partway through the Ozow flow, or when the saved profile they chose is deleted, deactivated or fails to load. | Nothing. A new transaction will have been created for the method they switched to. **No notification is sent for a voided transaction**, so find these by polling rather than by waiting. |
| `Error` | `Error` | Yes | No | An error occurred while the transaction was being processed. Not caused by anything the customer did. | Ask the customer to retry the transaction. |
| `Pending` | `Pending` | No | No | The payment still has to be verified. The outcome follows once it has been. | Wait for the update on your notification URL. Do not release anything on it. |
| `PendingInvestigation` | `Pending` | Yes | No | Ozow could not complete its verification with the bank, so the payment has to be checked manually against your bank statement. | Check your bank statement. If the funds arrived, contact Ozow Support with proof of payment to have the status updated to `Complete`. If they didn't, contact Support to have it set to `Error`. |

> ℹ️ `Pending` is non-final: an update follows. Handle it as "wait", never as an outcome. It means
> neither that the payment failed nor that it succeeded.

> ⚠️ Refunding a transaction does not change its status. If you need to know whether a transaction
> has been refunded, check the refund records rather than relying on the transaction status.

> ℹ️ Settlement timing depends on the payment method.

---

## Payout statuses

```mermaid
stateDiagram-v2
    direction LR

    state "Ended without paying" as Ended {
        PayoutProcessingError
        PayoutReturned
    }

    [*] --> PayoutReceived
    PayoutReceived --> Verification
    Verification --> SubmittedForProcessing
    SubmittedForProcessing --> PayoutComplete
    PayoutComplete --> PayoutReturned

    Verification --> Ended
    SubmittedForProcessing --> Ended

    Verification --> PayoutPendingInvestigation
    SubmittedForProcessing --> PayoutPendingInvestigation
    PayoutPendingInvestigation --> PayoutComplete
    PayoutPendingInvestigation --> Ended
```

The happy path is `PayoutReceived` → `Verification` → `SubmittedForProcessing` → `PayoutComplete`.
`Verification` can also reach `PayoutComplete` directly, so a payout that never appears in
`SubmittedForProcessing` has not skipped a step. `PayoutProcessingError` and `PayoutReturned` are
grouped because nothing follows either of them.

> ⚠️ **`PayoutComplete` is not final, though it almost never changes.** A completed payout moves
> to `PayoutReturned` if the destination bank sends the money back, which is a rare event rather
> than one to plan a flow around. Keep handling status updates for a payout you have already marked
> paid, and you will hear about it on the day it happens. The only two statuses nothing follows are
> `PayoutProcessingError` and `PayoutReturned`.

**Payout timing:** with `isRtc` set to `true` the payout is instant. With `isRtc` set to `false` it
takes 1-2 business days.

> ⚠️ Branch on the **sub-status**, not the parent status. `PayoutReceived`, `Verification` and
> `PayoutPendingInvestigation` are all non-final while carrying sub-statuses that are final
> failures.

### PayoutReceived

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | No | The payout request has been received. | Wait for a final status. |
| `Payout_Unclassified` | 100 | Yes | No sub-status could be determined. The payout has failed. | Rare edge case. Treat as a failure and investigate. |
| `Payout_ValidationFailed` | 101 | Yes | Request validation failed. | Check the `ErrorMessage` field for the reason, correct the request, then resubmit. |

### Verification

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | No | The payout is being verified. | Wait for a final status. |
| `Verification_Pending` | 201 | No | Ozow is waiting for a response from your verification webhook. | Wait for a final status. |
| `Verification_Success` | 203 | No | Your webhook verified the payout successfully. Processing continues. | Wait for a final status. |
| `Verification_Failed` | 202 | Yes | Your verification webhook returned a response that failed verification. | Check why your webhook rejected it, then resubmit the payout. |
| `Verification_Error` | 204 | Yes | Ozow couldn't reach your verification webhook. | Check that your webhook is reachable. If you've changed its URL, confirm Ozow has the new one. Then resubmit. |
| `Verification_AccountNumberDecryptionFailed` | 205 | Yes | The key returned by your webhook failed to decrypt the account number. | Check your webhook's key handling, then resubmit the payout. |
| `Verification_Success_Awaiting_Funds` | 206 | No | The payout verified, but your float balance is too low to cover it. | Top up your float. The payout continues on its own once the funds are there. Do not resubmit. |
| `Verification_Success_Awaiting_Submission` | 207 | No | The payout verified and is queued for submission. | Wait for a final status. |

### SubmittedForProcessing

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | No | The payout is being processed. | Wait for a final status. |
| `SubmittedForProcessing_PayoutAddedToBatch` | 301 | No | The payout has been added to a batch. | Wait for a final status. |
| `SubmittedForProcessing_PayoutSubmittedToBank` | 302 | No | The batch has been processed and submitted to the bank. | Wait for a final status. |
| `SubmittedForProcessing_PayoutSubmittedToPpi` | 303 | No | The payout has been submitted for processing. | Wait for a final status. |

### PayoutComplete

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | No | The payout completed successfully. | Release whatever the payout was for, and keep handling updates: this can still become `PayoutReturned`. |

### PayoutProcessingError

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | Yes | An error occurred while processing the payout. | Resubmit the payout. |
| `PayoutProcessingError_PayoutRejected` | 401 | Yes | The bank rejected the payout. | Resubmit the payout. |
| `PayoutProcessingError_PayoutCancelled` | 402 | Yes | Ozow stopped the payout before it was paid. | Check the `ErrorMessage` field, then resubmit. |
| `PayoutProcessingError_Insufficient_Balance` | 403 | Yes | Your float balance was too low to cover the payout. | **Top up your float. Do not resubmit**: see the warning below. |
| `PayoutProcessingError_PayoutInternalError` | 404 | Yes | An internal error occurred during processing. | Resubmit the payout. |
| `PayoutProcessingError_InvalidAccountNumber` | 405 | Yes | The account number is invalid. | Correct the account number, then resubmit. |

> 🚨 **On `PayoutProcessingError_Insufficient_Balance` you must not resubmit.** Top up your float
> instead. Once the float is allocated, the payout processes automatically. Resubmitting risks
> paying the recipient twice.

### PayoutReturned

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | Yes | The payout couldn't be paid into the recipient's account. | Resubmit the payout. |
| `PayoutReturned_Unpaid` | 9001 | Yes | The destination bank rejected the payment. | Check that the destination account is still active, then resubmit. |

### PayoutPendingInvestigation

| Sub-status | Code | Final? | What it means | What to do |
|---|---|---|---|---|
| None | None | No | The payout is under investigation. | Wait for a final status. |
| `PayoutPendingInvestigation_AmountMismatch` | 601 | Yes | The payout failed because the amounts didn't match. | Check the payout details, then resubmit. |

> ⚠️ **A payout cannot be cancelled once it is submitted.** Check the request before you send it:
> the account number, the amount and the reference are all final from that point.

---

## Refund statuses

A refund returns money to a customer for a payin you've already received. Refunds draw on your float
balance, not on the original transaction; the funds from that transaction have already been settled
to you.

> ⚠️ Refund statuses are returned as **integers**, not strings. This is different from payin, payout
> and settlement statuses, which are returned as text.

> ⚠️ **The numeric values are not in lifecycle order.** A refund progresses `0` → `2` → `1`. Don't
> treat a higher value as further along, and don't use greater-than comparisons to test progress.

```mermaid
stateDiagram-v2
    [*] --> Pending: 0
    Pending --> Submitted: 2
    Pending --> Cancelled: 4
    Submitted --> Complete: 1
    Submitted --> Failed: 3
    Submitted --> Returned: 5
```

This is the path a refund takes. Three further statuses sit outside it, `Invalid` (-1),
`PendingInvestigation` (-2) and `Error` (-3). Each one has a row below. Handle them wherever they
turn up rather than by position.

**Non-final:** `Pending` (0), `Submitted` (2), `PendingInvestigation` (-2)
**Final:** `Complete` (1), `Failed` (3), `Cancelled` (4), `Returned` (5), `Invalid` (-1), `Error` (-3)

**The negative values are real.** `Invalid`, `PendingInvestigation` and `Error` are returned by the
API alongside the six above. Switch on all nine, or give your default branch something safe to do.

| Value | Status | Final? | What it means | What to do |
|---|---|---|---|---|
| `0` | `Pending` | No | The refund request has been submitted and accepted, but not yet processed. | Wait for a final status. |
| `2` | `Submitted` | No | The refund has been assigned to a batch and is being processed. | Wait for a final status. |
| `1` | `Complete` | Yes | The refund was paid successfully. The customer has their money. | Nothing. |
| `3` | `Failed` | Yes | The refund payment failed. | Check that your float is funded, then submit a new refund. |
| `4` | `Cancelled` | Yes | The refund was cancelled before it was submitted for processing. | Nothing. If the customer is still owed a refund, submit a new one. |
| `5` | `Returned` | Yes | The refund was paid and came back. The destination account did not accept the credit, or the destination bank could not apply it. | Confirm the account details with the customer before you try again. Resubmitting the same details without checking returns the money a second time. |
| `-1` | `Invalid` | Yes | The refund could not be accepted as submitted. | Check the refund details against the original transaction, then submit a corrected refund. |
| `-2` | `PendingInvestigation` | No | The refund is being checked manually. | Wait. Ozow reports this as `Pending` on a `thin` webhook, so a handler reading the webhook rather than the API will not see this value. |
| `-3` | `Error` | Yes | The refund failed because of an error rather than a rejection. | Ozow reports this as `Failed` on a `thin` webhook. Treat it as a failure and submit a new refund. |

> ⚠️ **On `Returned`, do not resubmit the same details without checking them.** The money came back
> from the destination, so sending it again the same way returns it again. Confirm the account with
> the customer first.

> ℹ️ Refunds require a funded float. If your float is empty, refunds will not process. See [Float top-up](https://hub.ozow.com/payment-products/settlements-and-float/float-top-up.md).

> ℹ️ **Refund status is the source of truth for whether a customer has been refunded.** The original
> payin transaction keeps the status it had, so don't use transaction status to check refund state.

---

## Settlement statuses

A settlement is the transfer of your collected funds into your bank account. Settlement statuses
tell you where that transfer is, and they are **not** the same thing as the status of the
transactions inside it.

```mermaid
stateDiagram-v2
    direction LR
    [*] --> Pending
    Pending --> Submitted
    Submitted --> Complete
    Submitted --> PendingInvestigation
    PendingInvestigation --> Complete
```

**Non-final:** `Pending`, `Submitted`, `PendingInvestigation`
**Final:** `Complete`

**A settlement has four statuses, and none of them is a payin status.**

| Status | Final? | What it means | What to do |
|---|---|---|---|
| `Pending` | No | The settlement has been created. The payment out to you has not started. | Wait. If it stays here for an extended period, email [support@ozow.com](mailto:support@ozow.com). |
| `Submitted` | No | The settlement has been submitted to Ozow's bank. It might not have reached yours yet. | Wait. If it stays here for an extended period, email [support@ozow.com](mailto:support@ozow.com). |
| `Complete` | Yes | Ozow has submitted the payment and confirmed with Ozow's bank that it went out. Your own bank still has to clear it. | Reconcile against your bank statement rather than against this status. `Complete` is Ozow's leg finished; the clearing leg is your bank's and can lag it. |
| `PendingInvestigation` | No | The settlement is being checked manually. | Wait. If it stays here for an extended period, email [support@ozow.com](mailto:support@ozow.com) with your settlement reference. |

Settlement timing depends on the payment method the funds came in through.

---

## Which statuses mean money moved

Only these mean money moved:

- **Payin:** `Complete`, and only `Complete` settles
- **Payout:** `PayoutComplete`, which can still become `PayoutReturned` if the bank sends it back
- **Refund:** `Complete` (`1`), the integer and not the string
- **Settlement:** `Complete`, which is the payment out confirmed by Ozow's bank, with your own bank
  still to clear it

Everything else is either in progress or a failure. In particular:

- `Pending`, `Created`, `Verification`, `SubmittedForProcessing` and `PayoutReceived` are **in
  progress**. Wait.
- A payin `PendingInvestigation` needs a human. It is not a success.
- A payin `Complete` is not settled money. Check the settlement status for that.

---

# The contract

The operations those pages declare, as the specification defines them.

---

# List Transactions for Payment

> GET `/payments/{id}/transactions`
> Part of the One API reference. Source: https://hub.ozow.com/api-reference/one-api/get-payments-id-transactions/

Server: `https://one.ozow.com/v1` (Production)

Other environments: `https://stagingone.ozow.com/v1` (Staging)

List the transactions associated with the payment request with the specified `id`. An `id` matching no payment answers 200 with an empty result list rather than 404, so check whether a transaction came back rather than reading the status code.

## Authentication

- `Authentication` (oauth2)
  - Scopes: `payments`

## Path parameters

- `id` (string, required) - The unique identifier of the payment.

## Query parameters

- `limit` (integer) - The maximum number of items to return.
- `offset` (integer) - The number of items to discard in this paging operation.
- `fromDate` (string, required) - The date from which to filter, inclusive. Whole days only, so any time sent with it is discarded.
- `toDate` (string, required) - The date to filter up to, inclusive. Whole days only, so any time sent with it is discarded.

## Header parameters

- `Idempotency-Key` (string) - The unique key idempotency key as per the following [IETF Draft](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/)
- `X-Correlation-ID` (string) - Optional correlation id for the request, if not supplied a new one will be generated and passed onto all underlying requests and returned as a header.

## Responses

### 200 OK.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

- `links` (object) - Standard [JSON API pagination links](https://jsonapi.org/format/#fetching-pagination). Each link repeats the query that produced the collection and carries its own `limit` and `offset`. Follow the link rather than building the next URI. The example below is the second of three pages.
- `results` (array of Transaction)
- `meta` (object)

### 400 Bad Request

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

- `id` (string, uuid, required) - a unique identifier for this particular occurrence of the problem.
- `links` (object, nullable) - Present on an authentication or authorisation failure, and null otherwise.
  - `about` (string, uri) - A link that leads to further details about this particular occurrence of the problem. When derefenced, this URI SHOULD return a human-readable description of the error.
  - `type` (string, uri) - A link that identifies the type of error that this particular error is an instance of. This URI SHOULD be dereferencable to a human-readable explanation of the general error.
- `code` (string, required) - An application-specific error code, expressed as a string value. Key on this rather than on `title` or `detail`, which are written for a person. A rejection at the transport level uses the status name, one of `BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `NotAllowed`, `Conflict`, `UnsupportedMediaType`, `BadGateway` or `InternalServerError`. An operation refusing a request on its own rules returns a code of its own.
- `title` (string, required) - A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
- `detail` (string, required) - A human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized.
- `source` (object, nullable) - Where in the request the problem was found. All three keys are present whenever `source` is, with the ones that do not apply set to null. `source` itself is null where the failure is not about a part of the request.
  - `pointer` (string, json-pointer, nullable) - a JSON Pointer [RFC6901](https://tools.ietf.org/html/rfc6901) to the value in the request document that caused the error [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute]. This MUST point to a value in the request document that exists; if it doesn’t, the client SHOULD simply ignore the pointer.
  - `parameter` (string, nullable) - A string indicating which URI query parameter caused the error.
  - `header` (string, nullable) - A string indicating the name of a single request header which caused the error.
- `meta` (object, nullable) - A [meta object](https://jsonapi.org/format/#document-meta) containing non-standard meta-information about the error. Null where the request carried no `X-Correlation-ID`, and on an authentication failure, which does not echo it.
  - `correlationId` (string) - The `X-Correlation-ID` sent with the request, echoed back so it can be quoted to support. Absent when the request carried no correlation header.

### 401 Unauthorized

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

### 500 Internal Server Error.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The request failed on the Ozow side):

```json
{
  "id": "9e0d5a83-6b24-4c19-8f7a-2d1b3e6c0a97",
  "links": null,
  "code": "InternalServerError",
  "title": "Internal Server Error",
  "detail": "Error occurred while processing request.",
  "source": {
    "pointer": "/data",
    "parameter": null,
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```


---

# List Webhook Subscriptions

> GET `/webhooks`
> Part of the One API reference. Source: https://hub.ozow.com/api-reference/one-api/get-webhooks/

Server: `https://one.ozow.com/v1` (Production)

Other environments: `https://stagingone.ozow.com/v1` (Staging)

Retrieve a list of active webhook subscriptions.

## Authentication

- `Authentication` (oauth2)
  - Scopes: `webhooks`

## Query parameters

- `limit` (integer) - The maximum number of items to return.
- `offset` (integer) - The number of items to discard in this paging operation.

## Header parameters

- `X-Forwarded-For` (string) - The IP address of the end-consumer.
- `X-Correlation-ID` (string) - Optional correlation id for the request, if not supplied a new one will be generated and passed onto all underlying requests and returned as a header.

## Responses

### 200 OK

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

- `links` (object) - Standard [JSON API pagination links](https://jsonapi.org/format/#fetching-pagination). Each link repeats the query that produced the collection and carries its own `limit` and `offset`. Follow the link rather than building the next URI. The example below is the second of three pages.
- `results` (array of WebhookResponse)
- `meta` (object)

### 400 Bad Request.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

- `id` (string, uuid, required) - a unique identifier for this particular occurrence of the problem.
- `links` (object, nullable) - Present on an authentication or authorisation failure, and null otherwise.
  - `about` (string, uri) - A link that leads to further details about this particular occurrence of the problem. When derefenced, this URI SHOULD return a human-readable description of the error.
  - `type` (string, uri) - A link that identifies the type of error that this particular error is an instance of. This URI SHOULD be dereferencable to a human-readable explanation of the general error.
- `code` (string, required) - An application-specific error code, expressed as a string value. Key on this rather than on `title` or `detail`, which are written for a person. A rejection at the transport level uses the status name, one of `BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `NotAllowed`, `Conflict`, `UnsupportedMediaType`, `BadGateway` or `InternalServerError`. An operation refusing a request on its own rules returns a code of its own.
- `title` (string, required) - A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
- `detail` (string, required) - A human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized.
- `source` (object, nullable) - Where in the request the problem was found. All three keys are present whenever `source` is, with the ones that do not apply set to null. `source` itself is null where the failure is not about a part of the request.
  - `pointer` (string, json-pointer, nullable) - a JSON Pointer [RFC6901](https://tools.ietf.org/html/rfc6901) to the value in the request document that caused the error [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute]. This MUST point to a value in the request document that exists; if it doesn’t, the client SHOULD simply ignore the pointer.
  - `parameter` (string, nullable) - A string indicating which URI query parameter caused the error.
  - `header` (string, nullable) - A string indicating the name of a single request header which caused the error.
- `meta` (object, nullable) - A [meta object](https://jsonapi.org/format/#document-meta) containing non-standard meta-information about the error. Null where the request carried no `X-Correlation-ID`, and on an authentication failure, which does not echo it.
  - `correlationId` (string) - The `X-Correlation-ID` sent with the request, echoed back so it can be quoted to support. Absent when the request carried no correlation header.

Example (A field in the request body did not validate):

```json
{
  "id": "3a6c9e01-5f2b-4d8a-9c47-1e0b7d5a2f83",
  "links": null,
  "code": "BadRequest",
  "title": "Bad Request",
  "detail": "amount: Amount must be greater than 0",
  "source": {
    "pointer": "/amount",
    "parameter": null,
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 401 Unauthorised.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (No usable access token on the request):

```json
{
  "id": "1cecc2b7-1c29-418a-b26a-bf7546926083",
  "links": {
    "about": "https://ozow.stoplight.io/docs/one-api/zi18vomr0jm8c-generate-authentication-token",
    "type": "https://tools.ietf.org/html/rfc7235#section-3.1"
  },
  "code": "Unauthorized",
  "title": "Unauthorized Request",
  "detail": "Authorization header is missing or invalid.",
  "source": {
    "pointer": null,
    "parameter": null,
    "header": "Authorization"
  },
  "meta": null
}
```

### 403 Forbidden.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The token is valid but lacks the scope the operation needs):

```json
{
  "id": "c47a2e08-9b31-4f6d-85a0-7e2c1d9f3b56",
  "links": {
    "about": "https://ozow.stoplight.io/docs/one-api/zi18vomr0jm8c-generate-authentication-token",
    "type": "https://tools.ietf.org/html/rfc7235#section-3.1"
  },
  "code": "Forbidden",
  "title": "Forbidden Request",
  "detail": "Request is forbidden, most likely scope does not match required scope to perform requested action.",
  "source": {
    "pointer": null,
    "parameter": null,
    "header": "Authorization"
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 500 Internal Server Error.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The request failed on the Ozow side):

```json
{
  "id": "9e0d5a83-6b24-4c19-8f7a-2d1b3e6c0a97",
  "links": null,
  "code": "InternalServerError",
  "title": "Internal Server Error",
  "detail": "Error occurred while processing request.",
  "source": {
    "pointer": "/data",
    "parameter": null,
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```


---

# Get Webhook Secret

> GET `/webhooks/{id}/secret`
> Part of the One API reference. Source: https://hub.ozow.com/api-reference/one-api/get-webhooks-id-secret/

Server: `https://one.ozow.com/v1` (Production)

Other environments: `https://stagingone.ozow.com/v1` (Staging)

Retrieves the secret for the webhook.

## Authentication

- `Authentication` (oauth2)
  - Scopes: `webhooks`

## Path parameters

- `id` (string, required) - The unique identifier of the webhook subscription.

## Header parameters

- `X-Correlation-ID` (string) - Optional correlation id for the request, if not supplied a new one will be generated and passed onto all underlying requests and returned as a header.

## Responses

### 200 OK

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

- `secret` (string, required) - The secret key of the webhook to be used when validating the webhook signature.

### 400 Bad Request.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

- `id` (string, uuid, required) - a unique identifier for this particular occurrence of the problem.
- `links` (object, nullable) - Present on an authentication or authorisation failure, and null otherwise.
  - `about` (string, uri) - A link that leads to further details about this particular occurrence of the problem. When derefenced, this URI SHOULD return a human-readable description of the error.
  - `type` (string, uri) - A link that identifies the type of error that this particular error is an instance of. This URI SHOULD be dereferencable to a human-readable explanation of the general error.
- `code` (string, required) - An application-specific error code, expressed as a string value. Key on this rather than on `title` or `detail`, which are written for a person. A rejection at the transport level uses the status name, one of `BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `NotAllowed`, `Conflict`, `UnsupportedMediaType`, `BadGateway` or `InternalServerError`. An operation refusing a request on its own rules returns a code of its own.
- `title` (string, required) - A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
- `detail` (string, required) - A human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized.
- `source` (object, nullable) - Where in the request the problem was found. All three keys are present whenever `source` is, with the ones that do not apply set to null. `source` itself is null where the failure is not about a part of the request.
  - `pointer` (string, json-pointer, nullable) - a JSON Pointer [RFC6901](https://tools.ietf.org/html/rfc6901) to the value in the request document that caused the error [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute]. This MUST point to a value in the request document that exists; if it doesn’t, the client SHOULD simply ignore the pointer.
  - `parameter` (string, nullable) - A string indicating which URI query parameter caused the error.
  - `header` (string, nullable) - A string indicating the name of a single request header which caused the error.
- `meta` (object, nullable) - A [meta object](https://jsonapi.org/format/#document-meta) containing non-standard meta-information about the error. Null where the request carried no `X-Correlation-ID`, and on an authentication failure, which does not echo it.
  - `correlationId` (string) - The `X-Correlation-ID` sent with the request, echoed back so it can be quoted to support. Absent when the request carried no correlation header.

Example (A field in the request body did not validate):

```json
{
  "id": "3a6c9e01-5f2b-4d8a-9c47-1e0b7d5a2f83",
  "links": null,
  "code": "BadRequest",
  "title": "Bad Request",
  "detail": "amount: Amount must be greater than 0",
  "source": {
    "pointer": "/amount",
    "parameter": null,
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 401 Unauthorised.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (No usable access token on the request):

```json
{
  "id": "1cecc2b7-1c29-418a-b26a-bf7546926083",
  "links": {
    "about": "https://ozow.stoplight.io/docs/one-api/zi18vomr0jm8c-generate-authentication-token",
    "type": "https://tools.ietf.org/html/rfc7235#section-3.1"
  },
  "code": "Unauthorized",
  "title": "Unauthorized Request",
  "detail": "Authorization header is missing or invalid.",
  "source": {
    "pointer": null,
    "parameter": null,
    "header": "Authorization"
  },
  "meta": null
}
```

### 403 Forbidden.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The token is valid but lacks the scope the operation needs):

```json
{
  "id": "c47a2e08-9b31-4f6d-85a0-7e2c1d9f3b56",
  "links": {
    "about": "https://ozow.stoplight.io/docs/one-api/zi18vomr0jm8c-generate-authentication-token",
    "type": "https://tools.ietf.org/html/rfc7235#section-3.1"
  },
  "code": "Forbidden",
  "title": "Forbidden Request",
  "detail": "Request is forbidden, most likely scope does not match required scope to perform requested action.",
  "source": {
    "pointer": null,
    "parameter": null,
    "header": "Authorization"
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 500 Internal Server Error.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The request failed on the Ozow side):

```json
{
  "id": "9e0d5a83-6b24-4c19-8f7a-2d1b3e6c0a97",
  "links": null,
  "code": "InternalServerError",
  "title": "Internal Server Error",
  "detail": "Error occurred while processing request.",
  "source": {
    "pointer": "/data",
    "parameter": null,
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```


---

# Request Payment

> POST `/payments`
> Part of the One API reference. Source: https://hub.ozow.com/api-reference/one-api/post-payments/

Server: `https://one.ozow.com/v1` (Production)

Other environments: `https://stagingone.ozow.com/v1` (Staging)

Create a payment request with the specified channel and transaction details.

## Authentication

- `Authentication` (oauth2)
  - Scopes: `payments`

## Header parameters

- `Idempotency-Key` (string) - The unique key idempotency key as per the following [IETF Draft](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/)
- `X-Correlation-ID` (string) - Optional correlation id for the request, if not supplied a new one will be generated and passed onto all underlying requests and returned as a header.

## Request body

- `siteCode` (string, required, max length 50) - The merchant site code in use for this payment. Site codes are available on the Ozow dashboard.
- `region` (string, min length 2, max length 2) - ISO 3166-Alpha-2 code for the originating country of the payment. Must be "ZA" for South Africa. If region is not specified IP geolocation will be used to determine the applicable region.
- `amount` (object, required) - The currency and amount of the payment request.
- `variableAmount` (object) - If the payment amount can be changed by the payer, the variable amounts need to be passed in.
- `merchantReference` (string, required, max length 50) - The merchant's reference for the transaction. It is pre-populated in the payer's own reference field at their bank, shortened by banks that limit it, so it is not an internal-only value.
- `beneficiaryReference` (string, max length 20, pattern ^[A-Za-z0-9]*$) - The reference that appears on the merchant's bank statement for the payment. Letters and numbers only. Rejected as missing unless the site is configured to let the payer supply the reference. A site prefix, where one is configured, counts towards the 20 characters.
- `payerReference` (string, max length 20)
- `payer` (object) - Information on the payer used for identification and fraud purposes.
- `returnUrl` (string, uri) - The URI that Ozow needs to redirect back to once the payment has reached a conclusion. Must be reachable from the internet. `localhost` is rejected with a 403, so a local integration needs a tunnel rather than the address the browser uses.
- `notifyUrl` (string, uri) - Optional notify URL to send notifications of the status of the payment. The recommendation is to use webhooks instead of this method which are configurable via the webhooks endpoints of the API or via the Ozow Dashboard. Must be reachable from the internet. `localhost` is rejected with a 403, so a local integration needs a tunnel rather than the address the browser uses.
- `expireAt` (string, date-time, required) - The date and time the payment request should expire at and make the payment link unusable
- `institutionId` (string, uuid) - The institution to send the payer straight to, skipping the payment method selection screen. The identifier for each payment method is on that method's page under Payment products.

## Responses

### 200 OK

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

- `links` (object, required) - The relevant links to the this payment.
- `id` (string, uuid, required) - The identifier for the payment request.
- `status` (PaymentStatus, required, one of "Created", "Expired") - The status of the payment.
- `reason` (string) - The payment status reason.
- `redirectUrl` (string, uri) - The url to redirect a consumer to. A redirect url will be provided should a payment require further client interaction.

### 201 Created

- Header `X-Correlation-ID`: The correlation id for the request that was processed.
- Header `Location`: The unique URI for this resource.

`PaymentResponse`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/payment-response.md

### 400 Bad Request.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

- `id` (string, uuid, required) - a unique identifier for this particular occurrence of the problem.
- `links` (object, nullable) - Present on an authentication or authorisation failure, and null otherwise.
  - `about` (string, uri) - A link that leads to further details about this particular occurrence of the problem. When derefenced, this URI SHOULD return a human-readable description of the error.
  - `type` (string, uri) - A link that identifies the type of error that this particular error is an instance of. This URI SHOULD be dereferencable to a human-readable explanation of the general error.
- `code` (string, required) - An application-specific error code, expressed as a string value. Key on this rather than on `title` or `detail`, which are written for a person. A rejection at the transport level uses the status name, one of `BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `NotAllowed`, `Conflict`, `UnsupportedMediaType`, `BadGateway` or `InternalServerError`. An operation refusing a request on its own rules returns a code of its own.
- `title` (string, required) - A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
- `detail` (string, required) - A human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized.
- `source` (object, nullable) - Where in the request the problem was found. All three keys are present whenever `source` is, with the ones that do not apply set to null. `source` itself is null where the failure is not about a part of the request.
  - `pointer` (string, json-pointer, nullable) - a JSON Pointer [RFC6901](https://tools.ietf.org/html/rfc6901) to the value in the request document that caused the error [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute]. This MUST point to a value in the request document that exists; if it doesn’t, the client SHOULD simply ignore the pointer.
  - `parameter` (string, nullable) - A string indicating which URI query parameter caused the error.
  - `header` (string, nullable) - A string indicating the name of a single request header which caused the error.
- `meta` (object, nullable) - A [meta object](https://jsonapi.org/format/#document-meta) containing non-standard meta-information about the error. Null where the request carried no `X-Correlation-ID`, and on an authentication failure, which does not echo it.
  - `correlationId` (string) - The `X-Correlation-ID` sent with the request, echoed back so it can be quoted to support. Absent when the request carried no correlation header.

Example (A field in the request body did not validate):

```json
{
  "id": "3a6c9e01-5f2b-4d8a-9c47-1e0b7d5a2f83",
  "links": null,
  "code": "BadRequest",
  "title": "Bad Request",
  "detail": "amount: Amount must be greater than 0",
  "source": {
    "pointer": "/amount",
    "parameter": null,
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 401 Unauthorised.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (No usable access token on the request):

```json
{
  "id": "1cecc2b7-1c29-418a-b26a-bf7546926083",
  "links": {
    "about": "https://ozow.stoplight.io/docs/one-api/zi18vomr0jm8c-generate-authentication-token",
    "type": "https://tools.ietf.org/html/rfc7235#section-3.1"
  },
  "code": "Unauthorized",
  "title": "Unauthorized Request",
  "detail": "Authorization header is missing or invalid.",
  "source": {
    "pointer": null,
    "parameter": null,
    "header": "Authorization"
  },
  "meta": null
}
```

### 403 Forbidden.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The token is valid but lacks the scope the operation needs):

```json
{
  "id": "c47a2e08-9b31-4f6d-85a0-7e2c1d9f3b56",
  "links": {
    "about": "https://ozow.stoplight.io/docs/one-api/zi18vomr0jm8c-generate-authentication-token",
    "type": "https://tools.ietf.org/html/rfc7235#section-3.1"
  },
  "code": "Forbidden",
  "title": "Forbidden Request",
  "detail": "Request is forbidden, most likely scope does not match required scope to perform requested action.",
  "source": {
    "pointer": null,
    "parameter": null,
    "header": "Authorization"
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 409 Conflict.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The idempotency key was reused with a different body):

```json
{
  "id": "2f8b6d40-1c7e-49a5-b03f-8d5a2e1c9704",
  "links": null,
  "code": "Conflict",
  "title": "Conflict",
  "detail": "Idempotency key and request data do not match a previous request.",
  "source": {
    "pointer": null,
    "parameter": null,
    "header": "Idempotency-Key"
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 500 Internal Server Error.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The request failed on the Ozow side):

```json
{
  "id": "9e0d5a83-6b24-4c19-8f7a-2d1b3e6c0a97",
  "links": null,
  "code": "InternalServerError",
  "title": "Internal Server Error",
  "detail": "Error occurred while processing request.",
  "source": {
    "pointer": "/data",
    "parameter": null,
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```


---

# Cancel Payment Request

> POST `/payments/{id}/cancel`
> Part of the One API reference. Source: https://hub.ozow.com/api-reference/one-api/post-payments-id-cancel/

Server: `https://one.ozow.com/v1` (Production)

Other environments: `https://stagingone.ozow.com/v1` (Staging)

Cancels the payment request with the specified `id`.

## Authentication

- `Authentication` (oauth2)
  - Scopes: `payments`

## Path parameters

- `id` (string, required) - The unique identifier of the payment.

## Header parameters

- `Idempotency-Key` (string) - The unique key idempotency key as per the following [IETF Draft](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/)
- `X-Correlation-ID` (string) - Optional correlation id for the request, if not supplied a new one will be generated and passed onto all underlying requests and returned as a header.

## Responses

### 200 OK. The payment has been cancelled.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

### 201 Created. The payment has been cancelled

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

### 403 Forbidden.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

- `id` (string, uuid, required) - a unique identifier for this particular occurrence of the problem.
- `links` (object, nullable) - Present on an authentication or authorisation failure, and null otherwise.
  - `about` (string, uri) - A link that leads to further details about this particular occurrence of the problem. When derefenced, this URI SHOULD return a human-readable description of the error.
  - `type` (string, uri) - A link that identifies the type of error that this particular error is an instance of. This URI SHOULD be dereferencable to a human-readable explanation of the general error.
- `code` (string, required) - An application-specific error code, expressed as a string value. Key on this rather than on `title` or `detail`, which are written for a person. A rejection at the transport level uses the status name, one of `BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `NotAllowed`, `Conflict`, `UnsupportedMediaType`, `BadGateway` or `InternalServerError`. An operation refusing a request on its own rules returns a code of its own.
- `title` (string, required) - A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
- `detail` (string, required) - A human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized.
- `source` (object, nullable) - Where in the request the problem was found. All three keys are present whenever `source` is, with the ones that do not apply set to null. `source` itself is null where the failure is not about a part of the request.
  - `pointer` (string, json-pointer, nullable) - a JSON Pointer [RFC6901](https://tools.ietf.org/html/rfc6901) to the value in the request document that caused the error [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute]. This MUST point to a value in the request document that exists; if it doesn’t, the client SHOULD simply ignore the pointer.
  - `parameter` (string, nullable) - A string indicating which URI query parameter caused the error.
  - `header` (string, nullable) - A string indicating the name of a single request header which caused the error.
- `meta` (object, nullable) - A [meta object](https://jsonapi.org/format/#document-meta) containing non-standard meta-information about the error. Null where the request carried no `X-Correlation-ID`, and on an authentication failure, which does not echo it.
  - `correlationId` (string) - The `X-Correlation-ID` sent with the request, echoed back so it can be quoted to support. Absent when the request carried no correlation header.

Example (The token is valid but lacks the scope the operation needs):

```json
{
  "id": "c47a2e08-9b31-4f6d-85a0-7e2c1d9f3b56",
  "links": {
    "about": "https://ozow.stoplight.io/docs/one-api/zi18vomr0jm8c-generate-authentication-token",
    "type": "https://tools.ietf.org/html/rfc7235#section-3.1"
  },
  "code": "Forbidden",
  "title": "Forbidden Request",
  "detail": "Request is forbidden, most likely scope does not match required scope to perform requested action.",
  "source": {
    "pointer": null,
    "parameter": null,
    "header": "Authorization"
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 404 Not found.  The item with the specified identifier could not be found, or this resource is not allowed for the resource identifier.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (No resource with that identifier):

```json
{
  "id": "5b9e3c17-4a8d-42f0-9e61-3c7b0f2a8d15",
  "links": null,
  "code": "NotFound",
  "title": "Not Found",
  "detail": "The requested resource was not found.",
  "source": {
    "pointer": "/data",
    "parameter": "/v1/payments/497f6eca-6276-4993-bfeb-53cbbbba6f08",
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 500 Internal Server Error.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The request failed on the Ozow side):

```json
{
  "id": "9e0d5a83-6b24-4c19-8f7a-2d1b3e6c0a97",
  "links": null,
  "code": "InternalServerError",
  "title": "Internal Server Error",
  "detail": "Error occurred while processing request.",
  "source": {
    "pointer": "/data",
    "parameter": null,
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```


---

# Generate Authentication Token

> POST `/token`
> Part of the One API reference. Source: https://hub.ozow.com/api-reference/one-api/post-token/

Server: `https://one.ozow.com/v1` (Production)

Other environments: `https://stagingone.ozow.com/v1` (Staging)

Retrieve an authentication token as per the [OpenAuth 2.0 Client Credential Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4).

## Authentication

This operation takes no credentials.

## Header parameters

- `X-Correlation-ID` (string) - Optional correlation id for the request, if not supplied a new one will be generated and passed onto all underlying requests and returned as a header.

## Request body

- `client_id` (string, required) - The unique client id provided to the merchant during credential exchange.
- `client_secret` (string, required) - The secret provided to the merchant during credential exchange.
- `scope` (string, required) - The scope of permissions required. Can be just one or a list of space-delimited, case-sensitive strings.
- `grant_type` (string, required) - Must be _client_credentials_.

## Responses

### 200 Succesfull response as per [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-5.1)

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

- `access_token` (string, required) - The access token issued by the authorization server.
- `token_type` (string, required) - Must be `bearer` as in [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750).
- `expires_in` (string, required) - The lifetime in seconds of the access token. For example, the value "3600" denotes that the access token will expire in one hour from the time the response was generated.
- `scope` (string) - OPTIONAL, if identical to the scope requested by the client; otherwise, REQUIRED.

### 400 Bad Request.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

- `id` (string, uuid, required) - a unique identifier for this particular occurrence of the problem.
- `links` (object, nullable) - Present on an authentication or authorisation failure, and null otherwise.
  - `about` (string, uri) - A link that leads to further details about this particular occurrence of the problem. When derefenced, this URI SHOULD return a human-readable description of the error.
  - `type` (string, uri) - A link that identifies the type of error that this particular error is an instance of. This URI SHOULD be dereferencable to a human-readable explanation of the general error.
- `code` (string, required) - An application-specific error code, expressed as a string value. Key on this rather than on `title` or `detail`, which are written for a person. A rejection at the transport level uses the status name, one of `BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `NotAllowed`, `Conflict`, `UnsupportedMediaType`, `BadGateway` or `InternalServerError`. An operation refusing a request on its own rules returns a code of its own.
- `title` (string, required) - A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
- `detail` (string, required) - A human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized.
- `source` (object, nullable) - Where in the request the problem was found. All three keys are present whenever `source` is, with the ones that do not apply set to null. `source` itself is null where the failure is not about a part of the request.
  - `pointer` (string, json-pointer, nullable) - a JSON Pointer [RFC6901](https://tools.ietf.org/html/rfc6901) to the value in the request document that caused the error [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute]. This MUST point to a value in the request document that exists; if it doesn’t, the client SHOULD simply ignore the pointer.
  - `parameter` (string, nullable) - A string indicating which URI query parameter caused the error.
  - `header` (string, nullable) - A string indicating the name of a single request header which caused the error.
- `meta` (object, nullable) - A [meta object](https://jsonapi.org/format/#document-meta) containing non-standard meta-information about the error. Null where the request carried no `X-Correlation-ID`, and on an authentication failure, which does not echo it.
  - `correlationId` (string) - The `X-Correlation-ID` sent with the request, echoed back so it can be quoted to support. Absent when the request carried no correlation header.

Example (A field in the request body did not validate):

```json
{
  "id": "3a6c9e01-5f2b-4d8a-9c47-1e0b7d5a2f83",
  "links": null,
  "code": "BadRequest",
  "title": "Bad Request",
  "detail": "amount: Amount must be greater than 0",
  "source": {
    "pointer": "/amount",
    "parameter": null,
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 401 Unauthorised. A scope in `scope` is not one this client is allowed. Scopes are granted per client, so a client can authenticate and still be refused a scope.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (A requested scope is not granted to this client):

```json
{
  "id": "4f7c0d2b-8a15-4e93-b06d-7c2e9f1a5b48",
  "links": {
    "about": "https://ozow.stoplight.io/docs/one-api/zi18vomr0jm8c-generate-authentication-token",
    "type": "https://tools.ietf.org/html/rfc7235#section-3.1"
  },
  "code": "Unauthorized",
  "title": "Unauthorized Request",
  "detail": "Consumer does not have access to requested scope",
  "source": {
    "pointer": null,
    "parameter": null,
    "header": "Authorization"
  },
  "meta": null
}
```

### 404 Not found. No client matches `client_id`, or the client has been deactivated.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The client id does not resolve):

```json
{
  "id": "cccb6fb8-505e-4ff9-90b5-815ecf42424e",
  "links": null,
  "code": "NotFound",
  "title": "Not Found",
  "detail": "Consumer could not be found for client id YOUR_CLIENT_ID.",
  "source": {
    "pointer": "/data/clientId",
    "parameter": "/v1/token",
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 500 Internal Server Error.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The request failed on the Ozow side):

```json
{
  "id": "9e0d5a83-6b24-4c19-8f7a-2d1b3e6c0a97",
  "links": null,
  "code": "InternalServerError",
  "title": "Internal Server Error",
  "detail": "Error occurred while processing request.",
  "source": {
    "pointer": "/data",
    "parameter": null,
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```


---

# Create Webhook Subscription

> POST `/webhooks`
> Part of the One API reference. Source: https://hub.ozow.com/api-reference/one-api/post-webhooks/

Server: `https://one.ozow.com/v1` (Production)

Other environments: `https://stagingone.ozow.com/v1` (Staging)

Create a webhook subscription.

## Authentication

- `Authentication` (oauth2)
  - Scopes: `webhooks`

## Header parameters

- `Idempotency-Key` (string) - The unique key idempotency key as per the following [IETF Draft](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/)
- `X-Correlation-ID` (string) - Optional correlation id for the request, if not supplied a new one will be generated and passed onto all underlying requests and returned as a header.

## Request body

- `endpoint` (string, uri, required) - The uri of the webhook receiver. Must be reachable from the internet. `localhost` is rejected with a 403, so a local integration needs a tunnel rather than the address the browser uses.
- `eventType` (any, required, one of "transaction.complete", "refund.complete") - The type of event the webhook subscribes to.
- `messageType` (any, one of "thin", "full") - Defaults to `thin`. Specify `full` to receive a larger payload with as much detail as possible.

## Responses

### 200 OK. The request has been accepted.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

- `id` (string, uuid, required) - The unique identifier of the webhook.
- `endpoint` (string, uri, required) - The uri of the webhook receiver.
- `eventType` (any, required, one of "transaction.complete", "refund.complete") - The type of event the webhook subscribes to.
- `messageType` (any, required, one of "thin", "full") - Defaults to `thin`. Specify `full` to receive a larger payload with as much detail as possible.

### 201 Created. The request has been accepted.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`WebhookResponse`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/webhook-response.md

### 400 Bad Request.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

- `id` (string, uuid, required) - a unique identifier for this particular occurrence of the problem.
- `links` (object, nullable) - Present on an authentication or authorisation failure, and null otherwise.
  - `about` (string, uri) - A link that leads to further details about this particular occurrence of the problem. When derefenced, this URI SHOULD return a human-readable description of the error.
  - `type` (string, uri) - A link that identifies the type of error that this particular error is an instance of. This URI SHOULD be dereferencable to a human-readable explanation of the general error.
- `code` (string, required) - An application-specific error code, expressed as a string value. Key on this rather than on `title` or `detail`, which are written for a person. A rejection at the transport level uses the status name, one of `BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `NotAllowed`, `Conflict`, `UnsupportedMediaType`, `BadGateway` or `InternalServerError`. An operation refusing a request on its own rules returns a code of its own.
- `title` (string, required) - A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
- `detail` (string, required) - A human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized.
- `source` (object, nullable) - Where in the request the problem was found. All three keys are present whenever `source` is, with the ones that do not apply set to null. `source` itself is null where the failure is not about a part of the request.
  - `pointer` (string, json-pointer, nullable) - a JSON Pointer [RFC6901](https://tools.ietf.org/html/rfc6901) to the value in the request document that caused the error [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute]. This MUST point to a value in the request document that exists; if it doesn’t, the client SHOULD simply ignore the pointer.
  - `parameter` (string, nullable) - A string indicating which URI query parameter caused the error.
  - `header` (string, nullable) - A string indicating the name of a single request header which caused the error.
- `meta` (object, nullable) - A [meta object](https://jsonapi.org/format/#document-meta) containing non-standard meta-information about the error. Null where the request carried no `X-Correlation-ID`, and on an authentication failure, which does not echo it.
  - `correlationId` (string) - The `X-Correlation-ID` sent with the request, echoed back so it can be quoted to support. Absent when the request carried no correlation header.

Example (A field in the request body did not validate):

```json
{
  "id": "3a6c9e01-5f2b-4d8a-9c47-1e0b7d5a2f83",
  "links": null,
  "code": "BadRequest",
  "title": "Bad Request",
  "detail": "amount: Amount must be greater than 0",
  "source": {
    "pointer": "/amount",
    "parameter": null,
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 401 Unauthorised.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (No usable access token on the request):

```json
{
  "id": "1cecc2b7-1c29-418a-b26a-bf7546926083",
  "links": {
    "about": "https://ozow.stoplight.io/docs/one-api/zi18vomr0jm8c-generate-authentication-token",
    "type": "https://tools.ietf.org/html/rfc7235#section-3.1"
  },
  "code": "Unauthorized",
  "title": "Unauthorized Request",
  "detail": "Authorization header is missing or invalid.",
  "source": {
    "pointer": null,
    "parameter": null,
    "header": "Authorization"
  },
  "meta": null
}
```

### 403 Forbidden.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The token is valid but lacks the scope the operation needs):

```json
{
  "id": "c47a2e08-9b31-4f6d-85a0-7e2c1d9f3b56",
  "links": {
    "about": "https://ozow.stoplight.io/docs/one-api/zi18vomr0jm8c-generate-authentication-token",
    "type": "https://tools.ietf.org/html/rfc7235#section-3.1"
  },
  "code": "Forbidden",
  "title": "Forbidden Request",
  "detail": "Request is forbidden, most likely scope does not match required scope to perform requested action.",
  "source": {
    "pointer": null,
    "parameter": null,
    "header": "Authorization"
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 409 Conflict.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The idempotency key was reused with a different body):

```json
{
  "id": "2f8b6d40-1c7e-49a5-b03f-8d5a2e1c9704",
  "links": null,
  "code": "Conflict",
  "title": "Conflict",
  "detail": "Idempotency key and request data do not match a previous request.",
  "source": {
    "pointer": null,
    "parameter": null,
    "header": "Idempotency-Key"
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```

### 500 Internal Server Error.

- Header `X-Correlation-ID`: The correlation id for the request that was processed.

`Error`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/one-api/schemas/error.md

Example (The request failed on the Ozow side):

```json
{
  "id": "9e0d5a83-6b24-4c19-8f7a-2d1b3e6c0a97",
  "links": null,
  "code": "InternalServerError",
  "title": "Internal Server Error",
  "detail": "Error occurred while processing request.",
  "source": {
    "pointer": "/data",
    "parameter": null,
    "header": null
  },
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```


---

# TransactionCompleteFullData

> A schema in the One API reference. Source: https://hub.ozow.com/api-reference/one-api/schemas/transaction-complete-full-data/

The `data` of a `transaction.complete` delivery whose subscription asked for `full`. It is the field set the Payments API posts to a notify URL, so a handler written for that notification reads this unchanged. Every value is a string, including the amount and the flags.

## Fields

- `SiteCode` (string, required) - The site the transaction was created against.
- `TransactionId` (string, uuid, required) - The transaction identifier.
- `TransactionReference` (string, required) - Your own reference for the transaction.
- `Amount` (string, required) - The amount, to two decimal places, with a full stop as the separator.
- `Status` (string, required, one of "Successful", "Incomplete", "Pending", "Error") - The mapped status, the same four values the `thin` payload carries.
- `Optional1` (string, required) - Your first optional field, empty when it was not set.
- `Optional2` (string, required) - Your second optional field, empty when it was not set.
- `Optional3` (string, required) - Your third optional field, empty when it was not set.
- `Optional4` (string, required) - Your fourth optional field, empty when it was not set.
- `Optional5` (string, required) - Your fifth optional field, empty when it was not set.
- `CurrencyCode` (string, required) - The ISO 4217 currency code.
- `IsTest` (string, required, one of "True", "False") - Whether the transaction was a test one.
- `StatusMessage` (string, required) - The status detail, empty when there is none.
- `Hash` (string, required) - The check hash over the field set, the same one the Payments API notification carries. Verify the Svix signature rather than this: the signature covers the whole delivery.
- `SubStatus` (string) - Present only when the transaction has a sub-status.
- `SubStatusDescription` (string) - Present only when the sub-status has a description.
- `MaskedAccountNumber` (string) - The payer's masked account number. Present only when your site is configured to receive it and the transaction is complete, pending or under investigation.
- `BankName` (string) - The payer's bank. Absent when your site is configured for legacy fields only.
- `SmartIndicators` (string) - The risk indicators. Present only on a complete transaction that has them, and never when your site is configured for legacy fields only.
- `BankId` (string) - Present only on a live transaction when your site is configured to receive banking details.
- `AccountNumber` (string) - Present only on a live transaction when your site is configured to receive banking details.
- `PublicRecipientName` (string) - Present only on a live transaction when your site is configured to receive banking details.


---

# WebhookEnvelope

> A schema in the One API reference. Source: https://hub.ozow.com/api-reference/one-api/schemas/webhook-envelope/

Every delivery has this shape. `data` is what the subscription's message type decides.

## Fields

- `type` (string, required) - The event type the subscription was created for.
- `timestamp` (string, date-time, required) - When the event was raised.
- `data` (any of, required) - What the subscription's message type decides. A `thin` subscription receives `WebhookEventData`, which is every event's default and the only form the subscription events support. A `full` subscription to `transaction.complete` receives `TransactionCompleteFullData`, and to `refund.complete`, `RefundCompleteFullData`. A `full` subscription to any of the subscription events receives nothing at all.
  - Option 1: `WebhookEventData`
  - Option 2: `TransactionCompleteFullData`
  - Option 3: `RefundCompleteFullData`


---

# WebhookEventData

> A schema in the One API reference. Source: https://hub.ozow.com/api-reference/one-api/schemas/webhook-event-data/

The `data` of a delivery whose subscription asked for `thin`, which is every subscription event and the default for the rest.

## Fields

- `id` (string, uuid, required) - What the event is about: the transaction, the refund, or the subscription.
- `status` (string, required) - The outcome. The values differ per event: see the event's own description.
- `reason` (string, nullable) - The status message, when there is one to give.


---

# Webhooks management

> A group of operations in the One API reference. Source: https://hub.ozow.com/api-reference/one-api/tags/webhooks/

Subscribe to events, and replay one your server missed.

## Operations

- `GET /webhooks` - List Webhook Subscriptions
- `POST /webhooks` - Create Webhook Subscription
- `GET /webhooks/{id}` - Get Webhook Subscription
- `PUT /webhooks/{id}` - Update Webhook Subscription
- `DELETE /webhooks/{id}` - Delete Webhook Subscription
- `POST /webhooks/{id}/replay` - Replay Failed Messages
- `GET /webhooks/{id}/secret` - Get Webhook Secret


---

# Transaction completed

> POST to your notification URL
> Sent by Ozow. Part of the One API reference. Source: https://hub.ozow.com/api-reference/one-api/webhooks/transaction-complete/

Raised when a transaction reaches a final state.
`status` is one of `Successful`, `Incomplete`, `Pending` or `Error`. These are not the transaction's own statuses: they are mapped down to four. `Complete` becomes `Successful`, `Created` becomes `Incomplete`, `Pending` and `PendingInvestigation` become `Pending`, and everything else becomes `Error`, which includes a cancelled, abandoned or voided payment. `reason` carries the detail.

A subscription registered as `full` receives `TransactionCompleteFullData` in `data` instead, which is the field set the Payments API posts to a notify URL.

Delivered by Svix, with `svix-id`, `svix-timestamp` and `svix-signature` headers. Verify the signature with the webhook's secret before acting on the contents.

## Authentication

Ozow sends no credential with this call, so this check is the only thing standing between a real delivery and a stranger’s. Verify the delivery signature before acting on the contents: your notification URL is public, and anyone can post to it.

## Payload

**application/json**

- `type` (string, required) - The event type the subscription was created for.
- `timestamp` (string, date-time, required) - When the event was raised.
- `data` (any of, required) - What the subscription's message type decides. A `thin` subscription receives `WebhookEventData`, which is every event's default and the only form the subscription events support. A `full` subscription to `transaction.complete` receives `TransactionCompleteFullData`, and to `refund.complete`, `RefundCompleteFullData`. A `full` subscription to any of the subscription events receives nothing at all.
  - Option 1: `WebhookEventData`
  - Option 2: `TransactionCompleteFullData`
  - Option 3: `RefundCompleteFullData`

## Your response

### 200 Acknowledged. Return this once you have stored the event.

No body.


---

# Get Transaction

> GET `/GetTransaction`
> Part of the Payments API reference. Source: https://hub.ozow.com/api-reference/payments-api/get-get-transaction/

Server: `https://api.ozow.com` (Production)

Other environments: `https://stagingapi.ozow.com` (Staging)

This method is called when you want to query a transaction using Ozow's transaction.

## Authentication

- `ApiKey` (API key in the ApiKey header)
  - The unique API key for the merchant. See [Prerequisites and onboarding](../../getting-started/prerequisites-and-onboarding.md) for where to find it.

## Query parameters

- `siteCode` (string, required) - A unique code for the each of the merchant's sites. A site code is generated when adding a site in the Ozow merchant admin section.
- `transactionId` (string, required) - Ozow's reference for the transaction. This is passed back to the merchant in the redirect and notification responses.

## Request body

## Responses

### 200 Single transaction object

- `transactionId` (string, required, max length 50) - Ozow's unique reference for the transaction.
- `merchantCode` (string, required, max length 50) - Unique code assigned to each merchant.
- `siteCode` (string, required, max length 50) - The site code sent to Ozow in the request post.
- `transactionReference` (string, required, max length 50) - The merchant's transaction reference sent in the request post's TransactionReference variable.
- `currencyCode` (string, required, max length 3) - The transaction currency code sent in the request post.
- `amount` (number, double, required) - The transaction amount. The amount is in the currency specified by the currency code posted.
- `status` (string, required, max length 50) - The transaction status. Possible values are: 1. Complete - The payment was successful. 2. Cancelled - The payment was cancelled. 3. Error - An error occurred while processing the payment. 4. Abandoned – The payment was abandoned. 5. PendingInvestigation – An inconclusive result was received by the bank and the payment needs to be verified manually. 6. Pending – The status cannot be determined as yet but will be reposted to the notification URL as soon as it has been determined. Merchants not using the notification URL will receive a PendingInvestigation status.
- `statusMessage` (string, max length 150) - Message regarding the status of the transaction. This field will not always have a value. This is a user friendly message that can be displayed to the user e.g. User cancelled transaction.
- `createdDate` (string, required) - Transaction created date and time.
- `paymentDate` (string) - Transaction payment date and time.
- `subStatus` (string, max length 50) - The transaction sub status for failed transactions. The value provides an indication as to why the payment failed. Some examples: • Unclassified – Failure scenario has not been mapped • InsufficientFunds - User did not have sufficient funds available to complete the payment While there are several sub-statuses, they have not been included here as they are strictly for reporting.
- `bankName` (string, max length 50) - The name of the bank the payment was made from.
- `maskedAccountNumber` (string, max length 50) - The masked account number the payment was made from. If account number is 12 or more digits then the first and last four digits are unmasked e.g. 1234567898765 will be masked as 1234*****8765 If the account number is less than12 digits then the first and last 3 digits are left unmasked e.g. 123456789 will be masked as 123***789 **This is not available by default and a request by the merchant must be submitted along with a justification for requiring this information.**
- `smartIndicators` (string, max length 500, pattern HIGH_VALUE|FIRST_OZOW) - Some Ozow merchants have requested this information as they use this in their own processes. The can be ignored unless you have a purpose and application for this information. The application of these indicators are for the merchant’s discretion and in isolation do not constitute any action to be taken by the merchant. The field will contain a pipe delimited list of the following values if they are applicable e.g. HIGH_VALUE | FIRST_OZOW : * HIGH_VALUE – If a soft limit is configured on the site and the amount paid is higher or equal to the configured limit * FIRST_OZOW – First time a user has paid using Ozow * FIRST_MERCHANT – First time a user has paid the merchant using Ozow * NEW_OZOW – User has paid using Ozow for the first time in the past seven days * NEW_MERCHANT - User has paid the merchant using Ozow for the first time in the past seven days

### 401 Unauthorized. The `ApiKey` header is missing or does not match the site, or for a `Secure` operation the bearer token is missing, expired or invalid.

string

Example (example 1):

```json
API key is missing or invalid.
```

### 403 Forbidden. The credentials were readable but the site cannot be authorised, because no merchant matches the site code or the merchant is deactivated.

string

Example (example 1):

```json
Merchant for site code TSTSTE0001 is deactivated
```

### 500 Internal Server Error. Something failed on the Ozow side.

string


---

# Get Transaction By Reference

> GET `/GetTransactionByReference`
> Part of the Payments API reference. Source: https://hub.ozow.com/api-reference/payments-api/get-get-transaction-by-reference/

Server: `https://api.ozow.com` (Production)

Other environments: `https://stagingapi.ozow.com` (Staging)

This operation is used  when you want to query transactions using the merchant's transaction reference, specified when creating the payment request. 
This method is able to return multiple results. Ozow does not restrict the merchant from sending duplicate merchant references, though it is advised that a unique reference is sent per transaction. The number of results returned are limited to 10.
Note that the site code must be the same as the one used when the associated payment request was created.

## Authentication

- `ApiKey` (API key in the ApiKey header)
  - The unique API key for the merchant. See [Prerequisites and onboarding](../../getting-started/prerequisites-and-onboarding.md) for where to find it.

## Query parameters

- `siteCode` (string, required) - A unique code for the each of the merchant's sites. A site code is generated when adding a site in the Ozow merchant admin section.
- `transactionReference` (string, required) - The merchant's reference for the transaction.
- `isTest` (boolean) - Defaults to false. Use true only to get results for test requests.

## Request body

This method is called when you want to query transactions using the merchant's reference. This method is able to return multiple results. Ozow does not restrict the merchant from sending duplicate merchant references, though it is advised that a unique reference is sent per transaction. The number of results returned are limited to 10.

## Responses

### 200 Array of TransactionModel

**application/json**

array of TransactionModel

**application/xml**

array of TransactionModel

### 401 Unauthorized. The `ApiKey` header is missing or does not match the site, or for a `Secure` operation the bearer token is missing, expired or invalid.

string

Example (example 1):

```json
API key is missing or invalid.
```

### 403 Forbidden. The credentials were readable but the site cannot be authorised, because no merchant matches the site code or the merchant is deactivated.

string

Example (example 1):

```json
Merchant for site code TSTSTE0001 is deactivated
```

### 500 Internal Server Error. Something failed on the Ozow side.

string


---

# Create Payment Request

> POST `/postpaymentrequest`
> Part of the Payments API reference. Source: https://hub.ozow.com/api-reference/payments-api/post-post-payment-request/

Server: `https://api.ozow.com` (Production)

Other environments: `https://stagingapi.ozow.com` (Staging)

Creates a payment request with the requested parameter set.

## Authentication

- `ApiKey` (API key in the ApiKey header)
  - The unique API key for the merchant. See [Prerequisites and onboarding](../../getting-started/prerequisites-and-onboarding.md) for where to find it.

## Request body

**application/json**

- `siteCode` (string, required, max length 50) - A unique code for the site currently in use. A site code is generated when adding a site in the Ozow merchant admin section.
- `countryCode` (string, required, max length 2, pattern ^[A-Z]+) - The ISO 3166-1 alpha-2 code for the user's country. The country code will determine which banks will be displayed to the customer. Please note only South African (ZA) banks are currently supported by Ozow.
- `currencyCode` (string, required, max length 3, pattern ^[A-Z]+) - The ISO 4217 three-letter code for the transaction currency. Please note only the South African Rand (ZAR) is currently supported by Ozow, so any currency conversion must take place before posting to the Ozow site.
- `amount` (number, double, required) - The transaction amount. The amount is in the currency specified by the currency code posted.
- `transactionReference` (string, required, max length 50) - The merchant's reference for the transaction. This reference can be used to look up the transaction with the `GetTransactionByReference` operation.
- `bankReference` (string, required, max length 20) - The reference that will be pre-populated in the "their reference" field in the customers online banking site. This is the payment reference that appears on the merchant’s bank statement and can be used for recon purposes. Only alphanumeric characters, spaces, and dashes are allowed.
- `optional1` (string, max length 50) - Optional field the merchant can post for additional information they would need passed back in the response. These are also stored with the transaction details by Ozow, and can be useful for filtering transactions in the merchant admin section.
- `optional2` (string, max length 50) - Optional field the merchant can post for additional information they would need passed back in the response. These are also stored with the transaction details by Ozow, and can be useful for filtering transactions in the merchant admin section.
- `optional3` (string, max length 50) - Optional field the merchant can post for additional information they would need passed back in the response. These are also stored with the transaction details by Ozow, and can be useful for filtering transactions in the merchant admin section.
- `optional4` (string, max length 50) - Optional field the merchant can post for additional information they would need passed back in the response. These are also stored with the transaction details by Ozow, and can be useful for filtering transactions in the merchant admin section.
- `optional5` (string, max length 50) - Optional field the merchant can post for additional information they would need passed back in the response. These are also stored with the transaction details by Ozow, and can be useful for filtering transactions in the merchant admin section.
- `customer` (string, max length 100) - The customer’s name or identifier.
- `cancelUrl` (string, uri, max length 150) - The URL to which the redirect result should be posted to if the customer cancels the payment. This is also the page the customer will be redirected to. This URL can also be set for the applicable merchant site in the merchant admin section. If a value is set in the merchant admin and sent in the post, the posted value will be redirected to if the payment is cancelled.
- `errorUrl` (string, uri, max length 150) - The URL to which the redirect result should be posted if an error occurs while trying to process the payment. This is also the page the customer will be redirected to. This URL can also be set for the applicable merchant site in the merchant admin section. If a value is set in the merchant admin and sent in the post, the posted value will be redirected to if an error occurred while processing the payment.
- `successUrl` (string, uri, max length 150) - The URL to which the redirect result should be posted to if the payment is successful. This is also be the page the customer gets redirected to. This URL can also be set for the applicable merchant site in the merchant admin section. If a value is set in the merchant admin and sent in the post, the posted value will be redirected to if the payment was successful. Please note that it is not sufficient to assume that the payment was successful simply because the customer has been redirected back to this page. It is highly recommended that you check the response fields as well as the transaction status using our check transaction status API call.
- `notifyUrl` (string, uri, max length 150) - The URL that the notification result should be posted to. The result will post regardless of the outcome of the transaction. This URL can also be set for the applicable merchant site in the merchant admin section. If a value is set in the merchant admin and sent in the post, the notification result will be sent to the posted value. Find out more in the notification response section in step 2.
- `isTest` (boolean, required) - Accepted values are true or false. Send true to test your request posting and response handling. If set to true you will be redirected to select whether you would like a successful or unsuccessful redirect response sent back. Please note that notification responses are sent for test transactions and the online banking payment is skipped.
- `selectedBankId` (string, uuid) - If the 'SelectedBankId' field is populated by the Merchant, the Customer will be redirected to the Ozow login page of the selected bank. However, if the field is left empty, the Customer will be presented with Ozow bank selection screen. See [Payment method identifiers](../../integration-methods/apis/payin/payment-method-ids.md) for the value to send.
- `bankAccountNumber` (string, max length 20) - The bank account number the payment should be made to.
- `branchCode` (string, max length 10) - The branch code for the bank account.
- `bankAccountName` (string, max length 50, pattern ^[a-zA-Z0-9\s]+$) - The name of the beneficiary account the payment is made into. Letters, digits and spaces only. Required, along with `bankAccountNumber`, `branchCode` and `bankId`, whenever any one of them is sent.
- `payeeDisplayName` (string, max length 50) - The name shown on the site as the entity being paid (not in banking screens).
- `expiryDateUtc` (string, max length 19) - Payment will not be allowed to be made after this date. Date should be UTC and value should be formatted as yyyy-MM-dd HH:mm
- `allowVariableAmount` (boolean) - Allows the user to change the amount passed through before paying. This option must also be enabled for the site in the merchant admin portal to be used. Accepted values are true or false. DO NOT include false in the hash check string, just ignore instead.
- `variableAmountMin` (number, double) - If AllowVariableAmount is passed through as true, this will dictate the lowest acceptable amount the user can enter.
- `variableAmountMax` (number, double) - If AllowVariableAmount is passed through as true, this will dictate the highest acceptable amount the user can enter.
- `customerIdentifier` (string, max length 13) - Merchants classified as high-risk must provide a valid South African identity number. It's important to note that this is an optional field for all other merchants. Capitec Pay is the bank this most often applies to; see [Payment method identifiers](../../integration-methods/apis/payin/payment-method-ids.md) for what needs approval before you build against it, and reach out to [support@ozow.com](mailto:support@ozow.com) for whether your account is classified this way.
- `customerCellphoneNumber` (string, max length 10, pattern ^[0-9]+) - Merchant can provide customer cellphone number for faster login on certain banks. DO NOT include in the hash check string, just ignore instead.
- `hashCheck` (string, required, max length 250) - SHA512 hash used to ensure that certain fields in the message have not been altered after the hash was generated. See [Generate the hash check](../../integration-methods/apis/deprecated-integrations/redirect-to-ozow.md#step-1-generate-the-hash-check) for the field order and a worked example.

**application/xml**

`PaymentRequest`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/payments-api/schemas/payment-request.md

## Responses

### 200 OK. The request reached the API, which is not the same as it being accepted. A rejected request is also a 200, with the reason in `errorMessage` and no `url`. Check that field, not the status code.

**application/json**

- `paymentRequestId` (string, uuid, required, max length 50) - Ozow's unique identifier for the payment request.
- `url` (string, uri, required, max length 100) - Generated URL that allows payment for the request used to create the payment. You will need to redirect the payer to this URL, who upon completion of the payment will be redirected back to your site. **The payment Url you'll receive from the API is dynamic. Please do not hard code it into your integrations as it might change.**
- `errorMessage` (string, max length 50) - Error message generated when validating the request.

**application/xml**

`PaymentRequestResult`, the same schema listed in full earlier in this document. Its own page: https://hub.ozow.com/api-reference/payments-api/schemas/payment-request-result.md

### 401 Unauthorized. The `ApiKey` header is missing or does not match the site, or for a `Secure` operation the bearer token is missing, expired or invalid.

string

Example (example 1):

```json
API key is missing or invalid.
```

### 403 Forbidden. The credentials were readable but the site cannot be authorised, because no merchant matches the site code or the merchant is deactivated.

string

Example (example 1):

```json
Merchant for site code TSTSTE0001 is deactivated
```

### 500 Internal Server Error. Something failed on the Ozow side.

string


---

# Transaction notification

> POST to your notification URL
> Sent by Ozow. Part of the Payments API reference. Source: https://hub.ozow.com/api-reference/payments-api/webhooks/transaction-notification/

Sent to the notification URL once a transaction reaches a final status.

The URL comes from the `NotifyUrl` field on the payment request, or from the site configuration in the merchant admin site. Without one, no notification is sent.

Verify the `Hash` field before acting on the contents. A notification is an unauthenticated POST to a URL that anyone can call.

## Authentication

Ozow sends no credential with this call, so this check is the only thing standing between a real delivery and a stranger’s. Verify the Hash field before acting on the contents: your notification URL is public, and anyone can post to it.

## Payload

**application/x-www-form-urlencoded**

- `SiteCode` (string, required, max length 50) - The site code sent to Ozow in the request post.
- `TransactionId` (string, uuid, required, max length 50) - The transaction identifier generated by Ozow.
- `TransactionReference` (string, required, max length 50) - The merchant's transaction reference sent in the request post's TransactionReference variable.
- `Amount` (number, double, required) - The transaction amount, always written with two decimal places. That is the form the hash is built from, so use the value exactly as it was posted.
- `Status` (string, required, max length 50) - The transaction status. Possible values are: 1. Complete - The payment was successful. 2. Cancelled - The payment was cancelled. 3. Error - An error occurred while processing the payment. 4. Abandoned – The payment was abandoned. 5. PendingInvestigation – An inconclusive result was received by the bank and the payment needs to be verified manually. 6. Pending – The status cannot be determined as yet but will be reposted to the notification URL as soon as it has been determined. Merchants not using the notification URL will receive a PendingInvestigation status.
- `Optional1` (string, max length 50) - Optional fields sent in the request post.
- `Optional2` (string, max length 50) - Optional fields sent in the request post.
- `Optional3` (string, max length 50) - Optional fields sent in the request post.
- `Optional4` (string, max length 50) - Optional fields sent in the request post.
- `Optional5` (string, max length 50) - Optional fields sent in the request post.
- `CurrencyCode` (string, required, max length 3, pattern ^[A-Z]+) - The transaction currency code sent in the request post.
- `IsTest` (string, max length 5) - Whether the transaction was a test transaction, sent as `True` or `False`. Part of the hash, so use the value exactly as it was posted.
- `StatusMessage` (string, max length 500) - A message about the status, empty for most transactions. Part of the hash, so an empty value still counts as a field and contributes an empty string.
- `Hash` (string, required, max length 128) - SHA512 hash used to ensure that certain fields in the message have not been altered after the hash was generated. See the generate hash section for more details on how to validate the response variables using the hash.
- `SubStatus` (string, max length 50) - The transaction sub status for failed transactions. The value provides an indication as to why the payment failed. Some examples: • Unclassified – Failure scenario has not been mapped • InsufficientFunds - User did not have sufficient funds available to complete the payment While there are several sub-statuses, they have not been included here as they are strictly for reporting.
- `MaskedAccountNumber` (string, max length 50) - The masked account number the payment was made from. If account number is 12 or more digits then the first and last four digits are unmasked e.g. 1234567898765 will be masked as 1234*****8765 If the account number is less than12 digits then the first and last 3 digits are left unmasked e.g. 123456789 will be masked as 123***789 **This is not available by default and a request by the merchant must be submitted along with a justification for requiring this information.**
- `BankName` (string, max length 50) - The name of the bank the payment was made from.
- `SmartIndicators` (string, max length 500) - Some Ozow merchants have requested this information as they use this in their own processes. The can be ignored unless you have a purpose and application for this information. The application of these indicators are for the merchant’s discretion and in isolation do not constitute any action to be taken by the merchant. The field will contain a pipe delimited list of the following values if they are applicable e.g. HIGH_VALUE | FIRST_OZOW : * HIGH_VALUE – If a soft limit is configured on the site and the amount paid is higher or equal to the configured limit * FIRST_OZOW – First time a user has paid using Ozow * FIRST_MERCHANT – First time a user has paid the merchant using Ozow * NEW_OZOW – User has paid using Ozow for the first time in the past seven days * NEW_MERCHANT - User has paid the merchant using Ozow for the first time in the past seven days

## Your response

### 200 Acknowledged. Return this once you have stored the notification.

No body.
