# Take a recurring payment

> Everything needed to collect from a customer on a schedule with One API, from the consent the customer gives once through to each collection and the webhook that reports it.

A recurring payment is two separate things: consent, which the customer gives
once, and collection, which you trigger for each amount afterwards. They are
different calls and they fail for different reasons.

**Capitec is the only supported bank today**, on a limited rollout to approved
merchants. A customer who banks elsewhere needs a standard payment each cycle,
so that fallback is part of the design rather than an afterthought.

Four steps: create the subscription, redirect the customer to consent, action a
payment against that consent, handle the webhook. **A collection is not
confirmed by the response to the call that triggered it.** The outcome arrives
on the webhook, exactly as it does for a one-off payin.

Subscription statuses are their own set, separate from a collection's. A
subscription sits at `PendingAuthorization` until the customer approves consent
in their banking app, and only an `Active` one can be collected against; an
individual collection carries `Pending`, `Successful` or `Failed` instead. Copy
every name as written, including `Canceled`, which has one `l`.

## 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
- Build against `https://one.ozow.com/v1` for `one-api`
- 7 pages, 16 operations, inlined in full below
- The same package as links: https://hub.ozow.com/bundles/take-a-recurring-payment.md

---

# Implement against these

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

---

# Recurring payments

> Collect repeat payments with One API. One consent the customer approves, then merchant-initiated collections that need no further customer action.

Source: https://hub.ozow.com/integration-methods/apis/recurring-payments/set-up-recurring-payments/

This guide walks you through integrating recurring payments via One API. Recurring payments follow a
strict two-step process, a consent request that the customer approves once, followed by
merchant-initiated collections that happen silently without any further customer action.

> ℹ️ This guide uses the **One API**, Ozow's recommended API for new integrations. Recurring
> payments are not available on the older Payments API.

> ⚠️ **Limited availability**: Recurring payments are available to approved merchants only. Contact
> your account manager or [support@ozow.com](mailto:support@ozow.com) to enquire about eligibility
> and onboarding.

## Before you start

- Recurring payments must be enabled on your merchant profile by Ozow: contact your account manager
  to confirm this before integrating
- Your One API client must have the `subscriptions` scope, when creating your API client on the Ozow
  Dashboard, ensure this scope is included. Without it, all subscription endpoints will return an
  authorisation error
