---
title: Instalment invoices
---

# Instalment invoices

Some things are not paid for in one go. A ₦1.8m kitchen fit-out, a term's school fees, a six-week retainer, a wedding booked nine months out. The deal is agreed once and the money arrives in pieces, on terms the two parties settled between themselves.

Most gateways make you rebuild that yourself: your own schedule table, your own balance arithmetic, a fresh payment link every time, and a reconciliation job to work out which transfer belonged to which customer. ZevPay invoices carry the terms, so the invoice itself knows what is owed, what has been paid, and what the customer is allowed to pay next.

This page is the long version. If you only want the field reference, see the [Invoice API](/api/invoice).

## The shape of it

One invoice. Many payments. Each payment is a real transaction that settles on its own, exactly like a standalone checkout, so nothing about your payout reconciliation changes.

```
Invoice INV-2026-0041          ₦1,800,000
├── Payment 1   ₦600,000   paid 3 Mar   settled 4 Mar
├── Payment 2   ₦600,000   paid 2 Apr   settled 3 Apr
└── Payment 3   ₦600,000   paid 1 May   settled 2 May
```

The customer opens the same link every time. They see what they have already paid, choose what to pay now, and pay it. You get a webhook per payment and a settlement per payment.

## A worked example

A furniture maker takes a ₦1.8m commission. The terms are a third up front, and the customer can pay the rest however they like until delivery.

### 1. Create the invoice with the terms on it

```bash
curl -X POST https://api.zevpaycheckout.com/v1/checkout/invoice \
  -H "x-api-key: sk_live_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_name": "Adaeze Nwosu",
    "customer_email": "adaeze@example.com",
    "due_date": "2026-06-30T00:00:00Z",
    "line_items": [
      { "description": "Walnut dining set, 8 seater", "quantity": 1, "unit_price": 180000000 }
    ],
    "allow_partial_payments": true,
    "minimum_initial_payment": 60000000,
    "subsequent_payment_rule": "flexible",
    "due_date_action": "keep_collecting"
  }'
```

Amounts are in kobo throughout, so `180000000` is ₦1,800,000 and `60000000` is ₦600,000.

Those four fields are the whole policy:

| Field | What it does |
|-------|--------------|
| `allow_partial_payments` | Turns instalments on. Without it the invoice is pay-in-full. |
| `minimum_initial_payment` | The deposit. The first payment must be at least this. They may pay more. |
| `subsequent_payment_rule` | `flexible` lets them pay any amount afterwards. `full_balance` means the rest must come in one payment. |
| `due_date_action` | What happens when the due date passes. Defaults to `keep_collecting`. |

### 2. Send it

```bash
curl -X POST https://api.zevpaycheckout.com/v1/checkout/invoice/{public_id}/send \
  -H "x-api-key: sk_live_your_secret_key"
```

The customer gets an email with a link to the invoice page.

### 3. The customer pays what they agreed

On an instalment invoice the page asks how much they want to pay before it offers payment methods. It is pre-filled with the full balance, because most people pay it all, and it states the rule before they type rather than rejecting them after.

If they try to pay ₦400,000 against a ₦600,000 deposit, the page tells them the first payment must be at least ₦600,000. The server enforces the same rule, so editing the request achieves nothing.

### 4. You get a webhook per payment

```json
{
  "event": "invoice.payment_received",
  "data": {
    "invoice_public_id": "inv_8Kd93mQx2Lp",
    "invoice_number": "INV-2026-0041",
    "payment_id": "ipy_4Rm72nBw9Tc",
    "amount": 60000000,
    "amount_paid": 60000000,
    "amount_outstanding": 120000000,
    "is_partial": true,
    "is_fully_paid": false,
    "reference": "ZVP-CKOTX-9f2a1c",
    "paid_at": "2026-03-03T11:24:08.000Z"
  }
}
```

Fulfil on `is_fully_paid`, not on the event arriving. On an instalment invoice `invoice.payment_received` fires every time, and the first one is not the last one.

### 5. Reconcile

Each instalment is its own settled transaction. To see them:

```bash
curl https://api.zevpaycheckout.com/v1/checkout/invoice/{public_id}/payments \
  -H "x-api-key: sk_live_your_secret_key"
```

