# Collect

Collect is Tendar's mandate-based payment collection system. It lets you set up a collection plan for a customer, link their bank accounts through a consent flow, and automatically debit those accounts on a schedule you define. Think of it as direct debit on autopilot — you create a plan, the customer consents, and Tendar handles the rest.

This guide walks you through every part of the Collect feature: what it is, how it works, how to integrate it, and what happens behind the scenes at each stage. By the end, you will understand the full lifecycle of a Collect — from creation to completion.

***

## Before you begin

Make sure you have:

* Your **secret key** from the Tendar dashboard. All requests require a Bearer token:

```bash
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx
```

* An **active subscription** on your Tendar company account. Every Collect endpoint checks your subscription status before processing.
* A basic understanding of the customer whose payments you want to collect — you will need their name, email, phone number, BVN, and address.

***

## What is Collect?

At its core, a Collect is a collection plan. It describes:

* **Who** you are collecting from (the customer).
* **How much** you are collecting in total.
* **Over what period** (start date, end date, duration).
* **How often** payments should be made (frequency).
* **From which bank accounts** (mandates).

When you create a Collect, Tendar guides the customer through a consent flow where they authorize debits on their bank accounts via the NIBSS e-Mandate system. Once enough mandates are activated, Tendar generates an installment schedule and begins debiting the customer's accounts automatically on the scheduled dates.

Collect is designed for scenarios like:

* **Loan repayment** — Automatically collect loan repayments from a borrower's bank accounts on a schedule that matches their repayment plan.
* **Recurring billing** — Collect subscription fees, service charges, or any recurring amount from a customer over a defined period.
* **BNPL (Buy Now, Pay Later)** — Split a purchase into installments and collect each one automatically.

***

## How it works

Here is the high-level flow of a Collect, from creation to completion:

```
┌──────────────┐     ┌──────────────────┐     ┌─────────────────────┐
│   Create     │     │   Customer       │     │   Mandates are      │
│   Collect    │────▶│   gives consent  │────▶│   created on NIBSS  │
│              │     │   via checkout    │     │                     │
└──────────────┘     └──────────────────┘     └──────────┬──────────┘
                                                         │
                                              ┌──────────▼──────────┐
                                              │   Mandates are      │
                                              │   activated by      │
                                              │   customer's bank   │
                                              └──────────┬──────────┘
                                                         │
                     ┌──────────────────┐     ┌──────────▼──────────┐
                     │   Installments   │     │   Collect moves     │
                     │   are generated  │◀────│   to "ongoing"      │
                     │                  │     │                     │
                     └────────┬─────────┘     └─────────────────────┘
                              │
                   ┌──────────▼──────────┐
                   │   Scheduled debits  │
                   │   run automatically │
                   │   on each due date   │
                   └──────────┬──────────┘
                              │
                   ┌──────────▼──────────┐
                   │   Collect marked    │
                   │   "completed" when  │
                   │   fully paid        │
                   └─────────────────────┘
```

Let's break each stage down.

***

## The Collect lifecycle

Every Collect moves through a series of statuses. Understanding these statuses is key to integrating properly.