- You have completed standard One API authentication setup: see [Redirect: One
  API](https://hub.ozow.com/integration-methods/apis/payin/redirect-to-ozow.md) for the token flow
- If your business operates in a high-risk industry, Customer Identity Verification must be built
  into the consent step: see [Customer Identity
  Verification](https://hub.ozow.com/integration-methods/apis/payin/identity-verification.md)

> ℹ️ **Redirect only**: The consent step uses a redirect flow. Embedded integration is not supported
> for recurring payments. Your customer must be redirected to the Ozow-hosted consent page to
> approve the subscription.

## Environments

| Environment | Base URL | Dashboard |
|---|---|---|
| Production | `https://one.ozow.com/v1` | [dash.ozow.com](https://dash.ozow.com) |
| Staging | `https://stagingone.ozow.com/v1` | [stagingdash.ozow.com](https://stagingdash.ozow.com) |

## How it works

```mermaid
sequenceDiagram
    participant C as Customer
    participant M as Your system
    participant O as One API
    participant P as Capitec

    M->>O: POST /v1/subscriptions (consent request)
    O-->>M: Returns subscriptionId + redirectUrl
    M->>C: Redirects customer to redirectUrl
    C->>P: Approves consent in Capitec app (3 min window)
    P-->>O: Confirms consent approval
    O-->>M: Subscription status → Active
    M->>O: POST /subscriptions/{id}/transactions
    O->>P: Instructs debit
    P-->>O: Confirms collection
    O-->>M: Sends webhook notification
```

---

## Core integration

### Step 1: Create a subscription (consent request)

Create a subscription to initiate the consent process. The customer must approve the consent in
their Capitec app before any collections can be made.

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

**Request example**

```json
{
  "siteCode": "YOUR_SITE_CODE",
  "paymentMethod": "capitec",
  "amount": {
    "currency": "ZAR",
    "value": 500.00
  },
  "amountConstraints": {
    "min": { "currency": "ZAR", "value": 500 },
    "max": { "currency": "ZAR", "value": 600 }
  },
  "description": "Meal kit",
  "merchantReference": "SUB-001",
  "bankReference": "MEALKIT",
  "frequency": "Monthly",
  "firstPaymentDate": "2026-09-01",
  "occurrences": 12
}
```

**Key request fields**

| Field | Type | Required | Description |
|---|---|---|---|
| `siteCode` | string | Yes | Your Ozow site code |
| `paymentMethod` | string | Recommended | Must be `capitec`. Always specify this explicitly, when card support is added in future, omitting this field will show a payment method selection screen to the customer |
| `amount.currency` | string | Yes | Must be `ZAR` |
| `amount.value` | number | Yes | The recurring collection amount, at most 2 decimal places. Must fall inside the `amountConstraints` band |
| `amountConstraints.min` | object | Yes | Lower bound the customer authorises, as `currency` and `value`. Whole Rand, no cents |
| `amountConstraints.max` | object | Yes | Upper bound the customer authorises, as `currency` and `value`. Whole Rand, no cents, at most 100000, and greater than `min` |
| `description` | string | Yes | What the customer is subscribing to, shown to them. At most 20 characters |
| `merchantReference` | string | Yes | Unique reference per subscription used for reconciliation. At most 50 characters, no spaces |
| `bankReference` | string | Yes | The reference that appears on your bank statement for each payment. At most 20 characters, letters and numbers only |
| `frequency` | string | Yes | Collection cadence, `Daily`, `Weekly`, `Fortnightly`, `Monthly`, `Biannually`, or `Annually` |
| `firstPaymentDate` | string | Yes | Date from which collections may begin, today or later. Collections cannot be actioned before this date |
| `occurrences` | number | Yes | Total number of collections, 1 to 120. For indefinite subscriptions use 120 and create a new subscription when the limit is reached |
| `payableNow` | object | No | An immediate first charge, as `amount` and `date`. Not bound by `amountConstraints` |
| `identity` | object | No | Customer identity as `type`, `country`, and `identifier`. Required for high-risk industries: see [Customer Identity Verification](https://hub.ozow.com/integration-methods/apis/payin/identity-verification.md) |

For the full request schema see [One API reference](https://hub.ozow.com/api-reference/one-api.md).

**Successful response**

```json
{
  "subscriptionId": "00000000-0000-0000-0000-000000000000",
  "status": "PendingAuthorization",
  "redirectUrl": "https://pay.ozow.com/subscriptions/00000000-0000-0000-0000-000000000000",
  "links": {
    "self": "https://one.ozow.com/v1/subscriptions/00000000-0000-0000-0000-000000000000",
    "cancel": "https://one.ozow.com/v1/subscriptions/00000000-0000-0000-0000-000000000000",
    "transactions": "https://one.ozow.com/v1/subscriptions/00000000-0000-0000-0000-000000000000/transactions"
  }
}
```

Store the `subscriptionId`, you will need it for all subsequent calls.

---

### Step 2: Redirect the customer for consent

Redirect your customer's browser to the `redirectUrl` returned in the response. The customer will:

1. Land on the Ozow payment page
2. Enter their Capitec-linked cell phone number
3. Approve the consent request in their Capitec app

> ⚠️ **3-minute window**: Capitec allows 3 minutes for the customer to approve the consent request
> in their Capitec app. If the customer does not approve within this window, the consent request
> lapses and the subscription status changes to `Expired`. You must create a new subscription and
> redirect the customer through the consent flow again.

Once the customer approves, Capitec notifies Ozow and the subscription status updates from
`PendingAuthorization` to `Active`. Only then can collections be actioned.

> ℹ️ **Customer Identity Verification**: If your business operates in a high-risk industry, the
> Customer Identity Verification check must be included in this consent step; not in the action
> payment step. See [Customer Identity Verification](https://hub.ozow.com/integration-methods/apis/payin/identity-verification.md) for
> requirements.

---

### Step 3: Action a payment (trigger collection)

Once the subscription is `Active` and the `firstPaymentDate` has been reached, trigger a collection
by calling the transaction endpoint.

```endpoint
POST https://one.ozow.com/v1/subscriptions/{subscriptionId}/transactions
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
```

Replace `{subscriptionId}` with the ID returned in Step 1.

**Request example**

```json
{
  "amount": { "currency": "ZAR", "value": 500.00 },
  "merchantReference": "SUB-001-SEP"
}
```

Both fields are required. `amount.value` must fall inside the `amountConstraints` band the customer
authorised. `merchantReference` identifies this individual collection, not the subscription itself.

> ⚠️ **Pre-conditions**: Ozow validates all of the following before processing the collection. The
> call will be rejected if any condition is not met:
>
> - The subscription exists and has `Active` status
> - Today's date is on or after the `firstPaymentDate` specified during consent creation
> - The number of collections has not exceeded the `occurrences` limit

The customer does not receive any authentication prompt, the debit happens silently based on the
consent already granted.

---

### Step 4: Handle the webhook notification

Ozow sends webhook notifications for subscription events. Subscribe to these events in your One API
client webhook configuration:

| Event | Description |
|---|---|
| [`subscription.authorization.success`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-authorization-success.md) | The customer approved the consent, and the subscription can be collected against |
| [`subscription.authorization.failed`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-authorization-failed.md) | The consent was not approved |
| [`subscription.transaction.success`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-transaction-success.md) | An individual collection succeeded |
| [`subscription.transaction.failed`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-transaction-failed.md) | An individual collection failed |
| [`subscription.completed`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-completed.md) | The subscription took all its scheduled occurrences |
| [`subscription.canceled`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-canceled.md) | The subscription was cancelled. One `l`, unlike `Canceled` on the status |
| [`subscription.expired`](https://hub.ozow.com/api-reference/one-api/webhooks/subscription-expired.md) | The authorisation lapsed before the subscription became active |

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.

> ⚠️ **Important**: A subscription webhook is delivered as `thin` whatever message type you register
> it with, so `data` is the [`id`, `status` and
> `reason`](https://hub.ozow.com/api-reference/one-api/schemas/webhook-event-data.md) and nothing more. Fetch the subscription or the
> transaction for detail.

Handle these exactly as described in [Step 4: Handle the webhook
notification](https://hub.ozow.com/integration-methods/apis/payin/redirect-to-ozow.md#step-4-handle-the-webhook-notification) in the One API
redirect guide, the same Svix signature verification process applies.

---

## Subscription management

| Action | Endpoint | Description |
|---|---|---|
| Get subscription | `GET /subscriptions/{id}` | Retrieve details of a specific subscription |
| List subscriptions | `GET /subscriptions?siteCode=&status=&limit=&offset=` | List subscriptions for a site, filterable by status. `siteCode` is required |
| Cancel subscription | `POST /subscriptions/{id}/cancel` | Cancel an active subscription, communicates directly with Capitec to cancel on their side |
| List transactions | `GET /subscriptions/{id}/transactions` | List all collections made under a specific subscription |

> ⚠️ **Cancellation**: Cancelling a subscription communicates directly with Capitec and cancels the
> consent on their side. This action cannot be undone. No further collections will be possible after
> cancellation.

---

## Subscription statuses

| Status | Description |
|---|---|
| `PendingAuthorization` | Subscription created, customer has not yet approved consent in the Capitec app |
| `Active` | Customer has approved consent, collections can now be actioned |
| `Canceled` | Subscription cancelled, no further collections possible |
| `Completed` | All scheduled occurrences have been collected |
| `Expired` | The authorisation lapsed before the subscription became active, create a new subscription |
| `Failed` | The subscription could not be established or sustained |
| `Unknown` | The status could not be determined |

> ℹ️ **Note**: `Canceled` uses a single `l`. An individual collection carries its own status,
> `Pending`, `Successful`, or `Failed`.

---

## Go-live checklist

- [ ] Recurring payments have been enabled on your merchant profile by Ozow
- [ ] One API client has the `subscriptions` scope
- [ ] `paymentMethod` is set to `capitec` in the subscription payload
- [ ] `firstPaymentDate` is set to a valid present or future date
- [ ] `merchantReference` is unique per subscription with no spaces
- [ ] Customer Identity Verification is included in the consent step if required for your industry
- [ ] Redirect flow correctly sends customers to the Capitec consent page
- [ ] Webhook handlers are configured for the consent and collection events you rely on, spelled as
      the service spells them
- [ ] Your system handles the `Expired` status and can create a new subscription when needed
- [ ] Staging testing completed end to end before production go-live

---

## Next steps

- Review the [Building a secure
  integration](https://hub.ozow.com/getting-started/building-a-secure-integration.md) checklist before going
  live
- See the [One API reference](https://hub.ozow.com/api-reference/one-api.md) for the full subscription endpoint specifications
- Contact [support@ozow.com](mailto:support@ozow.com) to confirm recurring payments are enabled on
  your merchant profile

---

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

---

# How Ozow works

> How Ozow connects you to South African banks and payment methods, and the two directions money moves: payins from customers, payouts to recipients.

Source: https://hub.ozow.com/getting-started/

Ozow is a payment infrastructure layer that connects merchants to multiple payment methods and
banking rails. Instead of building separate integrations for each bank or payment method, you
integrate with Ozow to gain access to the full suite of Ozow payment products.

## The two directions of money movement

Every Ozow integration moves money in one of two directions.

**Payin**: a customer pays you. The customer initiates the payment, Ozow processes it, and you
receive the funds. This covers checkout and any other payment collection.

**Payout**: you send funds to a recipient. Your system initiates the transfer, Ozow processes it,
and the recipient receives the funds in their bank account. This covers disbursements, refunds to
bank accounts, and bulk payments.

The distinction runs through everything: different APIs, different credentials, different approval
processes, and a different structure in these docs. Work out which direction you need before you
start. The [Integration methods](https://hub.ozow.com/integration-methods.md) section is
organised around it.

## One integration, every way to pay

Payment methods are enabled on your Ozow account, not in your code.

Pay by Bank is enabled by default. Other methods you opt into (card, PayShap Request, voucher, buy
now pay later, crypto) are enabled by Ozow on your account, and they then appear on the Ozow payment
page automatically. You don't build a new integration or call a different endpoint for each one.

This means you can go live with Pay by Bank and add methods later as a commercial decision rather
than a development project.

## The core payment flows

### Payin

```mermaid
sequenceDiagram
    participant C as Customer
    participant M as Your system
    participant O as Ozow

    C->>M: Reaches checkout
    M->>O: Creates payment request
    O-->>M: Returns payment URL
    M->>C: Sends customer to Ozow
    C->>O: Completes payment
    O-->>M: Notifies your webhook of the outcome
    M->>O: Verifies the status
    M-->>C: Updates the order
```

Two things to notice. The payment request is created by **your server**, never by the customer's
browser. And the outcome arrives on **your webhook**, not in the customer's redirect back to your
site; the customer landing on your success page is not proof of payment. See [Building a secure
integration](https://hub.ozow.com/getting-started/building-a-secure-integration.md).

### Payout

There is no customer-facing step. The whole flow happens between your backend and Ozow.

```mermaid
sequenceDiagram
    participant M as Your system
    participant O as Ozow
    participant R as Recipient bank

    M->>O: Checks payout availability
    M->>O: Sends payout request
    O->>M: Calls your verification webhook
    M-->>O: Confirms the payout
    O->>R: Submits the payout to the bank
    R-->>O: Confirms the outcome
    O-->>M: Notifies your webhook of the final status
```

Before Ozow moves any money, it calls back to your system to confirm the payout is genuine.
If that call fails or can't be reached, the payout does not proceed. That's deliberate, and
it's why payout integrations require testing and sign-off before they go live.

## Getting paid: transactions and settlements

A completed transaction is not money in your bank account. These are two separate stages with two
separate status vocabularies.

```mermaid
flowchart LR
    A["Customer pays"] --> B["Transaction completes"]
    B --> C["Included in a settlement"]
    C --> D["Funds in your bank account"]
```

The transaction status tells you whether the customer's payment succeeded. The settlement status
tells you whether the money has actually reached you. Settlement happens on a delay that depends on
the payment method.

Use transaction status to fulfil orders. Use settlement status to reconcile your bank account. See
[Transaction and settlement statuses](https://hub.ozow.com/integration-methods/statuses.md).

## Paying out: your float

Money leaving Ozow doesn't come out of your incoming payments. It comes from a **float**: a balance
you pre-fund by transferring money to Ozow.

Both payouts and refunds draw on the float. If it's empty, they won't process. Payins don't need a
float at all, so if you're only collecting payments you can ignore this entirely.

See [Float top-up](https://hub.ozow.com/payment-products/settlements-and-float/float-top-up.md).

## Environments

Ozow provides separate staging and production environments. They're completely isolated, and staging
credentials are different from your production credentials.

Testing requirements differ by direction. Payin integrations can go straight to production. We
recommend working through the [payin test
cases](https://hub.ozow.com/integration-methods/testing/payin-test-cases-one-api.md), but you don't need to submit anything.
Payout integrations require mandatory staging testing and formal sign-off from Ozow before they're
enabled in production.

**Getting your credentials:**

- **Production credentials** are available to you directly in the [Ozow
  Dashboard](https://dash.ozow.com). Ozow will never send them to you.
- **Staging credentials** are issued on request. Ask your account manager or contact [support@ozow.com](mailto:support@ozow.com).

> 🚨 Ozow will never share your production credentials with you, and will never ask you for them. If
> anyone contacts you offering to send production credentials, or asking you to share yours, treat
> it as fraudulent and report it to [support@ozow.com](mailto:support@ozow.com).

## Where to go next

How you integrate depends on how much control you want over the payment experience and how much you
want to build, from no-code payment requests through to a full API integration.

Head to [Integration methods: overview](https://hub.ozow.com/integration-methods.md) to choose
the right path.

---

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

---

# Recurring payments

> Charge a customer repeatedly after a single authorisation.

Source: https://hub.ozow.com/payment-products/recurring-payments/

> ⚠️ **Limited availability, and by approval only.** Recurring payments are available on Capitec
> accounts, to merchants Ozow has approved for a specific use case. Not all use cases are
> authorised. Speak to your account manager before planning around them.

Recurring payments let a customer authorise you once, and then you charge them repeatedly without
sending them back to their bank each time.

That's the difference from a standard Ozow payment. Normally the customer authorises every
transaction. With recurring payments they give consent up front, including the limits they're
comfortable with, and after that you initiate the payments yourself, within those limits.

It's built for anything you bill more than once and usage-based billing where the amount changes
each time.

## What's available today

| | Status |
|---|---|
| Capitec | Available to approved merchants, in a limited rollout |
| Other methods | Not yet available |

Your customer needs to bank with a supported bank. If they don't, recurring payments aren't an
option for them and you'll need to fall back to a standard payment each cycle.

## How a recurring payment works

**Setting it up, once:**

1. You send the customer to Ozow to set up the agreement.
2. They authorise it with their bank, agreeing to the limits; how much you can take and how often.
3. Ozow confirms the agreement is active.

**Then, for each payment:**

1. You tell Ozow to collect, for an amount within the agreed limits.
2. Ozow collects from the customer's account. They don't need to do anything.
3. Ozow notifies your system of the outcome.

## Enabling recurring payments

Recurring payments require explicit approval from Ozow. They aren't a self-service opt-in, and **not
every use case is authorised**: approval depends on what you're billing for and how you're billing
for it, not just on whether the rollout has reached your customers' bank.

Speak to your account manager before you scope any work, and don't build against recurring payments
on the assumption you'll be approved.

You'll also need to be integrated for pay-ins already. The agreement is set up through a redirect to
Ozow, so if you're using an embedded checkout you'll need a redirect flow for this part.

## Things to know

**The customer sets the limits, and they can change their mind.** The agreement is between your
customer and their bank, and they can cancel it there at any time without telling you. Build for
that: a collection can fail because the agreement is gone, not because the money isn't there. Handle
the two differently; one is worth retrying, the other needs you to ask the customer to set up a new
agreement.

**Payments outside the agreed limits will fail.** If you need to collect more than the agreement
allows, or more often, that's a new agreement rather than a bigger request.

**This is not a debit order.** It doesn't run on the debit order system and it doesn't behave like
one. The customer authorises it directly with their bank and controls it from there.

**A failed collection isn't necessarily a lost customer.** Insufficient funds is the most common
reason and it's usually temporary. Decide your retry policy up front, how many attempts, how far
apart, and when you tell the customer.

**Your customer's bank determines availability, not you.** Check whether recurring payments are
possible for a given customer before you offer them a subscription, rather than after.

## Integrating recurring payments

The integration covers setting up the agreement, collecting payments against it, handling the
webhooks, and managing or cancelling agreements.

→ [Set up recurring payments](https://hub.ozow.com/integration-methods/apis/recurring-payments/set-up-recurring-payments.md)

**Status handling**: see [Transaction and settlement statuses](https://hub.ozow.com/integration-methods/statuses.md).

---

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

---

# Building a secure integration

> Where Ozow's security responsibility ends and yours begins: credentials, webhook endpoints, verifying notifications, and validating amounts.

Source: https://hub.ozow.com/getting-started/building-a-secure-integration/

Security is a shared responsibility between Ozow and you as the merchant. Understanding where Ozow's
responsibility ends and yours begins is essential to building an integration that is safe for your
customers and your business.

## The shared security model

Ozow secures the payment infrastructure. You secure how you integrate with it.

| Ozow is responsible for | You are responsible for |
|---|---|
| The security of the payment processing infrastructure | How you store and handle your API credentials |
| Encryption of payment data in transit and at rest within Ozow systems | The security of your callback and webhook endpoints |
| The integrity and availability of Ozow APIs | Verifying that notifications genuinely came from Ozow |
| Fraud monitoring within the Ozow platform | Validating transaction details before crediting orders |
| Physical and network security of Ozow's environments | Access controls on your systems and Ozow Dashboard |
| Compliance with applicable payment regulations on Ozow's side | Monitoring your own integration for anomalous activity |

A secure Ozow integration is not only about calling the right endpoints, it is about what happens on
your side of the connection too.

## Your security responsibilities

### Credentials and secrets

Your API credentials are the keys to your Ozow integration. If they are compromised, an attacker
could initiate payments or payouts on your behalf.

- Store all Ozow credentials, API keys, private keys, Client IDs, Client Secrets, and Payout API
  keys; in a secrets manager or environment variables. Never hardcode them or commit them to source
  control.
- Keep test and production credentials in strictly separate environments. Never use production
  credentials in a development or staging environment.
- Restrict access to production credentials to a named list of people and services. Access must be least-privilege.
- Have a documented process and a named owner for rotating credentials. Know what you would do if a
  key were compromised.

### Callback and webhook endpoint security

Ozow communicates payment outcomes by sending notifications to a URL you specify. This endpoint is a
critical part of your integration.

- Your callback and webhook URLs must be HTTPS only, using TLS 1.2 or later.
- Your endpoint must not expose stack traces, internal errors, or verbose logs in its response to callers.

### Verifying notifications

Receiving a notification is not the same as trusting it. You must verify that every notification
genuinely came from Ozow before acting on it.

- For Payments API integrations: verify every incoming notification using the hash check before
  updating any order status.
- For One API integrations: validate the message signature on every incoming webhook before acting
  on it.
- Log and alert on verification failures rather than silently discarding them. A pattern of
  verification failures is a signal worth investigating.
- Never mark a payment as complete based on the browser redirect alone. Always confirm status via
  the API or a verified webhook notification.
- Implement replay protection so that a previously processed transaction reference cannot be
  reprocessed to double-credit an order.

> ⚠️ **Important**: Ozow may occasionally send duplicate notifications for the same transaction.
> Your system must handle this gracefully, processing the same transaction twice must not result in
> double-crediting an order.

### Transaction integrity

Before crediting an order, validate that the payment details match what you originally requested.

- Verify that the amount, currency, and merchant reference in the notification match your original
  payment request.
- Handle duplicate notifications idempotently, receiving the same notification twice must have no
  additional effect.
- Periodically reconcile your order records against Ozow's transaction records rather than relying
  solely on webhook delivery.

### Payout-specific responsibilities

Payouts carry additional security requirements because they involve outgoing funds.

**Authorisation**

- For bulk payouts: Ozow recommends that the person who requests a bulk payout is different from the
  person who approves it. Ozow does not enforce this.
- For API payouts: access to the systems, credentials, and code that can trigger a payout must be
  restricted to a named list of people, with any changes requiring review.
- Your system must enforce a business-level authorisation step before calling Ozow's payout API.
  Being authenticated is not sufficient, there must be a deliberate approval within your own system
  before a payout is initiated.

**Beneficiary handling**

- Verify beneficiary bank details before the first payout to any new beneficiary.
- If you are not using stored beneficiary profiles, validate destination bank details on every
  payout request.
- Any change to stored beneficiary details must trigger a mandatory review or re-verification step
  before the next payout.
- Generate and persist a unique encryption key per payout request. Never reuse an encryption key
  across multiple payout requests.
- Enforce velocity and amount limits on payouts.
- Implement real-time alerting for anomalous payout activity, unusual amounts, unfamiliar
  beneficiaries, or off-hours activity are all signals worth acting on immediately.

**Verification request handling**

- Validate the access token on all incoming payout verification requests.
- Verify the hash on every verification request to confirm it genuinely originated from Ozow.
- Validate that the payout details in the verification request match a payout your system actually
  initiated: do not return a decryption key based on token and hash checks alone without confirming
  the payout is expected.

**Payout status verification**

- Confirm payout completion via the API, rather than assuming completion from the initial payout response.
- Verify the hash on every incoming payout status notification before trusting it.

### Access and monitoring

- Apply least-privilege access for all roles with access to your Ozow merchant Dashboard.
- Monitor for abnormal patterns in your payin traffic, spikes in failed verifications or unusual
  volumes are worth investigating.
- Maintain an immutable audit trail of who requested and who approved every payout, and when.
- Reconcile your internal ledger against Ozow's payout records on a regular cadence.

## Quick reference checklist

Use this checklist before going live with any Ozow integration.

### Payin integrations

- [ ] API credentials are stored securely and never hardcoded or committed to source control
- [ ] Test and production credentials are in strictly separate environments
- [ ] Production credentials are restricted to a named list of people and services
- [ ] Credential rotation process is documented with a named owner
- [ ] Callback URL is HTTPS only with TLS 1.2 or later
- [ ] Callback endpoint does not expose internal errors or stack traces
- [ ] Every notification is verified using hash check (Payments API) or message signature (One API)
  before being trusted
- [ ] Verification failures are logged and alerted on
- [ ] Payment status is confirmed via API, not the browser redirect alone
- [ ] Replay protection is in place for transaction references
- [ ] Amount, currency, and merchant reference are validated against the original request before crediting
- [ ] Duplicate notifications are handled idempotently
- [ ] Order records are periodically reconciled against Ozow transaction records
- [ ] Dashboard access follows least-privilege
- [ ] Monitoring is in place for anomalous payin traffic

### Payout integrations

- [ ] Payout API key is stored securely and never hardcoded or committed to source control
- [ ] Test and production payout credentials are in strictly separate environments
- [ ] Access to payout-triggering systems and code is restricted to a named list
- [ ] Bulk payout requestor and approver are different people (recommended, not enforced by Ozow)
- [ ] Business-level authorisation step is enforced before calling the payout API
- [ ] Beneficiary bank details are verified before the first payout to any new beneficiary
- [ ] A unique encryption key is generated and persisted per payout request
- [ ] Velocity and amount limits are enforced on payouts
- [ ] Real-time alerting is in place for anomalous payout activity
- [ ] Incoming verification requests are validated on token, hash, and expected payout details
- [ ] Payout completion is confirmed via API, not assumed from the initial response
- [ ] Payout status notifications are verified by hash before being trusted
- [ ] An immutable audit trail exists for every payout
- [ ] Internal ledger is reconciled against Ozow payout records regularly

---

# The contract

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

---

# Get Subscription by Id

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

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

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

Retrieve the details of a single subscription.

## Authentication

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

## Path parameters

- `subscriptionId` (string, required) - The unique identifier of the 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.

- `subscriptionId` (string, required) - The unique identifier of the subscription.
- `status` (string, required, one of "PendingAuthorization", "Active", "Canceled", "Completed", "Expired", "Failed", "Unknown") - The lifecycle status of a subscription: * `PendingAuthorization` - created, awaiting customer authorisation via `redirectUrl`. * `Active` - authorised; scheduled charges are being taken. * `Canceled` - cancelled; no further charges. * `Completed` - all scheduled occurrences have been taken. * `Expired` - the authorisation lapsed before activation. * `Failed` - the subscription could not be established or sustained. * `Unknown` - the status could not be determined.
- `siteCode` (string, required) - The site code the subscription belongs to.
- `amount` (object, required) - The recurring charge amount.
- `amountConstraints` (object) - The authorised per-payment band. Returned on create and get.
- `payableNow` (object) - The once-off sign-up charge. Returned on create only.
- `description` (string) - The subscription description.
- `merchantReference` (string, required) - Your reference for the subscription.
- `bankReference` (string) - The reference that appears on the merchant's bank statement for each payment in the subscription.
- `frequency` (string, one of "Daily", "Weekly", "Fortnightly", "Monthly", "Biannually", "Annually") - The billing cadence of a subscription.
- `firstPaymentDate` (string, date, required) - The date of the first scheduled charge.
- `occurrences` (integer, required) - The total number of scheduled charges.
- `redirectUrl` (string, uri) - The hosted authorisation page the customer must be redirected to in order to approve the subscription. Present while authorisation is pending.
- `authorization` (object) - The customer's authorisation (consent) status. Populated once the customer has authorised via `redirectUrl`.
  - `status` (string) - The provider-mirrored authorisation status.
- `links` (object, required) - Related resource links for the subscription.
  - `self` (string, uri) - The URI of this subscription.
  - `cancel` (string, uri) - The URI to cancel this subscription.
  - `transactions` (string, uri) - The URI to create an ad-hoc charge against this subscription.

### 401 Unauthorised.

- 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 (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
}
```

### 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"
  }
}
```

### 502 Bad Gateway. An upstream payment service was unreachable or returned an error; the request was not completed. Safe to retry shortly.

- 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 (An upstream payment service could not be reached):

```json
{
  "id": "6c3a1f95-8d07-4e2b-a94c-5f0b7d2e8134",
  "links": null,
  "code": "BadGateway",
  "title": "Bad Gateway",
  "detail": "The upstream service is currently unavailable. Please try again shortly.",
  "source": 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"
  }
}
```


---

# Create Subscription

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

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

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

Create a recurring payment subscription. The response includes a `redirectUrl` to the hosted page where the customer authorises the amount band; the subscription becomes active once authorised.

## Authentication

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

## 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 site code the subscription belongs to.
- `amount` (object, required) - The recurring charge amount. Must be `ZAR` and fall within the authorised band `[amountConstraints.min, amountConstraints.max]`.
- `amountConstraints` (object, required) - The per-payment authorisation band the customer approves.
- `payableNow` (object) - An optional once-off charge taken at sign-up. This charge is not bound by `amountConstraints`.
- `identity` (object) - An optional customer identifier used for matching. Treated as personal information: it is accepted on the request but is never returned on any response (POPIA data minimisation).
  - `type` (string, required, one of "said", "passport", "registration", "cellphone") - The type of identification for the customer.
  - `country` (string, required, min length 2, max length 2) - The ISO 3166 alpha-2 code for the country of identification.
  - `identifier` (string, required, max length 20) - The identifier value for the given identity type.
- `description` (string, required, max length 20) - A short description of the subscription.
- `merchantReference` (string, required, max length 50, pattern ^\S+$) - Your unique reference for the subscription. Must not contain spaces.
- `bankReference` (string, required, max length 20, pattern ^[a-zA-Z0-9]+$) - The reference that appears on the merchant's bank statement for each payment in the subscription. Letters and numbers only. A site prefix, where one is configured, counts towards the 20 characters.
- `frequency` (string, required, one of "Daily", "Weekly", "Fortnightly", "Monthly", "Biannually", "Annually") - The billing cadence of a subscription.
- `firstPaymentDate` (string, date, required) - The date of the first scheduled charge. Must be today or a future date.
- `occurrences` (integer, required, min 1, max 120) - The total number of scheduled charges over the life of the subscription.
- `paymentMethod` (string, one of "capitec") - Optional. The payment method (rail) used to collect charges. Defaults to `capitec` when omitted, which is currently the only supported method. Not returned on responses.

## Responses

### 201 Created. The subscription was created and is awaiting customer authorisation.

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

- `subscriptionId` (string, required) - The unique identifier of the subscription.
- `status` (string, required, one of "PendingAuthorization", "Active", "Canceled", "Completed", "Expired", "Failed", "Unknown") - The lifecycle status of a subscription: * `PendingAuthorization` - created, awaiting customer authorisation via `redirectUrl`. * `Active` - authorised; scheduled charges are being taken. * `Canceled` - cancelled; no further charges. * `Completed` - all scheduled occurrences have been taken. * `Expired` - the authorisation lapsed before activation. * `Failed` - the subscription could not be established or sustained. * `Unknown` - the status could not be determined.
- `siteCode` (string, required) - The site code the subscription belongs to.
- `amount` (object, required) - The recurring charge amount.
- `amountConstraints` (object) - The authorised per-payment band. Returned on create and get.
- `payableNow` (object) - The once-off sign-up charge. Returned on create only.
- `description` (string) - The subscription description.
- `merchantReference` (string, required) - Your reference for the subscription.
- `bankReference` (string) - The reference that appears on the merchant's bank statement for each payment in the subscription.
- `frequency` (string, one of "Daily", "Weekly", "Fortnightly", "Monthly", "Biannually", "Annually") - The billing cadence of a subscription.
- `firstPaymentDate` (string, date, required) - The date of the first scheduled charge.
- `occurrences` (integer, required) - The total number of scheduled charges.
- `redirectUrl` (string, uri) - The hosted authorisation page the customer must be redirected to in order to approve the subscription. Present while authorisation is pending.
- `authorization` (object) - The customer's authorisation (consent) status. Populated once the customer has authorised via `redirectUrl`.
  - `status` (string) - The provider-mirrored authorisation status.
- `links` (object, required) - Related resource links for the subscription.
  - `self` (string, uri) - The URI of this subscription.
  - `cancel` (string, uri) - The URI to cancel this subscription.
  - `transactions` (string, uri) - The URI to create an ad-hoc charge against this subscription.

### 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
}
```

### 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"
  }
}
```

### 502 Bad Gateway. An upstream payment service was unreachable or returned an error; the request was not completed. Safe to retry shortly.

- 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 (An upstream payment service could not be reached):

```json
{
  "id": "6c3a1f95-8d07-4e2b-a94c-5f0b7d2e8134",
  "links": null,
  "code": "BadGateway",
  "title": "Bad Gateway",
  "detail": "The upstream service is currently unavailable. Please try again shortly.",
  "source": null,
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```


---

# Cancel Subscription

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

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

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

Cancel a subscription. No further charges are taken once cancelled.

## Authentication

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

## Path parameters

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

## 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 subscription has been cancelled.

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

- `subscriptionId` (string, required) - The unique identifier of the subscription.
- `status` (string, required, one of "PendingAuthorization", "Active", "Canceled", "Completed", "Expired", "Failed", "Unknown") - The lifecycle status of a subscription: * `PendingAuthorization` - created, awaiting customer authorisation via `redirectUrl`. * `Active` - authorised; scheduled charges are being taken. * `Canceled` - cancelled; no further charges. * `Completed` - all scheduled occurrences have been taken. * `Expired` - the authorisation lapsed before activation. * `Failed` - the subscription could not be established or sustained. * `Unknown` - the status could not be determined.
- `siteCode` (string, required) - The site code the subscription belongs to.
- `amount` (object, required) - The recurring charge amount.
- `amountConstraints` (object) - The authorised per-payment band. Returned on create and get.
- `payableNow` (object) - The once-off sign-up charge. Returned on create only.
- `description` (string) - The subscription description.
- `merchantReference` (string, required) - Your reference for the subscription.
- `bankReference` (string) - The reference that appears on the merchant's bank statement for each payment in the subscription.
- `frequency` (string, one of "Daily", "Weekly", "Fortnightly", "Monthly", "Biannually", "Annually") - The billing cadence of a subscription.
- `firstPaymentDate` (string, date, required) - The date of the first scheduled charge.
- `occurrences` (integer, required) - The total number of scheduled charges.
- `redirectUrl` (string, uri) - The hosted authorisation page the customer must be redirected to in order to approve the subscription. Present while authorisation is pending.
- `authorization` (object) - The customer's authorisation (consent) status. Populated once the customer has authorised via `redirectUrl`.
  - `status` (string) - The provider-mirrored authorisation status.
- `links` (object, required) - Related resource links for the subscription.
  - `self` (string, uri) - The URI of this subscription.
  - `cancel` (string, uri) - The URI to cancel this subscription.
  - `transactions` (string, uri) - The URI to create an ad-hoc charge against this subscription.

### 401 Unauthorised.

- 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 (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
}
```

### 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"
  }
}
```

### 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"
  }
}
```

### 502 Bad Gateway. An upstream payment service was unreachable or returned an error; the request was not completed. Safe to retry shortly.

- 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 (An upstream payment service could not be reached):

```json
{
  "id": "6c3a1f95-8d07-4e2b-a94c-5f0b7d2e8134",
  "links": null,
  "code": "BadGateway",
  "title": "Bad Gateway",
  "detail": "The upstream service is currently unavailable. Please try again shortly.",
  "source": null,
  "meta": {
    "correlationId": "497f6eca-6276-4993-bfeb-53cbbbba6f08"
  }
}
```


---

# Create Subscription Transaction

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

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

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

Trigger an ad-hoc (manual) charge against an authorised subscription, in addition to its scheduled charges.

## Authentication

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

## Path parameters

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

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

- `amount` (object, required) - The amount to charge. Must be `ZAR` and within the subscription's authorised band.
- `merchantReference` (string, required, max length 50) - Your unique reference for this charge.

## Responses

### 201 Created. The ad-hoc charge was accepted.

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

- `transactionId` (string, required) - The unique identifier of the charge.
- `subscriptionId` (string, required) - The subscription the charge belongs to.
- `status` (string, required, one of "Pending", "Successful", "Failed") - The status of an individual subscription charge.
- `amount` (object, required) - The charged amount.
- `merchantReference` (string, required) - Your reference for this charge.

### 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
}
```

### 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"
  }
}
```

### 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"
  }
}
```

### 502 Bad Gateway. An upstream payment service was unreachable or returned an error; the request was not completed. Safe to retry shortly.

- 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 (An upstream payment service could not be reached):

```json
{
  "id": "6c3a1f95-8d07-4e2b-a94c-5f0b7d2e8134",
  "links": null,
  "code": "BadGateway",
  "title": "Bad Gateway",
  "detail": "The upstream service is currently unavailable. Please try again shortly.",
  "source": 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"
  }
}
```


---

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


---

# Subscription consent not approved

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

Raised when a consent is not approved. Beta, subject to change.

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.


---

# Subscription consent approved

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

Raised when a payer approves a consent. Beta, subject to change.

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.


---

# Subscription cancelled

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

Raised when a subscription is cancelled. Note the single `l`, which is how the service spells this event. Beta, subject to change.

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.


---

# Subscription 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/subscription-completed/

Raised when a subscription has taken all its scheduled occurrences. Beta, subject to change.

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.


---

# Subscription expired

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

Raised when an authorisation lapses before the subscription becomes active. Beta, subject to change.

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.


---

# Subscription collection failed

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

Raised when an individual collection fails. Beta, subject to change.

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.


---

# Subscription collection succeeded

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

Raised when an individual collection succeeds. Beta, subject to change.

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.


---

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