Invoice Checkout
Create an invoice on your server, share the returned payment link, and let us host the payment page. Invoices add line items, tax, due dates, and optional partial payments on top of the standard checkout. The payment page becomes a receipt once the invoice is paid.
For a single one-off payment without line items, use Standard Checkout instead.
How it works
- Create an invoice with customer details, line items, and a due date.
- Send it to move it from draft to active.
- Share the
payment_urlwith your customer by email, SMS, or in your app. - The customer opens the link and pays by bank transfer or ZevPay ID.
- The status updates automatically. Partial payments are supported.
- Your server receives a webhook for each status change.
Invoices are created with your secret key, so this is a server-side flow.
Create an invoice
curl -X POST https://api.zevpaycheckout.com/v1/checkout/invoice \
-H "Content-Type: application/json" \
-H "x-api-key: sk_test_your_secret_key" \
-d '{
"customer_name": "Chidi Okonkwo",
"customer_email": "chidi@example.com",
"line_items": [
{ "description": "Website Development", "quantity": 1, "unit_price": 35000000 },
{ "description": "Monthly Hosting (12 months)", "quantity": 12, "unit_price": 500000 }
],
"tax_rate": 7.5,
"due_date": "2026-03-31T00:00:00Z",
"note": "Thank you for your business."
}'The response includes a payment_url that you share with your customer. It carries an access token, so treat it as the private pay link for that customer.
{
"public_id": "abc123def456ghi78",
"invoice_number": "INV-202603-001",
"status": "draft",
"total": 44075000,
"payment_url": "https://secure.zevpaycheckout.com/pay/abc123def456ghi78?token=..."
}Amounts are in kobo
Every monetary field in the API is in kobo. 100 kobo is 1 Naira, so 35000000 kobo is ₦350,000.00.
See the full Invoice API reference.
Tax
There are two ways to add tax. Use one or the other, never both in the same request.
Percentage rate
Pass tax_rate as a percentage from 0 to 100 and we compute the tax:
{ "tax_rate": 7.5 }tax_amount is derived as subtotal * rate, and total = subtotal + tax_amount.
Exact amount
If your own system computes the tax and you need the total to land on an exact figure, pass tax_amount in kobo:
{ "tax_amount": 117054 }We use it verbatim: total = subtotal + tax_amount, to the kobo. The invoice page and customer emails show it as a dedicated Tax line, separate from your line items.
One or the other
Sending both tax_rate and tax_amount in the same request returns a 400 error. Requests with neither are untaxed.
Partial payments
By default, an invoice accepts partial payments. The customer can pay any amount up to the remaining balance and return later to pay the rest. Each partial payment fires its own invoice.payment_received webhook with is_partial and is_fully_paid flags.
To require the full balance in one payment, set allow_partial_payments: false:
curl -X POST https://api.zevpaycheckout.com/v1/checkout/invoice \
-H "x-api-key: sk_test_your_secret_key" \
-d '{
"customer_name": "Chidi Okonkwo",
"customer_email": "chidi@example.com",
"due_date": "2026-03-31T00:00:00Z",
"line_items": [{ "description": "One-off service", "quantity": 1, "unit_price": 5000000 }],
"allow_partial_payments": false
}'What this changes
| Behaviour | allow_partial_payments: true (default) | allow_partial_payments: false |
|---|---|---|
| Bank-transfer amount | Any amount up to the remaining balance | Locked to the exact remaining balance. Under and over payments are rejected at the bank. |
| ZevPay ID method | Available | Hidden. ZevPay ID is customer-initiated and cannot enforce an exact amount. |
| Status after a partial payment | Moves to partial. The customer can return via the same link to finish. | Cannot reach partial. Every successful payment is a full payment. |
Always check is_fully_paid
Even with allow_partial_payments: false, check is_fully_paid on invoice.payment_received before you fulfil. It is a cheap safeguard against future changes such as an admin partial-crediting a customer for a service issue.
Post-payment return URL
Set a return_url on the invoice and the payment page shows a "Return to {your business}" button after payment and redirects there after 5 seconds.
curl -X POST https://api.zevpaycheckout.com/v1/checkout/invoice \
-H "x-api-key: sk_test_your_secret_key" \
-d '{
"customer_name": "Chidi Okonkwo",
"customer_email": "chidi@example.com",
"due_date": "2026-03-31T00:00:00Z",
"line_items": [{ "description": "Subscription", "quantity": 1, "unit_price": 1000000 }],
"return_url": "https://yourstore.com/orders/12345/thank-you"
}'On return, we append two query parameters so you can match the payment without a database lookup:
https://yourstore.com/orders/12345/thank-you?status=success&invoice=abc123def456ghi78| Parameter | Description |
|---|---|
status | Currently always success on the post-payment redirect. Leaves room for future states. |
invoice | The invoice's public_id. Use it to look up the full state via the GET invoice endpoint. |
Falls back to your checkout config
If you do not pass return_url, we fall back to the redirect URL in your checkout config (per key, with a merchant default). Set the default in your dashboard under Checkout, then Settings, and pass return_url per invoice only when you need to override it.
Invoice lifecycle
draft -> sent -> partial -> paid
| | |
+--------+--------+--> cancelled
sent or partial -> overdue (automatic, past due date)| Status | Description |
|---|---|
draft | Created but not active. The payment link is not usable yet. |
sent | Active and awaiting payment. |
partial | The customer has made a partial payment. |
paid | Fully paid. |
overdue | Past the due date and not fully paid. Set automatically. |
cancelled | Cancelled by you via the API or dashboard. |
Send an invoice
A draft is not payable. Send it to make the payment link work:
curl -X POST https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78/send \
-H "x-api-key: sk_test_your_secret_key"This moves the invoice from draft to sent and sets issued_at.
Overdue detection
Invoices past their due_date and not fully paid are marked overdue automatically. The check runs hourly. Overdue invoices can still be paid, and become paid when the balance is cleared.
Update a draft
While an invoice is in draft, update any field with the PATCH endpoint. Send only the fields you want to change:
curl -X PATCH https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78 \
-H "Content-Type: application/json" \
-H "x-api-key: sk_test_your_secret_key" \
-d '{ "note": "Updated scope and revised pricing." }'Update rules
- Only draft invoices can be updated. Updating a sent, paid, or cancelled invoice returns a
400error. - If you send
line_items, the whole list is replaced, not merged. Send the complete list every time. - Totals are recalculated when
line_items,tax_rate, ortax_amountchange.
Payment flow
When the customer opens the payment_url:
- The invoice details, total, and remaining balance are shown.
- A checkout session is created for the remaining balance.
- The customer picks a method and pays.
amount_paidis updated.- If the balance is not cleared, the status stays
partialand the same link works again later. - When fully paid, the status becomes
paidand the page becomes a receipt.
Customer emails
We email the invoice customer_email on three events:
| Event | When | Includes |
|---|---|---|
| Invoice sent | You call the send endpoint | Amount due, due date, and a Pay Now link. |
| Partial payment received | The customer paid less than the total | Amount paid, balance remaining, and a link to pay the rest. |
| Payment received in full | Payments reach the total | A receipt link. The same invoice URL serves as a permanent receipt. |
Webhook events
| Event | When |
|---|---|
invoice.created | Invoice created via the API. |
invoice.sent | Invoice sent (draft to sent). |
invoice.payment_received | Payment received, partial or full. |
invoice.paid | Invoice fully paid. |
invoice.overdue | Invoice past its due date. |
invoice.cancelled | Invoice cancelled. |
app.post('/webhooks/zevpay', (req, res) => {
res.status(200).send('OK'); // respond fast, process asynchronously
const { event, data } = req.body;
switch (event) {
case 'invoice.paid':
fulfillOrder(data.public_id, data.amount_paid);
break;
case 'invoice.payment_received':
updatePaymentProgress(data.public_id, data.amount_paid, data.total);
break;
case 'invoice.overdue':
sendPaymentReminder(data.customer_email, data.payment_url);
break;
}
});Always verify the webhook signature. See Verifying Signatures.
Invoice compared to other options
| Invoice | Standard | Inline | |
|---|---|---|---|
| Line items | Yes | Optional | No |
| Tax calculation | Built in | No | No |
| Partial payments | Yes | No | No |
| Expiry | Due date | 30 minutes | 30 minutes |
| Session created by | Your server, invoice API | Your server | The browser SDK |