Invoice
Create programmable invoices via API and let your customers pay through the standard checkout gateway. Invoices support partial payments, tax calculations, line items, and automated status tracking.
Required permission: invoices
How it works
- Create an invoice via API with customer details, line items, and due date
- Send the invoice (transitions from draft to sent)
- Share the
payment_urlwith your customer - Customer opens the payment URL and pays via bank transfer or PayID
- Invoice status updates automatically:
partial→paid - You receive webhook events for each status change
Create Invoice
POST /v1/checkout/invoiceCreates a new invoice in draft status. You must send the invoice before customers can pay.
Authentication
Requires a secret key (sk_*).
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_name | string | Yes | Customer's full name (max 255 characters) |
customer_email | string | Yes | Customer's email address |
customer_address | string | No | Customer's street address |
customer_city | string | No | Customer's city (max 100) |
customer_state | string | No | Customer's state/region (max 100) |
customer_country | string | No | Customer's country (max 100) |
line_items | array | Yes | Array of line item objects (at least one) |
line_items[].description | string | Yes | Description of the item (max 500) |
line_items[].quantity | number | Yes | Quantity (supports decimals, min 0.01) |
line_items[].unit_price | integer | Yes | Unit price in kobo |
tax_rate | number | No | Tax rate as percentage (0–100, e.g., 7.5 for 7.5%). Default: 0. Mutually exclusive with tax_amount |
tax_amount | integer | No | Flat tax amount in kobo, taken verbatim: total = subtotal + tax_amount, exactly. Use when your system computes tax itself. Mutually exclusive with tax_rate |
due_date | string | Yes | Due date in ISO 8601 format |
invoice_number | string | No | Custom invoice number (max 64). Auto-generated if omitted (e.g., INV-202603-001) |
reference | string | No | Your unique reference for reconciliation (max 255) |
note | string | No | Note to display on the invoice payment page (max 2000) |
payment_methods | string[] | No | Methods to offer on this invoice's checkout: bank_transfer, payid, card, crypto. Omit for your key's configuration. See Choosing payment methods |
crypto_assets | string[] | No | Narrow the crypto assets offered, e.g. ["USDT:TRON"] |
metadata | object | No | Custom key-value data stored with the invoice |
Example request
curl -X POST https://api.zevpaycheckout.com/v1/checkout/invoice \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_test_your_secret_key" \
-d '{
"customer_name": "Chidi Okonkwo",
"customer_email": "chidi@example.com",
"customer_address": "15 Admiralty Way, Lekki Phase 1",
"customer_city": "Lagos",
"customer_state": "Lagos",
"customer_country": "Nigeria",
"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",
"reference": "PROJECT-2026-001",
"note": "Thank you for your business!",
"metadata": {
"project_id": "PRJ-123"
}
}'const response = await fetch(
"https://api.zevpaycheckout.com/v1/checkout/invoice",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer sk_test_your_secret_key",
},
body: JSON.stringify({
customer_name: "Chidi Okonkwo",
customer_email: "chidi@example.com",
customer_address: "15 Admiralty Way, Lekki Phase 1",
customer_city: "Lagos",
customer_state: "Lagos",
customer_country: "Nigeria",
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",
reference: "PROJECT-2026-001",
note: "Thank you for your business!",
metadata: { project_id: "PRJ-123" },
}),
}
);
const data = await response.json();import requests
response = requests.post(
"https://api.zevpaycheckout.com/v1/checkout/invoice",
headers={
"Content-Type": "application/json",
"Authorization": "Bearer sk_test_your_secret_key",
},
json={
"customer_name": "Chidi Okonkwo",
"customer_email": "chidi@example.com",
"customer_address": "15 Admiralty Way, Lekki Phase 1",
"customer_city": "Lagos",
"customer_state": "Lagos",
"customer_country": "Nigeria",
"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",
"reference": "PROJECT-2026-001",
"note": "Thank you for your business!",
"metadata": {"project_id": "PRJ-123"},
},
)
data = response.json()Response
{
"success": true,
"data": {
"public_id": "abc123def456ghi78",
"invoice_number": "INV-202603-001",
"status": "draft",
"customer_name": "Chidi Okonkwo",
"customer_email": "chidi@example.com",
"customer_address": "15 Admiralty Way, Lekki Phase 1",
"customer_city": "Lagos",
"customer_state": "Lagos",
"customer_country": "Nigeria",
"subtotal": 41000000,
"tax_rate": 7.5,
"tax_amount": 3075000,
"total": 44075000,
"amount_paid": 0,
"currency": "NGN",
"due_date": "2026-03-31T00:00:00.000Z",
"issued_at": null,
"paid_at": null,
"cancelled_at": null,
"merchant_reference": "PROJECT-2026-001",
"metadata": { "project_id": "PRJ-123" },
"note": "Thank you for your business!",
"payment_url": "https://invoice.zevpaycheckout.com/pay/abc123def456ghi78?token=...",
"created_at": "2026-03-08T10:00:00.000Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
public_id | string | Unique public identifier for the invoice |
invoice_number | string | Human-readable invoice number (auto-generated or custom) |
status | string | Current status: draft, sent, partial, paid, overdue, cancelled |
customer_name | string | Customer's full name |
customer_email | string | Customer's email address |
customer_address | string | null | Customer's street address |
customer_city | string | null | Customer's city |
customer_state | string | null | Customer's state/region |
customer_country | string | null | Customer's country |
subtotal | integer | Subtotal in kobo (sum of line items, before tax) |
tax_rate | number | Tax rate as a percentage (e.g., 7.5). 0 when the invoice uses a flat tax_amount |
tax_amount | integer | Tax amount in kobo (derived from tax_rate, or the flat amount you supplied) |
total | integer | Total amount in kobo (subtotal + tax) |
amount_paid | integer | Cumulative amount paid so far, in kobo |
currency | string | Currency code. Always "NGN" |
due_date | string | Due date in ISO 8601 format |
issued_at | string | null | Timestamp when invoice was sent (ISO 8601). null for drafts |
paid_at | string | null | Timestamp when invoice was fully paid. null until paid |
cancelled_at | string | null | Timestamp when invoice was cancelled. null unless cancelled |
merchant_reference | string | null | Your custom reference, if provided |
metadata | object | null | Your custom key-value data, if provided |
note | string | null | Invoice note, if provided |
payment_url | string | URL for the customer to view and pay the invoice |
created_at | string | Invoice creation timestamp (ISO 8601) |
Amounts
All monetary amounts in the API are in kobo (1 NGN = 100 kobo). For example, 35000000 kobo = ₦350,000.00.
Payment URL
Store the public_id (and optionally the payment_url) in your database. The payment_url resolves to the payment page when the invoice is payable, or shows a receipt when paid.
Update Invoice
PATCH /v1/checkout/invoice/:publicIdUpdates an invoice. What you may change depends on whether it has been sent.
Draft invoices are fully editable, including line items, tax and the customer.
Sent invoices can still have their terms changed, because things like extending a due date are ordinary and should not cost you the invoice's history or its link:
| Editable after sending | Not editable after sending |
|---|---|
due_date | line_items |
note | tax_rate, tax_amount |
allow_partial_payments | customer_name, customer_email, customer_address |
minimum_initial_payment | invoice_number |
subsequent_payment_rule | |
due_date_action | |
payment_methods, crypto_assets | |
return_url, merchant_reference, metadata |
The second column is the money and who owes it. Changing that after a customer has agreed the invoice, and possibly already paid part of it, produces a different document rather than an edited one. Cancel and issue a new invoice instead.
Paid and cancelled invoices cannot be edited at all.
Extending a due date
The common case. Send the new date and nothing else:
curl -X PATCH https://api.zevpaycheckout.com/v1/checkout/invoice/{public_id} \
-H "x-api-key: sk_live_your_secret_key" \
-H "Content-Type: application/json" \
-d '{ "due_date": "2026-07-31T00:00:00Z" }'An invoice that had already gone overdue returns to sent, or to partial if part of it is paid, as soon as the new date is in the future. The customer stops seeing a past-due invoice immediately rather than waiting for the next sweep.
The change is recorded on the invoice's activity log as "Due date extended", with the old and new dates, so there is a record of when the terms moved and what moved them.
Authentication
Requires a secret key (sk_*).
Path parameters
| Parameter | Type | Description |
|---|---|---|
publicId | string | The invoice's public_id |
Request body
All fields are optional. Only include the fields you want to change.
| Parameter | Type | Description |
|---|---|---|
customer_name | string | Customer's full name |
customer_email | string | Customer's email address |
customer_address | string | Customer's street address |
customer_city | string | Customer's city |
customer_state | string | Customer's state/region |
customer_country | string | Customer's country |
line_items | array | Replacement line items (replaces all existing items) |
line_items[].description | string | Item description |
line_items[].quantity | number | Quantity |
line_items[].unit_price | integer | Unit price in kobo |
tax_rate | number | Tax rate as percentage (0–100). Mutually exclusive with tax_amount |
tax_amount | integer | Flat tax amount in kobo (total = subtotal + tax_amount, exactly). Mutually exclusive with tax_rate |
due_date | string | Due date in ISO 8601 format |
invoice_number | string | Custom invoice number |
note | string | Invoice note |
return_url | string | URL the customer lands on after paying |
allow_partial_payments | boolean | Whether the customer may pay less than the remaining balance |
payment_methods | string[] | Methods to offer on this invoice's checkout. Send [] to fall back to your key's configuration |
crypto_assets | string[] | Narrow the crypto assets offered. Send [] to offer every eligible asset |
metadata | object | Custom key-value data |
Line items replacement
When you include line_items in an update, all existing line items are replaced, not merged. Always send the complete list of line items you want on the invoice.
Example request
curl -X PATCH https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_test_your_secret_key" \
-d '{
"note": "Updated: payment due by end of month",
"due_date": "2026-04-15T00:00:00Z"
}'const response = await fetch(
"https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78",
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer sk_test_your_secret_key",
},
body: JSON.stringify({
note: "Updated: payment due by end of month",
due_date: "2026-04-15T00:00:00Z",
}),
}
);
const data = await response.json();import requests
response = requests.patch(
"https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78",
headers={
"Content-Type": "application/json",
"Authorization": "Bearer sk_test_your_secret_key",
},
json={
"note": "Updated: payment due by end of month",
"due_date": "2026-04-15T00:00:00Z",
},
)
data = response.json()Response
Returns the updated invoice object (same shape as Create Invoice response).
Errors
| Status | Message | Cause |
|---|---|---|
| 404 | Invoice not found | Invoice does not exist or does not belong to your account |
| 400 | Cannot edit an invoice with status "paid" / "cancelled" | The invoice is closed |
| 400 | An invoice that has been sent cannot change | You tried to edit line items, tax or the customer after sending. Adjust the terms instead, or cancel and re-issue |
| 400 | Invoice total must be greater than zero | Updated line items result in zero or negative total |
| 400 | minimum_initial_payment cannot be more than the invoice total | The deposit exceeds what is being billed |
| 400 | minimum_initial_payment cannot be changed after the first payment has been made | The deposit rule only applies to the first payment, which has already happened |
List Invoices
GET /v1/checkout/invoiceReturns a paginated list of your invoices, ordered by creation date (newest first).
Authentication
Requires a secret key (sk_*).
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (min 1). Default: 1 |
page_size | integer | No | Items per page (1–100). Default: 20 |
status | string | No | Filter by status: draft, sent, partial, paid, overdue, cancelled |
customer_email | string | No | Filter by exact customer email address |
search | string | No | Search by invoice number, customer name, or customer email (partial match) |
customer_email vs search
customer_email performs an exact match on the email field. search performs a partial match (contains) across invoice number, customer name, and email. Use customer_email when you know the exact email; use search for flexible lookups.
Example request
# List all sent invoices
curl "https://api.zevpaycheckout.com/v1/checkout/invoice?status=sent&page=1&page_size=20" \
-H "Authorization: Bearer sk_test_your_secret_key"
# Filter by customer email
curl "https://api.zevpaycheckout.com/v1/checkout/invoice?customer_email=chidi@example.com" \
-H "Authorization: Bearer sk_test_your_secret_key"// List all sent invoices
const response = await fetch(
"https://api.zevpaycheckout.com/v1/checkout/invoice?status=sent&page=1",
{
headers: { "Authorization": "Bearer sk_test_your_secret_key" },
}
);
const data = await response.json();
// Filter by customer email
const byEmail = await fetch(
"https://api.zevpaycheckout.com/v1/checkout/invoice?customer_email=chidi@example.com",
{
headers: { "Authorization": "Bearer sk_test_your_secret_key" },
}
);import requests
# List all sent invoices
response = requests.get(
"https://api.zevpaycheckout.com/v1/checkout/invoice",
headers={"Authorization": "Bearer sk_test_your_secret_key"},
params={"status": "sent", "page": 1, "page_size": 20},
)
data = response.json()
# Filter by customer email
by_email = requests.get(
"https://api.zevpaycheckout.com/v1/checkout/invoice",
headers={"Authorization": "Bearer sk_test_your_secret_key"},
params={"customer_email": "chidi@example.com"},
)Response
{
"success": true,
"data": {
"items": [
{
"public_id": "abc123def456ghi78",
"invoice_number": "INV-202603-001",
"status": "sent",
"customer_name": "Chidi Okonkwo",
"customer_email": "chidi@example.com",
"customer_address": "15 Admiralty Way, Lekki Phase 1",
"customer_city": "Lagos",
"customer_state": "Lagos",
"customer_country": "Nigeria",
"subtotal": 41000000,
"tax_rate": 7.5,
"tax_amount": 3075000,
"total": 44075000,
"amount_paid": 0,
"currency": "NGN",
"due_date": "2026-03-31T00:00:00.000Z",
"issued_at": "2026-03-08T10:05:00.000Z",
"paid_at": null,
"cancelled_at": null,
"merchant_reference": "PROJECT-2026-001",
"metadata": { "project_id": "PRJ-123" },
"note": "Thank you for your business!",
"payment_url": "https://invoice.zevpaycheckout.com/pay/abc123def456ghi78?token=...",
"created_at": "2026-03-08T10:00:00.000Z"
}
],
"total": 1,
"page": 1,
"page_size": 20,
"total_pages": 1
}
}Pagination fields
| Field | Type | Description |
|---|---|---|
items | array | Array of invoice objects (same fields as Create response) |
total | integer | Total number of invoices matching the filters |
page | integer | Current page number |
page_size | integer | Number of items per page |
total_pages | integer | Total number of pages |
Get Invoice
GET /v1/checkout/invoice/:publicIdReturns a single invoice with its line items and payment history.
This is the endpoint to reach for when your records and ours disagree. One call tells you what the invoice is worth, what has been paid, what is still owed, and every payment that made up the difference, each with the reference of the transaction it settled as.
{
"status": "partial",
"total": 180000000,
"amount_paid": 120000000,
"amount_outstanding": 60000000,
"collecting": true,
"allow_partial_payments": true,
"minimum_initial_payment": 60000000,
"subsequent_payment_rule": "flexible",
"payments": [
{
"payment_id": "ipy_7Bn21kFd5Qa",
"amount": 60000000,
"paid_at": "2026-04-02T09:15:44.000Z",
"payer_name": "ADAEZE NWOSU",
"payment_channel": "bank_transfer",
"transaction": {
"reference": "ZVP-CKOTX-3d81be",
"fees": 200000,
"net_amount": 59800000,
"settlement_state": "paid_out"
}
}
]
}amount_outstanding is served rather than left to you to subtract, because client-side arithmetic on money is where rounding bugs start.
Recovering from a missed webhook
Webhooks are delivered at least once and retried, but a receiver that was down, a deploy that dropped a request, or a bug in your handler all end the same way: your side thinks an invoice is unpaid when it is not.
You do not need a special endpoint for that. Fetch the invoice:
statusandamount_outstandingtell you where it stands now.payments[]is the full history. Anything in it that is not in your ledger is what you missed.payments[].transaction.referenceis the same reference that appeared on theinvoice.payment_receivedwebhook and on your settlement, so it is the key to match on.payments[].payment_ididentifies one instalment for good, andGET /v1/checkout/invoice/{public_id}/payments/{payment_id}verifies a single one.
Treat the fetch as authoritative and your webhook handler as an optimisation. A daily reconciliation pass over open invoices costs one request each and closes the gap without anyone noticing there was one. The same pattern applies to every payment type we offer: see Recovering from a missed webhook for the equivalent endpoint on sessions, static accounts, virtual accounts and PayIDs, plus a reconciliation pass you can copy.
Match on the transaction reference, not the amount
On a flexible instalment invoice a customer may pay the same amount twice. Amounts are not unique; reference and payment_id are.
Authentication
Requires a secret key (sk_*).
Path parameters
| Parameter | Type | Description |
|---|---|---|
publicId | string | The invoice's public_id |
Example request
curl https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78 \
-H "Authorization: Bearer sk_test_your_secret_key"const response = await fetch(
"https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78",
{
headers: { "Authorization": "Bearer sk_test_your_secret_key" },
}
);
const data = await response.json();import requests
response = requests.get(
"https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78",
headers={"Authorization": "Bearer sk_test_your_secret_key"},
)
data = response.json()Response
{
"success": true,
"data": {
"public_id": "abc123def456ghi78",
"invoice_number": "INV-202603-001",
"status": "partial",
"customer_name": "Chidi Okonkwo",
"customer_email": "chidi@example.com",
"customer_address": "15 Admiralty Way, Lekki Phase 1",
"customer_city": "Lagos",
"customer_state": "Lagos",
"customer_country": "Nigeria",
"subtotal": 41000000,
"tax_rate": 7.5,
"tax_amount": 3075000,
"total": 44075000,
"amount_paid": 20000000,
"currency": "NGN",
"due_date": "2026-03-31T00:00:00.000Z",
"issued_at": "2026-03-08T10:05:00.000Z",
"paid_at": null,
"cancelled_at": null,
"merchant_reference": "PROJECT-2026-001",
"metadata": { "project_id": "PRJ-123" },
"note": "Thank you for your business!",
"payment_url": "https://invoice.zevpaycheckout.com/pay/abc123def456ghi78?token=...",
"created_at": "2026-03-08T10:00:00.000Z",
"line_items": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"description": "Website Development",
"quantity": 1,
"unit_price": 35000000
},
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"description": "Monthly Hosting (12 months)",
"quantity": 12,
"unit_price": 500000
}
],
"payments": [
{
"id": "550e8400-e29b-41d4-a716-446655440003",
"amount": 20000000,
"paid_at": "2026-03-10T14:30:00.000Z",
"payer_name": "CHIDI OKONKWO",
"payment_channel": "bank_transfer"
}
]
}
}Line item fields
| Field | Type | Description |
|---|---|---|
id | string | Line item UUID |
description | string | Item description |
quantity | number | Quantity |
unit_price | integer | Unit price in kobo |
Payment fields
| Field | Type | Description |
|---|---|---|
id | string | Payment record UUID |
amount | integer | Payment amount in kobo |
paid_at | string | Payment timestamp (ISO 8601) |
payer_name | string | null | Name of the person who made the payment |
payment_channel | string | null | Payment channel: "bank_transfer" or "payid" |
List Invoice Payments
GET /v1/checkout/invoice/:publicId/paymentsEvery payment made against an invoice, newest first, each joined to the transaction it settled as. Requires a secret key.
GET /v1/checkout/invoice/:publicId already embeds this. Use this endpoint when you want the payments without the line items and customer detail, for example in a nightly reconciliation job over many invoices.
{
"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",
"public_id": "ctx_5Km91xRe7Yb",
"gross_amount": 60000000,
"fees": 200000,
"net_amount": 59800000,
"settlement_state": "paid_out",
"settled_at": "2026-04-03T12:00:00.000Z"
}
}
]
}Each instalment settles separately, on the same T+1 cycle as any other payment, which is why each carries its own settlement_state rather than the invoice carrying one.
Verify One Payment
GET /v1/checkout/invoice/:publicId/payments/:paymentIdThe same payment object for a single instalment, plus the invoice's balance. Requires a secret key.
Use it when you have a payment_id from a webhook and want to confirm it against us before fulfilling, which is the safe pattern when a webhook arrives from an unverified source or after a retry you are unsure about.
Invoice Activity Log
GET /v1/checkout/invoice/:publicId/eventsAn append-only record of what has been done to the invoice: created, sent, terms changed with the fields that moved, due date extended, reminders, each payment, paused, resumed, cancelled, overdue. Requires a secret key.
Each entry carries an actor:
actor | Meaning |
|---|---|
api | Your integration did it |
dashboard | Someone did it by hand in the dashboard |
system | We did it, such as a due date passing |
customer | The payer did it |
That field is the one that settles arguments. When a customer says the terms changed after they agreed them, this is where you look.
Send Invoice
Transitions an invoice from draft to sent. Sets the issued_at timestamp and makes the payment_url active.
POST /v1/checkout/invoice/:publicId/sendAuthentication
Requires a secret key (sk_*).
Path parameters
| Parameter | Type | Description |
|---|---|---|
publicId | string | The invoice's public_id |
Example request
curl -X POST https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78/send \
-H "Authorization: Bearer sk_test_your_secret_key"const response = await fetch(
"https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78/send",
{
method: "POST",
headers: { "Authorization": "Bearer sk_test_your_secret_key" },
}
);
const data = await response.json();import requests
response = requests.post(
"https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78/send",
headers={"Authorization": "Bearer sk_test_your_secret_key"},
)
data = response.json()Response
Returns the updated invoice object with status: "sent" and issued_at populated.
Errors
| Status | Message | Cause |
|---|---|---|
| 404 | Invoice not found | Invoice does not exist or does not belong to your account |
| 400 | Only draft invoices can be sent | Invoice is not in draft status |
Send Reminder
POST /v1/checkout/invoice/:publicId/remindRe-sends the invoice email to the customer. Requires a secret key.
On a part-paid invoice the reminder chases the outstanding balance rather than 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 sends mail to your customer, so a loop in your automation is a loop in their inbox.
{
"public_id": "inv_8Kd93mQx2Lp",
"invoice_number": "INV-2026-0041",
"reminder_count": 3,
"last_reminder_sent_at": "2026-05-02T09:00:00.000Z"
}| Status | Message | Cause |
|---|---|---|
| 400 | A reminder was sent recently. Try again in N minute(s) | Rate limit |
| 400 | Cannot send a reminder: | The invoice is paid, cancelled, draft or paused |
Pause and Resume Collection
POST /v1/checkout/invoice/:publicId/pause
POST /v1/checkout/invoice/:publicId/resumeStops and restarts payment on an invoice without closing it. Requires a secret key.
Pausing is not cancelling. The invoice keeps the status it earned, keeps its link, and the customer can still open it and see everything they have already paid. Only new payment stops. Use it for a dispute, a delivery problem, or a customer who asked you to hold.
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" }'reason is optional and is shown to the customer on the invoice page. Both calls are idempotent.
{
"public_id": "inv_8Kd93mQx2Lp",
"status": "partial",
"collecting": false,
"paused_at": "2026-05-04T10:12:00.000Z",
"paused_reason": "On hold while we agree the revised spec"
}Fires invoice.paused and invoice.resumed.
Cancel Invoice
Cancels an invoice. Only invoices with status draft, sent, partial, or overdue can be cancelled. The payment link stops accepting payment and invoice.cancelled fires.
Balance settled outside ZevPay?
If you are cancelling because part of the invoice was paid where we cannot see it, collect the remainder with a checkout session on the same secret key. See collecting a balance settled outside ZevPay.
POST /v1/checkout/invoice/:publicId/cancelAuthentication
Requires a secret key (sk_*).
Path parameters
| Parameter | Type | Description |
|---|---|---|
publicId | string | The invoice's public_id |
Example request
curl -X POST https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78/cancel \
-H "Authorization: Bearer sk_test_your_secret_key"const response = await fetch(
"https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78/cancel",
{
method: "POST",
headers: { "Authorization": "Bearer sk_test_your_secret_key" },
}
);
const data = await response.json();import requests
response = requests.post(
"https://api.zevpaycheckout.com/v1/checkout/invoice/abc123def456ghi78/cancel",
headers={"Authorization": "Bearer sk_test_your_secret_key"},
)
data = response.json()Response
Returns the updated invoice object with status: "cancelled" and cancelled_at populated.
Errors
| Status | Message | Cause |
|---|---|---|
| 404 | Invoice not found | Invoice does not exist or does not belong to your account |
| 400 | Cannot cancel an invoice with status "paid" | Invoice is already fully paid |
Invoice Status Lifecycle
draft → sent → partial → paid
↓ ↓ ↓
└───────┴───────┴──→ cancelled
sent/partial → overdue (automatic, when past due date)
overdue → paid (if payment received after due date)
overdue → cancelled| Status | Description |
|---|---|
draft | Invoice created but not yet sent to customer. Payment URL is not usable |
sent | Invoice sent, awaiting payment. Payment URL is active |
partial | Customer has made a partial payment |
paid | Invoice fully paid |
overdue | Past due date, not fully paid (set automatically by hourly check) |
cancelled | Invoice cancelled by merchant |
Webhook Events
Subscribe to these events via your webhook configuration:
| Event | Description |
|---|---|
invoice.created | Invoice created via API |
invoice.sent | Invoice status changed to sent |
invoice.payment_received | Payment received (partial or full) |
invoice.paid | Invoice fully paid |
invoice.overdue | Invoice past due date, not fully paid |
invoice.paused | Collection stopped without closing the invoice |
invoice.resumed | Collection restarted |
invoice.reminder_sent | A reminder email went out |
invoice.cancelled | Invoice cancelled |
On an instalment invoice, invoice.payment_received fires every time
It is not a signal that the invoice is settled. Fulfil on is_fully_paid, or on invoice.paid.
See the full Webhook Events Reference for complete payload documentation.
Example webhook payload
{
"event": "invoice.payment_received",
"data": {
"public_id": "abc123def456ghi78",
"invoice_number": "INV-202603-001",
"status": "partial",
"customer_name": "Chidi Okonkwo",
"customer_email": "chidi@example.com",
"subtotal": 41000000,
"tax_rate": 7.5,
"tax_amount": 3075000,
"total": 44075000,
"amount_paid": 20000000,
"currency": "NGN",
"due_date": "2026-03-31T00:00:00.000Z",
"issued_at": "2026-03-08T10:05:00.000Z",
"paid_at": null,
"merchant_reference": "PROJECT-2026-001",
"metadata": { "project_id": "PRJ-123" },
"payment_url": "https://invoice.zevpaycheckout.com/pay/abc123def456ghi78?token=...",
"payment_amount": 20000000,
"amount_remaining": 24075000,
"payer_name": "CHIDI OKONKWO",
"payment_channel": "bank_transfer"
}
}Error Responses
All error responses follow this format:
{
"success": false,
"error": {
"code": "HTTP_ERROR",
"message": "An invoice that has been sent cannot change lineItems."
},
"correlationId": "b7f3c2a1-..."
}The readable message is at error.message. Show that to whoever is going to act on it, and quote the correlationId if you contact support.
Common errors
| Status | Message | Endpoint |
|---|---|---|
| 401 | API key is required | All: missing or invalid API key |
| 403 | This endpoint requires a secret key | All: using a public key instead of secret key |
| 403 | Invoices are only available for business accounts | Create: personal account trying to create invoices |
| 400 | At least one line item is required | Create: empty line_items array |
| 400 | Invoice total must be greater than zero | Create/Update: calculated total is zero or negative |
| 400 | Invalid due date format | Create/Update: invalid ISO date string |
| 400 | An invoice that has been sent cannot change | Update: financial fields edited after sending. Terms such as due_date remain editable |
| 400 | Only draft invoices can be sent | Send: invoice is not in draft status |
| 404 | Invoice not found | Get/Update/Send/Cancel: invalid public_id or not your invoice |