| Status               | What it means                                                                                                                                                         |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending_consent`    | The Collect has been created but the customer has not yet given consent. A checkout link has been generated and (optionally) emailed to them.                         |
| `pending_validation` | The customer gave consent. Their bank accounts have been fetched from NIBSS and mandates have been created. Mandates are now being submitted to NIBSS for validation. |
| `pending_signature`  | *(Signature type only)* The customer needs to sign the mandate documents.                                                                                             |
| `pending_activation` | Mandates have been submitted to NIBSS and are awaiting approval by the customer's bank.                                                                               |
| `ongoing`            | Enough mandates have been activated (based on your `mandate_percentage` threshold). Installments have been generated and automatic collections are running.           |
| `suspended`          | Collections have been paused — either manually by you, or automatically because all active mandates were lost.                                                        |
| `completed`          | The full amount has been collected. The Collect is done.                                                                                                              |
| `rejected`           | The required percentage of mandates was rejected by the customer's bank.                                                                                              |
| `failed`             | The required percentage of mandates expired, were deleted, or reached a terminal state.                                                                               |
| `terminated`         | You manually terminated the Collect before it completed.                                                                                                              |

These transitions happen automatically based on mandate statuses. You do not need to move a Collect through these states yourself — Tendar handles it. The one exception is manual actions like suspend, resume, and terminate, which you trigger explicitly.

***

## The Collect object

When you create or fetch a Collect, Tendar returns an object with the following fields:

| Field                   | Type     | Description                                                                                       |
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------- |
| `id`                    | string   | Tendar's internal ID for this Collect.                                                            |
| `user_id`               | string   | The Tendar user ID, if the Collect was created with an existing user.                             |
| `user_data`             | object   | The customer's details: `full_name`, `email`, `phone`, `address`.                                 |
| `loan`                  | string   | The loan ID, if this Collect is tied to a loan.                                                   |
| `frequency`             | integer  | How often payments are made within each `frequency_type` period.                                  |
| `frequency_type`        | string   | The interval for payments: `daily`, `weekly`, `monthly`, or `yearly`.                             |
| `duration`              | integer  | How long the Collect runs, in units of `duration_type`.                                           |
| `duration_type`         | string   | The unit for the duration: `daily`, `weekly`, `monthly`, or `yearly`.                             |
| `purpose`               | string   | A short description of what the collection is for (5–60 characters).                              |
| `reference`             | string   | A unique reference for this Collect. Auto-generated if you do not provide one (prefixed `coll-`). |
| `status`                | string   | The current status of the Collect (see lifecycle above).                                          |
| `status_reason`         | string   | A human-readable reason when the status is negative (e.g. "mandate rejected").                    |
| `paid`                  | boolean  | Whether the Collect has been fully paid.                                                          |
| `start_date`            | datetime | When the collection period begins. Must be in the future.                                         |
| `end_date`              | datetime | When the collection period ends. Auto-calculated from `start_date` + `duration`.                  |
| `currency`              | string   | The currency. Currently only `NGN` is supported.                                                  |
| `amount`                | number   | The total amount to collect.                                                                      |
| `amount_collected`      | number   | How much has been collected so far.                                                               |
| `amount_remaining`      | number   | How much is still outstanding.                                                                    |
| `generate_installments` | boolean  | Whether Tendar should automatically generate the installment schedule.                            |
| `type`                  | string   | The mandate type. Currently always `e-mandate`.                                                   |
| `mandate_percentage`    | number   | The percentage of mandates that must be active before the Collect can start (0–100).              |
| `percentage_active`     | number   | The current percentage of mandates that are active.                                               |
| `no_of_mandate`         | integer  | Total number of mandates created for this Collect.                                                |
| `no_of_active_mandate`  | integer  | Number of mandates currently active.                                                              |
| `expected_activation`   | integer  | The minimum number of mandates that need to be active (derived from `mandate_percentage`).        |
| `disbursement_data`     | object   | Disbursement settings if this Collect is tied to a loan (see loan-backed Collects).               |
| `send_email`            | boolean  | Whether to email the checkout link to the customer.                                               |
| `callback_url`          | string   | A URL where Tendar will POST webhook events for this Collect.                                     |
| `success_url`           | string   | A URL to redirect the customer to after they complete the consent flow.                           |
| `metadata`              | object   | A flexible key-value store for any custom data.                                                   |
| `installment_generated` | boolean  | Whether installments have been generated for this Collect.                                        |
| `last_pay_date`         | datetime | The date of the most recent payment.                                                              |
| `next_pay_date`         | datetime | The date of the next scheduled payment.                                                           |
| `started`               | boolean  | Whether the Collect has started (mandates were activated at least once).                          |
| `bvn`                   | string   | The customer's BVN. Redacted in API responses.                                                    |
| `mandates`              | array    | The mandates linked to this Collect (populated when fetching a single Collect).                   |
| `installments`          | array    | The installment schedule (populated when fetching a single Collect).                              |
| `checkout`              | object   | The checkout object (returned on creation).                                                       |

***

## Step 1: Create a Collect

Creating a Collect is the first step. You define the collection plan and Tendar kicks off the consent flow.

There are three ways to create a Collect, depending on your use case:

### Option A: Create with user data (standalone)

This is the most common approach for non-loan collections. You provide the customer's details directly.

**Endpoint**

```bash
POST /api/v1/collect/create
```

**Request body**

```json
{
  "user_data": {
    "full_name": "Bayo Ciroma",
    "email": "bayo.ciroma@gmail.com",
    "phone": "+2348181093644",
    "address": "Lagos, Nigeria"
  },
  "bvn": "22345678901",
  "frequency": 1,
  "frequency_type": "monthly",
  "duration": 3,
  "duration_type": "monthly",
  "purpose": "for loan",
  "reference": "",
  "start_date": "2025-06-07T00:00:00Z",
  "currency": "NGN",
  "amount": 5000,
  "generate_installments": true,
  "mandate_percentage": 10,
  "send_email": true,
  "success_url": "https://yourplatform.com/success",
  "callback_url": "https://yourplatform.com/webhooks",
  "metadata": {
    "key": "value"
  }
}
```

**What each field means:**

* **`user_data`** — The customer's details. All four fields (`full_name`, `email`, `phone`, `address`) are required. The phone must be in E.164 format (e.g. `+234...`).
* **`bvn`** — The customer's Bank Verification Number. Must be exactly 11 characters. Required for NIBSS mandate creation.
* **`frequency`** and **`frequency_type`** — How often installments are due. For example, `frequency: 1` with `frequency_type: "monthly"` means once per month.
* **`duration`** and **`duration_type`** — The total length of the collection period. `duration: 3` with `duration_type: "monthly"` means 3 months.
* **`purpose`** — A short description (5–60 characters) that will appear on the mandate.
* **`reference`** — An optional unique reference. If left empty, Tendar generates one (e.g. `coll-jnatdkocto`).
* **`start_date`** — When the collection period begins. Must be in the future.
* **`currency`** — Must be `NGN` (only Nigerian Naira is supported).
* **`amount`** — The total amount you want to collect.
* **`generate_installments`** — Set to `true` to let Tendar automatically split the amount into installments based on the frequency. Set to `false` if you want to define installments manually later.
* **`mandate_percentage`** — The percentage of the customer's mandates that must be active before the Collect starts. For example, `10` means at least 10% of their bank accounts must have active mandates. This is useful when customers have many bank accounts — you may not need all of them to be active.
* **`send_email`** — Whether to email the consent checkout link to the customer.
* **`success_url`** — A URL to redirect the customer to after completing consent.
* **`callback_url`** — A URL where Tendar sends webhook events for this specific Collect.
* **`metadata`** — Any custom key-value pairs you want to attach.

**Response** `200 OK`

```json
{
  "data": {
    "created_at": "2025-06-06T15:20:16.296Z",
    "updated_at": "2025-06-06T15:20:16.296Z",
    "id": "684307307d6565e682c9ff8d",
    "collect": "684307307d6565e682c9ff8c",
    "email": "bayo.ciroma@gmail.com",
    "status": "created",
    "url": "https://collect.tendar.co/684307307d6565e682c9ff8d",
    "send_email": true,
    "expires_at": "2025-06-09T15:20:16.266Z",
    "reference": "coll-wllvvpxrgz"
  },
  "error": false,
  "message": "Collect created successfully"
}
```

Notice that the response returns the **checkout** object, not the Collect itself. This is by design — the first thing you need after creation is the checkout URL to send to the customer. The `url` field is the consent link. The `collect` field is the ID of the underlying Collect.

### Option B: Create with a user ID

If the customer already exists in your Tendar system, you can pass their `user_id` instead of `user_data`. Tendar will look up their name, email, phone, and address automatically.

```json
{
  "user_id": "usr-dcloimuzin",
  "bvn": "22345678901",
  "frequency": 1,
  "frequency_type": "monthly",
  "duration": 3,
  "duration_type": "monthly",
  "purpose": "for loan",
  "start_date": "2025-06-07T00:00:00Z",
  "currency": "NGN",
  "amount": 50000,
  "generate_installments": true,
  "mandate_percentage": 100,
  "send_email": true,
  "metadata": { "key": "value" }
}
```

### Option C: Create with a loan ID (loan-backed)

If you are collecting repayments for a specific loan, pass the `loan` ID. Tendar will automatically pull the loan's amount, duration, frequency, and repayment schedule to configure the Collect.

```json
{
  "loan": "6843641e2e4441aa8fb32802",
  "bvn": "22345678901",
  "purpose": "for loan",
  "mandate_percentage": 10,
  "send_email": true,
  "disbursement_data": {
    "with_provider": false,
    "beneficiary": "",
    "next_pay_date": ""
  },
  "success_url": "https://yourplatform.com/success",
  "callback_url": "",
  "metadata": { "key": "value" }
}
```

When you create a loan-backed Collect:

* The `amount`, `duration`, `duration_type`, `frequency`, `frequency_type`, and `currency` are derived from the loan. You do not set them yourself.
* If the loan has a `down_payment`, the Collect amount is reduced by the down payment amount.
* If the loan has already been disbursed, the Collect amount is set to the `amount_remaining_to_pay` on the loan.
* The `start_date` defaults to tomorrow.
* `generate_installments` is automatically set to `true`.
* If the loan hasn't been disbursed yet, the Collect can trigger disbursement when installments are generated (controlled by `disbursement_data`).

The `disbursement_data` object controls how the loan is disbursed when the Collect becomes active:

| Field           | Type    | Description                                               |
| --------------- | ------- | --------------------------------------------------------- |
| `with_provider` | boolean | Whether to use a payment provider for the disbursement.   |
| `beneficiary`   | string  | The bank account ID to disburse to (if using a provider). |
| `callback_url`  | string  | A webhook URL for disbursement events.                    |
| `next_pay_date` | string  | The first repayment date (DD/MM/YYYY format).             |

### Validation rules

Tendar validates your Collect before creating it:

* The `start_date` must be in the future.
* The total duration cannot exceed 365 days.
* The installment plan must be valid — for example, you cannot have a `weekly` frequency with a `daily` duration of 3 days, because 3 days is less than one week.
* If you provide a custom `reference`, it must be unique within your company.
* Only `NGN` currency is supported.

### What happens behind the scenes

When you create a Collect, Tendar:

1. Validates all input and creates the Collect in `pending_consent` status.
2. Creates a **checkout** — a time-limited session (72 hours) that the customer uses to give consent.
3. Optionally sends a **consent email** to the customer with the checkout link.
4. Fires a `collect.create` webhook event.

***

## Step 2: The checkout and consent flow

After you create a Collect, the customer needs to give consent. This happens through the checkout flow.

### How the checkout works

{% stepper %}
{% step %}

#### Customer opens the checkout URL

The customer opens the **checkout URL** (e.g. `https://collect.tendar.co/684307307d6565e682c9ff8d`).
{% endstep %}