```json
{
  "invoice": {
    "public_id": "inv_8Kd93mQx2Lp",
    "status": "partial",
    "total": 180000000,
    "amount_paid": 120000000,
    "amount_outstanding": 60000000,
    "collecting": true
  },
  "payments": [
    {
      "payment_id": "ipy_7Bn21kFd5Qa",
      "amount": 60000000,
      "paid_at": "2026-04-02T09:15:44.000Z",
      "payer_name": "ADAEZE NWOSU",
      "payment_method": "bank_transfer",
      "transaction": {
        "reference": "ZVP-CKOTX-3d81be",
        "gross_amount": 60000000,
        "fees": 200000,
        "net_amount": 59800000,
        "settlement_state": "paid_out",
        "settled_at": "2026-04-03T12:00:00.000Z"
      }
    }
  ]
}
```

Every payment carries its own reference, fee, net and settlement state. Verify a single one with `GET /v1/checkout/invoice/{public_id}/payments/{payment_id}`.

## Choosing your terms

### A deposit, then the balance in one go

The common shape for work with a delivery date. A third up front, the rest before it ships.

```json
{
  "allow_partial_payments": true,
  "minimum_initial_payment": 60000000,
  "subsequent_payment_rule": "full_balance"
}
```

After the deposit the page stops offering a choice and asks for the balance.

### Pay it down however you like

School fees, a retainer, anything where you care that it clears, not how.

```json
{
  "allow_partial_payments": true,
  "subsequent_payment_rule": "flexible"
}
```

No minimum, any amount, as many payments as it takes.

### A floor on every payment

There is deliberately no per-payment minimum beyond the first. If you need one, set `minimum_initial_payment` and `full_balance`, which gives you exactly two payments of known size. Chasing twelve ₦5,000 payments costs more in reconciliation than it collects.

## Changing your mind

Terms are not frozen at creation. A customer asking to pay in instalments halfway through is a normal conversation, and re-issuing the invoice would lose its history and its link.

This works on invoices you have already sent, and on part-paid ones. What stays fixed after sending is the money itself: line items, tax and the customer. Everything about *how* it gets paid, including the due date, remains yours to change.

### Relaxing or tightening the balance rule mid-invoice

The most common version of this: you asked for the balance in one payment, the customer comes back and says they cannot manage it, and you agree to let them pay it down.

```bash
curl -X PATCH https://api.zevpaycheckout.com/v1/checkout/invoice/{public_id} \
  -H "x-api-key: sk_live_your_secret_key" \
  -d '{ "subsequent_payment_rule": "flexible" }'
```

It takes effect on their next payment. On a ₦1.8m invoice with ₦600,000 already paid:

| Rule | What the customer may pay next |
|------|--------------------------------|
| `full_balance` | Exactly ₦1,200,000 |
| `flexible` | Any amount up to ₦1,200,000 |

The reverse works too. Tighten a flexible invoice to `full_balance` and the next payment must clear whatever is outstanding at that moment, not the original balance.

Two things hold either way. The ceiling is always the outstanding balance, so no rule lets a customer overpay. And `minimum_initial_payment` cannot be changed once the first payment has landed, because a deposit rule cannot apply retroactively to money already taken.

If the invoice was created with `allow_partial_payments: false`, send both fields to open it up:

```bash
-d '{ "allow_partial_payments": true, "subsequent_payment_rule": "flexible" }'
```

Every one of these changes lands on the activity log with the fields that moved, so what was agreed and when survives the conversation.

```bash
curl -X PATCH https://api.zevpaycheckout.com/v1/checkout/invoice/{public_id} \
  -H "x-api-key: sk_live_your_secret_key" \
  -d '{ "subsequent_payment_rule": "flexible" }'
```

Every change is recorded on the invoice's activity log with the fields that changed, so the record of what was agreed and when survives the conversation.

## Extending a due date

```bash
curl -X PATCH https://api.zevpaycheckout.com/v1/checkout/invoice/{public_id} \
  -H "x-api-key: sk_live_your_secret_key" \
  -d '{ "due_date": "2026-07-31T00:00:00Z" }'
```

An invoice that had already gone overdue returns to `sent`, or `partial` if part of it is paid, the moment the new date is in the future. Your customer stops seeing a past-due invoice straight away.

The old and new dates are recorded on the activity log, so the record of when the terms moved survives the conversation that moved them.

## Chasing payment

```bash
curl -X POST https://api.zevpaycheckout.com/v1/checkout/invoice/{public_id}/remind \
  -H "x-api-key: sk_live_your_secret_key"
```

