# 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