{% step %}

#### Customer logs in with OTP

They receive an **OTP** via email and log in to the checkout session.
{% endstep %}

{% step %}

#### Checkout fetches the consent link

The checkout page fetches a **consent link** from NIBSS. The customer is redirected to the NIBSS consent page.
{% endstep %}

{% step %}

#### Customer authorizes access

On the NIBSS consent page, the customer authorizes access to their bank accounts.
{% endstep %}

{% step %}

#### NIBSS returns an auth code

After consent, NIBSS returns an **auth code**. Tendar uses this to fetch the customer's bank accounts.
{% endstep %}

{% step %}

#### Tendar creates mandates

Tendar creates a **mandate** for each supported bank account.
{% endstep %}

{% step %}

#### Collect moves to validation

The Collect status moves from `pending_consent` to `pending_validation`.
{% endstep %}
{% endstepper %}

### Checkout object

| Field        | Type     | Description                                           |
| ------------ | -------- | ----------------------------------------------------- |
| `id`         | string   | The checkout ID.                                      |
| `collect`    | string   | The Collect this checkout belongs to.                 |
| `email`      | string   | The email used for OTP login.                         |
| `status`     | string   | One of `created`, `ongoing`, or `expired`.            |
| `url`        | string   | The checkout URL for the customer.                    |
| `send_email` | boolean  | Whether Tendar emailed the link.                      |
| `expires_at` | datetime | When this checkout expires (72 hours after creation). |
| `reference`  | string   | The Collect's reference.                              |

### Checkout statuses

| Status    | Meaning                                                                 |
| --------- | ----------------------------------------------------------------------- |
| `created` | The checkout was created but the customer hasn't completed consent yet. |
| `ongoing` | The customer has given consent and mandates are being processed.        |
| `expired` | The checkout expired (72 hours passed). You can create a new one.       |

### Creating a new checkout

If a checkout expires or the customer needs a new link, you can create a fresh checkout at any time:

**Endpoint**