Re-sends the invoice email. On a part-paid invoice the reminder chases the outstanding balance, not the original total, so it never reads as a demand for money already paid.

Rate limited to one reminder an hour per invoice. This endpoint points at your customer's inbox, and a loop in your automation is a loop in their mailbox.

## Stopping collection

Two different things, for two different situations.

**Pause** when you want to stop taking money but keep the invoice alive. A dispute, a delivery problem, a customer who asked you to hold. The invoice keeps its status and its history, the customer can still open the page and see everything they have paid, and you can resume without re-issuing anything.

```bash
curl -X POST https://api.zevpaycheckout.com/v1/checkout/invoice/{public_id}/pause \
  -H "x-api-key: sk_live_your_secret_key" \
  -d '{ "reason": "On hold while we agree the revised spec" }'
```

The reason is shown to the customer. Resume with `POST /{public_id}/resume`.

**Cancel** when the deal is off. The invoice closes and stops collecting for good. Payments already made stay visible to the customer on the same link, because money they sent you does not disappear when your paperwork does.

```bash
curl -X POST https://api.zevpaycheckout.com/v1/checkout/invoice/{public_id}/cancel \
  -H "x-api-key: sk_live_your_secret_key"
```

Cancelling a part-paid invoice does not refund anything. If you owe the customer money back, refund the specific payment.

## What happens at the due date

By default, nothing stops. The invoice moves to `overdue` and keeps accepting payment, because an overdue invoice is one you still want paid. Closing it would refuse a customer paying a day late, and on an instalment invoice it would strand someone who has already paid a deposit with no way to finish.

If you need a hard stop, say so per invoice:

| `due_date_action` | At the due date |
|-------------------|-----------------|
| `keep_collecting` | Default. Status goes `overdue`, payment still accepted. |
| `pause` | Collection stops. The invoice and its history stay visible. Resume any time. |
| `cancel` | The invoice closes. |

Use `pause` for a price that expires, `cancel` for an event that has passed, and the default for everything else.

## The activity log

Every invoice keeps an append-only record of what was done to it.

```bash
curl https://api.zevpaycheckout.com/v1/checkout/invoice/{public_id}/events \
  -H "x-api-key: sk_live_your_secret_key"
```

```json
{
  "events": [
    {
      "type": "payment_received",
      "title": "Part payment received",
      "description": "600000.00 received from ADAEZE NWOSU. 1200000.00 still outstanding.",
      "actor": "customer",
      "created_at": "2026-03-03T11:24:08.000Z"
    },
    {
      "type": "policy_changed",
      "title": "Payment terms changed",
      "description": "Changed: subsequentPaymentRule",
      "actor": "api",
      "created_at": "2026-03-01T16:02:11.000Z"
    },
    {
      "type": "sent",
      "title": "Invoice sent to customer",
      "description": "Emailed to adaeze@example.com.",
      "actor": "api",
      "created_at": "2026-03-01T15:58:03.000Z"
    }
  ]
}
```

`actor` answers the question that matters when something is disputed: `api` means your integration did it, `dashboard` means a person clicked it, `system` means we did (a due date passing), and `customer` means the payer.

The same log renders on the invoice detail page in your dashboard.

## Webhooks to handle

| Event | When |
|-------|------|
| `invoice.payment_received` | Every payment. Check `is_fully_paid` before fulfilling. |
| `invoice.paid` | The balance reached zero. |
| `invoice.overdue` | The due date passed. |
| `invoice.paused` / `invoice.resumed` | Collection stopped or restarted. |
| `invoice.reminder_sent` | A reminder went out. |
| `invoice.cancelled` | The invoice was closed. |

## Things worth knowing

**Overpayment is refused, not banked.** The most a customer can pay is the outstanding balance. Letting someone overpay an invoice creates a refund, not a sale.

**The amount is locked once chosen.** When a customer says they are paying ₦600,000, the account they are given accepts ₦600,000. They cannot underpay the instalment they just picked, which is the whole point of asking.

**Each payment settles separately.** Same T+1 cycle, same fee structure, same settlement destination as any other payment on that API key. An instalment invoice does not settle as a lump when it completes.

**Partial payments and ZevPay ID.** ZevPay ID is customer-initiated and cannot enforce an amount, so it is hidden on invoices that require an exact payment.
