Skip to content

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.

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:

FieldWhat it does
allow_partial_paymentsTurns instalments on. Without it the invoice is pay-in-full.
minimum_initial_paymentThe deposit. The first payment must be at least this. They may pay more.
subsequent_payment_ruleflexible lets them pay any amount afterwards. full_balance means the rest must come in one payment.
due_date_actionWhat 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:

RuleWhat the customer may pay next
full_balanceExactly ₦1,200,000
flexibleAny 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_actionAt the due date
keep_collectingDefault. Status goes overdue, payment still accepted.
pauseCollection stops. The invoice and its history stay visible. Resume any time.
cancelThe 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

EventWhen
invoice.payment_receivedEvery payment. Check is_fully_paid before fulfilling.
invoice.paidThe balance reached zero.
invoice.overdueThe due date passed.
invoice.paused / invoice.resumedCollection stopped or restarted.
invoice.reminder_sentA reminder went out.
invoice.cancelledThe 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.

ZevPay Checkout Developer Documentation