```bash
POST /api/v1/collect/checkout/create
```

**Request body**

```json
{
  "reference": "coll-jnatdkocto",
  "email": "bayo.ciroma@gmail.com",
  "send_email": true
}
```

You can identify the Collect by its `reference` or by passing a `collect` ID directly. When a new checkout is created, Tendar automatically expires all previous checkouts for the same Collect.

**Response** `200 OK`

```json
{
  "data": {
    "created_at": "2025-06-06T15:40:10.801Z",
    "updated_at": "2025-06-06T15:40:10.801Z",
    "id": "68430bda89201b71762b5c67",
    "collect": "68430b7989201b71762b5c63",
    "email": "bayo.ciroma@gmail.com",
    "status": "created",
    "url": "https://collect.tendar.co/68430bda89201b71762b5c67",
    "send_email": true,
    "expires_at": "2025-06-09T15:40:10.583Z",
    "reference": "coll-jnatdkocto"
  },
  "error": false,
  "message": "Checkout created successfully"
}
```

### Checkout API endpoints (for building custom checkout UIs)

If you are building your own checkout experience instead of using Tendar's hosted checkout page, these endpoints are available:

**Send login OTP**

```bash
GET /api/v1/collect/checkout/auth/send-login-otp/:checkout_id
```

Sends a one-time password to the email associated with the checkout. Returns a success message with the email address.

**Login**

```bash
POST /api/v1/collect/checkout/auth/login/:checkout_id
```

```json
{
  "token": "123456"
}
```

Verifies the OTP and returns an auth token. The token is valid for 72 hours and gives access to the authenticated checkout endpoints.

**Fetch checkout (authenticated)**

```bash
GET /api/v1/collect/checkout/fetch
```

Requires the collect auth token. Returns the full checkout object with the associated Collect details.

**Fetch consent link (authenticated)**

```bash
POST /api/v1/collect/checkout/consent-link
```

```json
{
  "consent_callback": "https://yourcheckout.com/callback"
}
```

Returns a NIBSS consent URL. Redirect the customer to this URL to authorize access to their bank accounts. After consent, NIBSS redirects back to your `consent_callback` with an auth code.

**Process consent (authenticated)**

After the customer completes consent on the NIBSS page, you receive an auth code. The `ProcessCheckoutMandates` function uses this code to fetch the customer's bank accounts and create mandates. This happens automatically when using Tendar's hosted checkout page.

**List mandates (authenticated)**

```bash
GET /api/v1/collect/checkout/mandate/list
```

Returns all mandates for the Collect associated with this checkout.

**Request mandate activation (authenticated)**

```bash
PUT /api/v1/collect/checkout/mandate/activate/request/:mandate_id
```

```json
{
  "latitude": 6.5244,
  "longitude": 3.3792
}
```

Submits a specific mandate to NIBSS for activation. Requires the customer's geolocation. This is how the customer selects which bank accounts to activate.

**Fetch checkout by ID (unauthenticated)**

```bash
GET /api/v1/collect/checkout/fetch/:checkout_id
```

Returns basic checkout information without requiring authentication. Useful for checking if a checkout is still valid.

***

## Step 3: Mandates

Mandates are the bridge between the customer's bank accounts and your Collect. They represent the customer's authorization for Tendar to debit their account.

### How mandates are created

When the customer completes the NIBSS consent flow, Tendar:

1. Fetches all bank accounts linked to the customer's BVN.
2. Filters to only banks that support the NIBSS e-Mandate system.
3. Creates a mandate record for each supported bank account with status `created`.

The customer does not need to activate all mandates. The `mandate_percentage` setting on the Collect determines the minimum threshold.

### Mandate statuses

| Status                | What it means                                                                                 |
| --------------------- | --------------------------------------------------------------------------------------------- |
| `created`             | The mandate has been created locally but not yet submitted to NIBSS.                          |
| `awaiting_activation` | The mandate has been submitted to NIBSS and is waiting for the customer's bank to approve it. |
| `active`              | The bank approved the mandate. This account can now be debited.                               |
| `suspended`           | The mandate has been temporarily suspended.                                                   |
| `rejected`            | The bank rejected the mandate.                                                                |
| `revoked`             | The mandate was revoked (disapproved by the bank).                                            |
| `deleted`             | The mandate has been deleted on NIBSS.                                                        |
| `expired`             | The mandate expired (either the activation window or the Collect's end date passed).          |

### Mandate activation flow

{% stepper %}
{% step %}

#### Customer requests activation

The customer requests activation for a specific mandate (from the checkout UI) by providing their geolocation.
{% endstep %}

{% step %}

#### Tendar submits the mandate

Tendar submits the mandate to NIBBS with the account details, collection amount, narration, start/end dates, and the customer's BVN.
{% endstep %}

{% step %}

#### NIBSS returns a mandate code

NIBSS returns a mandate code. The mandate moves to `awaiting_activation`.
{% endstep %}

{% step %}

#### Activation window starts

The mandate has a **30-minute activation window**. If the bank does not act within 30 minutes, Tendar suspends the mandate on NIBSS and resets it to `created` status (so the customer can try again).
{% endstep %}

{% step %}

#### Tendar polls for status

Tendar polls NIBSS for the mandate status. When the bank approves it, the mandate moves to `active`.
{% endstep %}

{% step %}

#### Collect becomes ongoing

Once enough mandates are active (based on `mandate_percentage`), the Collect transitions to `ongoing`.
{% endstep %}
{% endstepper %}

### The mandate percentage

The `mandate_percentage` is a powerful control. It determines what percentage of mandates must be active for the Collect to proceed.

**Example:** A customer has 8 bank accounts. You set `mandate_percentage: 10`. That means at least 10% of 8 = 0.8, rounded up to **1 mandate** must be active. As soon as any one of the 8 mandates becomes active, the Collect moves to `ongoing` and collections start.

If you set `mandate_percentage: 100`, **all 8 mandates** must be active. This gives you maximum coverage but is harder to achieve.

The `expected_activation` field on the Collect shows the calculated minimum number of mandates required:

```
expected_activation = ceil(mandate_percentage / 100 × no_of_mandate)
```

### Mandate expiration

Mandates have two expiration mechanisms:

1. **Activation expiration** — If a mandate stays in `awaiting_activation` for more than 30 minutes, Tendar resets it to `created`. This prevents stale pending mandates.
2. **Collect expiration** — When the Collect's `end_date` passes, all active mandates are expired and deleted on NIBSS.

### Supported banks

Not all Nigerian banks support the NIBSS e-Mandate system. Tendar automatically filters bank accounts to only include supported banks. The currently supported bank codes include: Access Bank (044), ALAT/Wema (035A/035), Ecobank (050), Fidelity (070), First Bank (011), FCMB (214), Globus (00103), GTBank (058), Keystone (082), Providus (101), Stanbic IBTC (221), Standard Chartered (068), Sterling (232), Suntrust (100), Union (032), UBA (033), Unity (215), Zenith (057), Citibank (023), Heritage (030), Jaiz (301), Polaris (076), Titan Trust (102), and more.

If a customer's bank is not supported, that particular account will simply not have a mandate created for it.

***

## Step 4: Installments

Once a Collect moves to `ongoing` status, Tendar generates the installment schedule. Installments are the individual payment records that make up the collection plan.

### Automatic installment generation

When `generate_installments` is `true` and the Collect has a `frequency` and `frequency_type`, Tendar calculates the schedule automatically:

{% stepper %}
{% step %}

#### Split the amount

The total `amount` is divided equally across the number of installments.
{% endstep %}

{% step %}

#### Assign scheduled dates

Each installment is assigned a `scheduled_date` starting from `start_date`, spaced by the frequency interval.
{% endstep %}

{% step %}

#### Cap the last date

The last installment's date is capped at `end_date`.
{% endstep %}
{% endstepper %}

**Example:** A ₦5,000 Collect with `frequency: 1`, `frequency_type: "monthly"`, `duration: 3`, `duration_type: "monthly"`, starting June 7:

| SN |    Amount | Scheduled Date    |
| -- | --------: | ----------------- |
| 1  | ₦1,666.67 | July 7, 2025      |
| 2  | ₦1,666.67 | August 7, 2025    |
| 3  | ₦1,666.67 | September 5, 2025 |

### Manual installment generation

If `generate_installments` is `false` (or you want custom installment amounts and dates), you can generate installments manually after the Collect is ongoing:

**Endpoint**

```bash
POST /api/v1/collect/installment/generate/:collect_id
```

**Request body**

```json
{
  "installments": [
    { "amount": 1000, "date": "15/06/2025" },
    { "amount": 1000, "date": "30/06/2025" },
    { "amount": 1000, "date": "15/07/2025" },
    { "amount": 1000, "date": "30/07/2025" },
    { "amount": 1000, "date": "15/08/2025" }
  ]
}
```

**Validation rules:**

* Each installment must have an `amount` greater than 0 and a `date` in `DD/MM/YYYY` format.
* All dates must fall between the Collect's `start_date` and `end_date`.
* The total installment amount cannot exceed the Collect's `amount`.
* Installments can only be generated once per Collect.

**Response** `200 OK`

```json
{
  "error": false,
  "message": "Installments generated successfully"
}
```

### Loan-backed installment generation

When a Collect is tied to a loan, the installment generation process is different:

1. If the loan has **not been disbursed**, Tendar first triggers the disbursement (using the `disbursement_data` on the Collect). If the disbursement is pending (provider-based), installment generation waits. Once the disbursement succeeds, a worker event triggers installment generation.
2. Once the loan is disbursed, Tendar pulls the **loan's repayment schedule** and converts each unpaid repayment into a Collect installment.

This means the installment dates and amounts match the loan's repayment plan exactly — you do not need to configure them separately.

### The installment object

| Field              | Type     | Description                                                                     |
| ------------------ | -------- | ------------------------------------------------------------------------------- |
| `id`               | string   | The installment ID.                                                             |
| `amount`           | number   | The amount due for this installment.                                            |
| `amount_collected` | number   | How much has been collected for this installment.                               |
| `amount_remaining` | number   | How much is still outstanding for this installment.                             |
| `currency`         | string   | The currency (NGN).                                                             |
| `scheduled_date`   | datetime | When this installment is due.                                                   |
| `last_pay_date`    | datetime | The date of the last payment against this installment.                          |
| `paid`             | boolean  | Whether this installment is fully paid.                                         |
| `sn`               | integer  | The sequence number (1, 2, 3, ...).                                             |
| `is_processing`    | boolean  | Whether this installment is currently being processed by the collection engine. |

### Adding more installments

You can add installments to an ongoing Collect after the initial generation:

**Endpoint**

```bash
POST /api/v1/collect/installment/add/:collect_id
```

**Request body**

```json
{
  "installments": [
    { "amount": 1000, "date": "16/08/2025" }
  ]
}
```

The new installments are merged with existing ones, re-sorted by date, and re-numbered. The same validation rules apply — dates must be within the Collect's range and the total cannot exceed the Collect amount.

### Editing an installment

You can change the amount or date of an existing installment, as long as it has not been paid or is not currently being processed:

**Endpoint**

```bash
PUT /api/v1/collect/installment/edit/:collect_id/:installment_id
```

**Request body**

```json
{
  "amount": 1500,
  "date": "20/07/2025"
}
```

Both fields are optional. The installment list is re-sorted and re-validated after the edit.

### Removing an installment

You can remove an unpaid, unprocessed installment:

**Endpoint**

```bash
PUT /api/v1/collect/installment/remove/:collect_id/:installment_id
```

The remaining installments are re-sorted and re-numbered.

***

## Step 5: Automatic collections

This is where Collect truly shines. Once installments are generated, Tendar automatically debits the customer's bank accounts on the scheduled dates.

### How the collection engine works

{% stepper %}
{% step %}

#### Start collections

At the beginning of each day, Tendar looks for all installments due on that date. For each one, it creates an **installment collection** record and schedules it to run every 6 hours throughout the day.
{% endstep %}

{% step %}

#### Process collections

Every 6 hours, each scheduled collection attempts to debit the customer's active mandates via NIBSS fund transfer.
{% endstep %}

{% step %}

#### End collections

At the end of the day, Tendar closes out all ongoing collections for that date.
{% endstep %}
{% endstepper %}

### The collection process in detail

When a collection runs for a specific installment:

1. Tendar checks that the Collect is still `ongoing`, the installment is unpaid, and there are active mandates.
2. For each active mandate (starting with the default), Tendar:
   * Calculates the amount to charge (based on the installment's remaining amount and a `percentage_to_charge`).
   * Initiates a NIBSS fund transfer from the customer's bank account.
   * If successful, updates the installment's `amount_collected` and `amount_remaining`.
   * Creates a `CollectPayment` record.
   * If the Collect is tied to a loan, publishes a recollection event to update the loan's repayment status.
   * Funds the company's wallet with the collected amount.
3. If the installment is fully paid after processing a mandate, the collection stops — no further mandates are tried.
4. If a mandate debit fails, Tendar moves to the next mandate.

### Smart retry with percentage reduction

The collection engine has a built-in retry strategy:

* **First attempt:** Tries to charge 100% of the remaining amount.
* **After a failure:** Reduces the `percentage_to_charge` by 10% on the next attempt (minimum 10%).
* **After a partial success:** Resets the `percentage_to_charge` back to 100%.

This means if a customer's account doesn't have the full amount, Tendar progressively tries smaller amounts until it finds one that works — maximizing the chance of collecting something on each run.

### Collection statuses

| Status      | Meaning                                                                                                                                |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `pending`   | The collection has been created but hasn't been processed yet.                                                                         |
| `ongoing`   | The collection is being processed (debits are being attempted).                                                                        |
| `completed` | The installment was fully paid through this collection.                                                                                |
| `discarded` | The collection was skipped (e.g. the Collect is no longer ongoing, the installment was already paid, or there are no active mandates). |

### What happens when a collection completes

After a successful debit:

1. The installment's `amount_collected` increases and `amount_remaining` decreases.
2. The Collect's `amount_collected` increases and `amount_remaining` decreases.
3. If the installment is fully paid, it is marked as `paid: true`.
4. If the entire Collect is fully paid (`amount_remaining` ≤ 0), the Collect status moves to `completed`.
5. A `collect.payment.create` webhook event is sent.
6. If the Collect is completed, a `collect.completed` webhook event is sent.
7. The collected amount is deposited into your company's Tendar wallet.
8. A collection charge (1.5% of the amount) is deducted from your wallet.

***

## Manual payments

In addition to automatic collections, you can record payments manually. This is useful when a customer makes a payment outside the automatic debit system — for example, a bank transfer, cash payment, or payment through a different channel.

**Endpoint**

```bash
POST /api/v1/collect/payment/manual/:collect_id
```

**Request body**

```json
{
  "amount": 2000,
  "payment_channel": "bank_transfer",
  "reference": "",
  "paid_at": "2025-07-15T10:00:00Z"
}
```

**What each field means:**

* **`amount`** — The payment amount. Must be greater than 0.
* **`payment_channel`** — An optional label (e.g. `"bank_transfer"`, `"cash"`). Informational only.
* **`reference`** — An optional unique reference. Auto-generated if empty.
* **`paid_at`** — When the payment was made. Defaults to now if not provided.

Tendar processes the payment by applying it to the oldest unpaid installment first, then rolling over any excess to the next installment, and so on. The Collect's `amount_collected`, `amount_remaining`, `last_pay_date`, and `next_pay_date` are updated accordingly.

If the payment brings the total collected to the full amount, the Collect is marked as `completed`.

**Response** `200 OK`

```json
{
  "data": {
    "id": "...",
    "collect": "...",
    "status": "success",
    "amount": 2000,
    "currency": "NGN",
    "payment_channel": "bank_transfer",
    "reference": "col-xkqmprtywz",
    "paid_at": "2025-07-15T10:00:00Z"
  },
  "error": false,
  "message": "Payment created successfully"
}
```

***

## Managing a Collect

### Edit a Collect

You can edit a Collect while it is still in `pending_consent` status. Most fields can be changed — user data, amount, frequency, duration, dates, and more.

Once the Collect has moved past `pending_consent` (i.e., mandates have been created), only limited fields can be edited: `metadata`, `callback_url`, `success_url`, `mandate_percentage`, `disbursement_data`, and `generate_installments`.

Once installments have been generated, the Collect cannot be edited at all.

**Endpoint**

```bash
PUT /api/v1/collect/edit/:collect_id
```

**Request body** — any of the editable fields:

```json
{
  "purpose": "updated purpose",
  "amount": 6000,
  "metadata": { "updated": true },
  "mandate_percentage": 50
}
```

**Response** `200 OK`

Returns the updated Collect object.

### Suspend a Collect

Pauses an ongoing Collect. Automatic collections will stop running.

**Endpoint**

```bash
PUT /api/v1/collect/suspend/:collect_id
```

**Conditions:** The Collect must be in `ongoing` status.

**Response** `200 OK`

```json
{
  "error": false,
  "message": "Collect suspended successfully"
}
```

### Resume a Collect

Resumes a suspended Collect. Automatic collections will start running again.

**Endpoint**

```bash
PUT /api/v1/collect/resume/:collect_id
```

**Conditions:** The Collect must be in `suspended` status and must have been started at least once (i.e., `started: true`).

**Response** `200 OK`

```json
{
  "error": false,
  "message": "Collect resumed successfully"
}
```

### Terminate a Collect

Permanently stops a Collect. This cannot be undone.

**Endpoint**

```bash
PUT /api/v1/collect/terminate/:collect_id
```

**Conditions:** The Collect cannot already be in a terminal status (`completed`, `terminated`, `rejected`, or `failed`).

**Response** `200 OK`

```json
{
  "error": false,
  "message": "Collect terminated successfully"
}
```

***

## Fetching Collects

### List all Collects

**Endpoint**

```bash
GET /api/v1/collect/list
```

**Query parameters**

| Parameter         | Description                                           |
| ----------------- | ----------------------------------------------------- |
| `page`            | Page number (default: 1).                             |
| `limit`           | Results per page (default: 20).                       |
| `status`          | Filter by status (e.g. `ongoing`, `pending_consent`). |
| `paid`            | Filter by paid status (`true` or `false`).            |
| `user_id`         | Filter by user ID.                                    |
| `sort-created_at` | Sort by creation date (`asc` or `desc`).              |
| `created_at`      | Filter by creation date.                              |

**Response** `200 OK`

Returns a paginated list of Collect objects (without mandates or installments).

### Fetch a single Collect

**Endpoint**

```bash
GET /api/v1/collect/fetch/:collect_id
```

Returns the full Collect object including `mandates` and `installments` arrays. This gives you a complete picture of the collection plan and its current state in a single call.

### Fetch by reference

**Endpoint**

```bash
GET /api/v1/collect/fetch/reference/:ref
```

Same as fetch by ID, but using the Collect's reference string.

***

## Webhooks

Tendar sends webhook events to your `callback_url` (and your global webhook URL) at key points in the Collect lifecycle. All events follow this structure:

```json
{
  "company_id": "64245949dbcc9f021a769e65",
  "event": "collect.ongoing",
  "service": "recollection",
  "data": { ... }
}
```

### Collect events

| Event                        | When it fires                                                    |
| ---------------------------- | ---------------------------------------------------------------- |
| `collect.create`             | A new Collect was created.                                       |
| `collect.pending_consent`    | The Collect is waiting for customer consent.                     |
| `collect.pending_validation` | Mandates have been created and are being validated.              |
| `collect.pending_activation` | Mandates are awaiting bank activation.                           |
| `collect.ongoing`            | Enough mandates are active and installments are being generated. |
| `collect.suspended`          | The Collect was suspended.                                       |
| `collect.rejected`           | The required mandates were rejected.                             |
| `collect.failed`             | The required mandates reached a terminal state.                  |
| `collect.terminated`         | The Collect was manually terminated.                             |
| `collect.completed`          | The full amount has been collected.                              |
| `collect.payment.create`     | A payment was recorded (automatic or manual).                    |

### Using webhooks effectively

Listen for `collect.ongoing` to know when a Collect has started and installments are ready. Listen for `collect.payment.create` to track individual payments in real time. Listen for `collect.completed` to know when the customer has fully paid.

If you provided a `callback_url` on the Collect, events are sent to both your global webhook URL **and** the Collect-specific callback URL.

***

## Automatic status transitions

One of the most important things to understand about Collect is that **status transitions are automatic**. You do not manually move a Collect from `pending_validation` to `ongoing`. Instead, Tendar monitors mandate statuses and transitions the Collect automatically.

Here is the decision logic:

### When mandates are being validated/activated

* If the percentage of active mandates meets or exceeds `mandate_percentage` → **ongoing**
* If the percentage of mandates awaiting activation (plus active) meets the threshold → **pending\_activation**
* If the percentage of rejected mandates exceeds `100 - mandate_percentage` → **rejected**
* If the percentage of terminal mandates (deleted + revoked + expired + rejected) exceeds `100 - mandate_percentage` → **failed**

### When the Collect is ongoing

* If at least one mandate is still active → stays **ongoing**
* If no mandates are active and all remaining mandates are terminal → **terminated**
* If no mandates are active but non-terminal mandates remain → **suspended**

### When the Collect is suspended (and was previously started)

* If an active mandate appears again → **ongoing**
* If all remaining mandates become terminal → **terminated**

This means your integration should be designed around webhooks and status checks rather than explicit state management.

***

## Charges and billing

Creating a Collect incurs a charge against your Tendar wallet (the **Collect Creation** charge). This is deducted at creation time. If the charge fails (insufficient balance), the Collect is not created.

When automatic collections succeed, a **Collect Recollection** charge (1.5% of the collected amount) is applied to your wallet. The collected amount is deposited first, then the charge is deducted.

Make sure your Tendar wallet has sufficient funds to cover creation charges, especially if you are creating Collects in bulk.

***

## Concurrency and idempotency

Tendar uses distributed locks extensively throughout the Collect system:

* **Collect creation/editing** — Locked per Collect to prevent race conditions.
* **Mandate activation** — Locked per mandate to prevent duplicate NIBSS submissions.
* **Status transitions** — Locked per Collect to prevent conflicting status updates.
* **Collection processing** — Locked per installment to prevent double-debits.
* **Checkout processing** — Locked per checkout to prevent duplicate consent processing.

Custom references serve as natural idempotency keys. If you provide a `reference` that already exists, the request is rejected.

***

## Error handling

Here are the most common errors you might encounter:

| Error                                     | HTTP Status | Cause                                                                         | Resolution                                       |
| ----------------------------------------- | ----------: | ----------------------------------------------------------------------------- | ------------------------------------------------ |
| `user not found`                          |         404 | The `user_id` does not exist.                                                 | Verify the user ID.                              |
| `loan not found`                          |         404 | The `loan` ID does not exist.                                                 | Verify the loan ID.                              |
| `loan has been paid`                      |         400 | The loan is fully paid.                                                       | You cannot create a Collect for a paid loan.     |
| `this loan already has an active collect` |         400 | A Collect already exists for this loan.                                       | Terminate the existing Collect first.            |
| `invalid installment plan`                |         400 | The frequency/duration combination is invalid.                                | Ensure the frequency fits within the duration.   |
| `duration cannot exceed 365 days`         |         400 | The total duration exceeds one year.                                          | Reduce the duration.                             |
| `start date cannot be in the past`        |         400 | The `start_date` is before today.                                             | Use a future date.                               |
| `only NGN currency is supported`          |         400 | You specified a non-NGN currency.                                             | Use `"NGN"`.                                     |
| `reference already exists`                |         400 | A Collect with this reference already exists.                                 | Use a different reference or omit it.            |
| `not an ongoing collect`                  |         400 | You tried to generate installments/add installments on a non-ongoing Collect. | Wait until the Collect reaches `ongoing` status. |
| `installment already generated`           |         400 | Installments have already been generated.                                     | Use "add installment" instead.                   |
| `this collect cannot be edited`           |         400 | The Collect is in a terminal or restricted status.                            | Editing is only allowed in certain statuses.     |
| `this collect cannot be suspended`        |         400 | The Collect is not in `ongoing` status.                                       | Only ongoing Collects can be suspended.          |
| `this collect cannot be resumed`          |         400 | The Collect is not suspended or hasn't been started yet.                      | Only started, suspended Collects can be resumed. |
| `collect checkout not found`              |         404 | The checkout ID is invalid.                                                   | Verify the checkout ID.                          |
| `This checkout has expired`               |         400 | The checkout's 72-hour window has passed.                                     | Create a new checkout.                           |

***

## Putting it all together

Here is a typical end-to-end integration flow:

{% stepper %}
{% step %}

#### Create the Collect

```bash
curl -X POST https://api.tendar.io/api/v1/collect/create \
  -H "Authorization: Bearer sk_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "user_data": {
      "full_name": "Bayo Ciroma",
      "email": "bayo.ciroma@gmail.com",
      "phone": "+2348181093644",
      "address": "Lagos, Nigeria"
    },
    "bvn": "22345678901",
    "frequency": 1,
    "frequency_type": "monthly",
    "duration": 3,
    "duration_type": "monthly",
    "purpose": "Monthly subscription",
    "currency": "NGN",
    "amount": 15000,
    "generate_installments": true,
    "mandate_percentage": 10,
    "send_email": true,
    "success_url": "https://yourplatform.com/success",
    "callback_url": "https://yourplatform.com/webhooks"
  }'
```

Save the checkout `url` from the response.
{% endstep %}

{% step %}

#### Customer gives consent

The customer opens the checkout URL, logs in with OTP, and authorizes access to their bank accounts through NIBSS. They then select which mandates to activate.

You do not need to do anything here — the checkout flow handles it. Listen for the `collect.ongoing` webhook to know when the process is complete.
{% endstep %}

{% step %}

#### Monitor via webhooks

Set up your webhook endpoint to handle events:

```json
{
  "event": "collect.ongoing",
  "data": {
    "id": "684307307d6565e682c9ff8c",
    "status": "ongoing",
    "amount": 15000,
    "no_of_active_mandate": 1,
    "installment_generated": true,
    "next_pay_date": "2025-07-07T00:00:00Z"
  }
}
```

{% endstep %}

{% step %}

#### Automatic collections run

Tendar debits the customer's bank accounts on the scheduled dates. You receive `collect.payment.create` events for each successful debit.
{% endstep %}

{% step %}

#### Track progress

Poll the Collect to see the current state:

```bash
curl https://api.tendar.io/api/v1/collect/fetch/684307307d6565e682c9ff8c \
  -H "Authorization: Bearer sk_live_xxxxxxxx"
```

The response includes `amount_collected`, `amount_remaining`, and the full `installments` array with per-installment payment details.
{% endstep %}

{% step %}

#### Completion

When the full amount is collected, you receive a `collect.completed` webhook:

```json
{
  "event": "collect.completed",
  "data": {
    "id": "684307307d6565e682c9ff8c",
    "status": "completed",
    "amount": 15000,
    "amount_collected": 15000,
    "amount_remaining": 0,
    "paid": true
  }
}
```

{% endstep %}
{% endstepper %}

***

## What to read next

| Guide                                                                                   | What you will learn                                                                                                   |
| --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| [Direct Debit](https://docs.tendar.co/documentation/recollection/broken-reference)      | An alternative approach to bank-account-based collections, using Paystack-backed direct debit authorizations.         |
| [Recollect Loan](https://docs.tendar.co/documentation/recollection/broken-reference)    | How to record loan repayments — useful if your Collect is tied to a loan and you need to track repayments separately. |
| [Card Tokenization](https://docs.tendar.co/documentation/recollection/broken-reference) | Another collection method — save customer cards and charge them on demand.                                            |
| [Overview](https://docs.tendar.co/documentation/recollection/broken-reference)          | The full Recollection Service overview with all available collection methods.                                         |
