Skip to content

Invoice reference

The invoice object, the amounts it computes, and the eight calls that manage it.

An invoice is a document you issue and your customer pays on a page Wajub hosts. This page is the reference for the invoice object and its calls; Billing & subscriptions walks through the cycle.

Invoicing is a live-mode product gated on your plan. A sandbox key gets 403 This feature is only available in live mode., a plan without the feature gets 403 Invoicing is not available on your plan., and a plan with a lifetime cap gets 403 You have reached the maximum number of invoices for your plan. once you hit it. Only documents of type invoice count toward that cap.

The eight calls

Everything the API does with invoices is here. /send is the call that e-mails an invoice, its PDF is a public link (below), and no endpoint charges a customer.

CallWhat it does
GET /invoicesList, filter and sort your invoices
POST /invoicesCreate one, always as draft
GET /invoices/{id}Retrieve one
PUT /invoices/{id}Replace one, only while it is a draft
DELETE /invoices/{id}Delete one, only while it is a draft
POST /invoices/{id}/sendNumber it, e-mail it with its PDF, and open its public page
POST /invoices/{id}/mark-paidRecord a payment you collected elsewhere
POST /invoices/{id}/cancelClose it out

{id} is the id the API returns, the inv_ string.

The invoice object

This is what every call returns. items is present whenever the invoice was loaded with its lines, which is all eight of them.

Response · an invoice with one taxed line
{
"id": "inv_LRQqYvlhrgUOE225KMKU",
"invoice_number": "INV-2026-0001",
"document_type": "invoice",
"status": "sent",
"customer": "cus_7Kq2mX9vL4tRb3nP8sZcWd1e",
"customer_name": "Amina Traoré",
"customer_email": "amina@example.com",
"customer_company_name": "Traoré & Fils",
"customer_phone": "+237670000000",
"customer_address": "BP 1204, Douala",
"invoice_date": "2026-09-12",
"due_date": "2026-10-12",
"currency": "XAF",
"language": "fr",
"subtotal": 300000,
"discount_amount": 0,
"has_global_discount": false,
"global_discount_type": null,
"global_discount_value": null,
"tax_amount": 57750,
"total": 357750,
"amount_paid": 0,
"amount_due": 357750,
"payment_type": "full",
"is_recurring": false,
"recurring_interval": null,
"next_invoice_date": null,
"recurring_parent": null,
"notes": null,
"terms": null,
"footer": null,
"accept": {
},
"sent_at": "2026-09-12T09:14:02+00:00",
"paid_at": null,
"cancelled_at": null,
"items": [
{
"id": "00000000-0000-0000-0000-000000000001",
"name": "Consulting service",
"description": null,
"quantity": 2,
"unit": null,
"unit_price": 150000,
"tax_rate": 19.25,
"tax_amount": 57750,
"discount_type": null,
"discount_value": 0,
"discount_amount": 0,
"subtotal": 300000,
"total": 357750,
"sort_order": 0
}
],
"created_at": "2026-09-12T09:10:44+00:00",
"updated_at": "2026-09-12T09:14:02+00:00"
}

invoice_number is null on a draft and assigned the first time the invoice is sent. It comes from your account's numbering, the same sequence the Dashboard draws from, whether you send the invoice here or there: the prefix (INV- unless you changed it), the year, then a four-digit counter, as set in the Dashboard's invoice settings. The counter restarts at 0001 each year unless you turned that off, and a number is unique within your account. status is one of draft, sent, viewed, partial, paid, overdue, cancelled or refunded; the transitions between them and the webhooks they emit are on Billing & subscriptions.

customer is the customer the invoice is addressed to, by the cus_ id the Customers API returns, and null when you addressed it to a name and an e-mail only. Match on customer.

language is the language of the invoice's e-mail and PDF, fr or en, recorded when the invoice is created, here or in the Dashboard. accept is the restriction on the methods the payer is offered on the invoice's page, {} when there is none.

Two fields are computed rather than stored. amount_due is always total minus amount_paid, floored at zero, so it never goes negative on an overpayment. amount_paid only moves through mark-paid, through a payment made on the hosted page, or through a credit note issued from the Dashboard, which lowers it by its amount. A payment on the page counts all three: the invoice's online payments, plus what mark-paid recorded, less its credit notes. An invoice with a 1 000 credit note turns paid only once 1 000 more than its total has come in.

Creating one

customer_name, items, invoice_date and currency are the four required fields. Everything else is optional, and the invoice is always created as draft whatever you send.

POSThttps://api.wajub.com/invoices
customer_namestringrequired
Who the invoice is addressed to. Required even when you pass customer_id.
customer_idstringoptional
One of your customers, by its cus_ id. Its name, email, company, phone and address are copied onto the invoice and override the customer_* fields you sent. A customer that is not yours answers 422.
customer_emailstringoptional
Where /send e-mails the invoice and its PDF, and where its [reminders](/billing/invoice/reminders) go.
customer_company_namestringoptional
Printed on the document alongside the name.
customer_phonestringoptional
Printed on the document.
customer_addressstringoptional
Printed on the document.
itemsarrayrequired
At least one line. See the line object below.
invoice_datedaterequired
The issue date printed on the document.
due_datedateoptional
When payment is expected. Must be on or after invoice_date.
payment_terms_daysintegeroptional
Sets due_date to that many days from today, and only when due_date is absent. Counted from today, not from invoice_date.
currencystringrequired
A currency Wajub supports: XAF, XOF, NGN and the rest of the [supported list](/resources/coverage). Any other code answers 422.
languageenumoptional
fr or en: the language of the e-mail /send sends, of the reminders and of the PDF. Defaults to the preferred language of the customer customer_id names, else your account's language ([**Checkout — Language**](https://dashboard.wajub.com/settings/regional/language)), else en. Any other value answers 422.
acceptobjectoptional
Restricts the methods the payer is offered on the invoice's page, as on [POST /payments](/payments/payment-methods#choosing-what-a-payment-accepts).
is_recurringbooleanoptionaldefault : false
Marks the invoice as a template. See [Recurring invoices](/billing/invoice/recurring).
recurring_intervalenumoptional
daily, weekly, monthly, quarterly or yearly.
payment_typeenumoptionaldefault : full
full, split or milestone. Instalments are covered below.
payment_schedulesarrayoptional
The instalments, when payment_type is not full.
notesstringoptional
Free text printed on the document, 5000 characters.
termsstringoptional
Terms and conditions, 5000 characters.
footerstringoptional
Footer line, 500 characters.
template_idstring (uuid)optional
An invoice template created in the Dashboard. It drives the printed layout only.
document_typeenumoptionaldefault : invoice
invoice, quote or estimate. A quote and an estimate stay drafts: see [Quotes and estimates](#quotes-and-estimates).
global_discount_typeenumoptional
percentage or fixed: a discount on the whole invoice, sent with global_discount_value. See [An invoice-wide discount](#an-invoice-wide-discount).
global_discount_valuenumberoptional
The percentage, up to 100, or the amount when the type is fixed. Sent without global_discount_type, or the other way round, it answers 422.
has_global_discountbooleanoptional
true asks for both fields above. false removes the invoice-wide discount, whatever they say.

Each entry in items is its own object.

namestringrequired
The line label.
descriptionstringoptional
A second line under the label.
quantitynumberrequired
How many.
unit_priceintegerrequired
Price for one, in whole units of the currency.
unitstringoptional
A label such as hour or day. Cosmetic.
tax_ratenumberoptional
Percentage from 0 to 100, applied on top of the line.
discount_typeenumoptional
percentage or fixed.
discount_valuenumberoptional
The percentage, up to 100, or the amount when the type is fixed, up to the line's quantity × unit_price: a discount never takes a line below zero. Beyond, 422 on items.N.discount_value.
curl https://api.wajub.com/invoices \
  -H "Authorization: sk.xxxxxxxxxxxxxxxx…" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_name": "Traoré & Fils",
    "customer_email": "amina@example.com",
    "invoice_date": "2026-09-12",
    "payment_terms_days": 30,
    "currency": "XAF",
    "items": [
      { "name": "Consulting service", "quantity": 2, "unit_price": 150000, "tax_rate": 19.25 },
      { "name": "Onboarding", "quantity": 1, "unit_price": 100000, "discount_type": "percentage", "discount_value": 10 }
    ]
  }'

Creation is the only invoice call that honours Idempotency-Key. Replaying a create with the same key returns the first invoice instead of issuing a second one; replaying a send or a mark-paid runs again.

Amounts are in the major unit

unit_price, subtotal, discount_amount, tax_amount, total and amount_paid are all in the major unit of the currency: 150000 means one hundred and fifty thousand francs, not fifteen hundred.

Decimals are accepted. "unit_price": 1500.50 is stored as sent, and a rate that does not divide the line keeps its decimals: 19.25% of 1000 is 192.5. Wajub keeps two decimals on every amount.

How a line is computed

The line does its own arithmetic on every save, in this order. You send the first four values; the other four are computed and returned.

subtotal        = quantity × unit_price
discount_amount = discount_type = percentage  →  subtotal × discount_value / 100
                  discount_type = fixed       →  discount_value
                  otherwise                   →  0
taxable         = subtotal − discount_amount
tax_amount      = taxable × tax_rate / 100
total           = taxable + tax_amount

Tax is exclusive: it is added on top of the line, never carved out of it. And it is computed after the discount, so a 10% discount on a taxed line lowers the tax with it.

The invoice then sums the lines, each net of its discount.

subtotal        = Σ (line subtotal − line discount_amount)
discount_amount = the invoice-wide discount, 0 without one
tax_amount      = Σ line tax_amount
total           = Σ line total − discount_amount
amount_due      = max(0, total − amount_paid)

A line discount comes off the invoice's subtotal, total and amount_due, and its tax with it. The invoice's discount_amount is the invoice-wide discount only: the lines' own are in items[].discount_amount.

An invoice-wide discount

global_discount_type and global_discount_value take a discount off the whole invoice, after the line discounts. A percentage is reckoned on the subtotal, the lines before tax; a fixed amount is taken as it is, up to that subtotal. The tax stays the lines' own, and total is the lines' totals less the discount, never below zero.

Two lines and 10% off the invoice
2 × 50 000, 10% off the line, VAT 19.25%   line total  107 325
1 × 20 000, 5 000 off the line             line total   15 000

subtotal         105 000   (90 000 + 15 000)
discount_amount   10 500   (10% of 105 000)
tax_amount        17 325
total            111 825   (107 325 + 15 000 − 10 500)

The invoice returns the discount you set as has_global_discount, global_discount_type and global_discount_value, and what it came to as discount_amount. The payment page charges the discounted total, and the receipt and the payment confirmation list the discount after the lines.

Quotes and estimates

Create a quote or an estimate with document_type: quote or estimate. It is read, corrected, cancelled and deleted by its id like an invoice, and GET /invoices?document_type=quote lists quotes. It stays a draft: it is not sent, numbered nor paid through the API, so POST /invoices/{id}/send answers 400 Only an invoice can be sent through the API. and mark-paid answers 400 Only an invoice can be marked as paid. A document_type is set at creation and kept: PUT does not change it.

Paying in instalments

Set payment_type to split or milestone and pass the schedule. The hosted page then charges one entry at a time instead of the full balance.

amountnumberrequired
What this instalment charges.
labelstringoptional
Shown on the payment page. Defaults to Installment 1 or Milestone 1.
due_datedateoptional
Shown on the payment page.

The customer opens the invoice and sees the first entry that is not yet paid, charged at its amount capped by the remaining amount_due. When it settles, Wajub marks that entry paid, adds the amount to amount_paid, and moves the invoice to partial. The next visit offers the next entry. The invoice turns paid when amount_paid reaches the total, and once every entry is settled with a balance still outstanding the page falls back to charging the remainder in one go.

Listing

GET /invoices returns your live invoices, newest first, twenty-five at a time.

GEThttps://api.wajub.com/invoices
searchstringoptional
Full-text over number, customer name and email.
statusenumoptional
One status, exactly as it appears on the object: draft, sent, viewed, partial, paid, overdue, cancelled or refunded.
customer_idstringoptional
One customer, by its cus_ id.
date_fromdateoptional
Lower bound on invoice_date.
date_todateoptional
Upper bound on invoice_date, on or after date_from.
due_date_fromdateoptional
Lower bound on due_date.
due_date_todateoptional
Upper bound on due_date, on or after due_date_from.
amount_minnumberoptional
Lower bound on total, in the major unit, as total reads: 10000 is ten thousand francs. Each invoice is compared in its own currency.
amount_maxnumberoptional
Upper bound on total, in the major unit.
payment_statusenumoptional
unpaid, partially_paid or fully_paid, read from amount_paid against total.
sort_byenumoptionaldefault : created_at
invoice_date, due_date, total, amount_paid, status or created_at. The list follows it first, then the newest first among invoices that tie.
sort_direnumoptionaldefault : desc
asc or desc.
per_pageintegeroptionaldefault : 25
Between 1 and 100.
document_typeenumoptionaldefault : invoice
invoice, quote or estimate: one kind of document per list. Without it, the list holds invoices only.

A status, payment_status, document_type, sort_by or sort_dir outside these values is a 422 on that field, as a per_page out of range is. So is a date bound that is not a date, or an upper bound before its lower one.

curl -G https://api.wajub.com/invoices \
  -H "Authorization: sk.xxxxxxxxxxxxxxxx…" \
  -d status=overdue \
  -d customer_id=cus_7Kq2mX9vL4tRb3nP8sZcWd1e

The list is cursor-paginated on the default sort. Ask for a custom sort_by or sort_dir and it falls back to page numbers.

Correcting an invoice

PUT /invoices/{id} replaces the document rather than patching it. customer_name, items, invoice_date and currency are required on every call, so send the whole invoice back with your change applied, not the change alone. The lines you send replace the previous ones outright: each update replaces the lines and their ids, so every items[].id changes.

recurring_interval can be changed on update; the recurring_frequency alias accepted at creation is not, so use recurring_interval everywhere. The invoice-wide discount is kept unless you send it: has_global_discount: false removes it. language is left as it is unless you send it, even when customer_id changes. A PUT sends one invoice.updated, with the lines and totals it saved, and none when it changes nothing. A deleted invoice is never returned to you again, and its invoice number stays taken.

Recording a payment you collected elsewhere

mark-paid is for money that arrived outside Wajub, in cash or by bank transfer. It moves the balance; it does not create a transaction and it does not settle anything.

POSThttps://api.wajub.com/invoices/{id}/mark-paid
amountnumberoptional
What you received, above zero. Omit it and the full total is recorded.
payment_datedateoptional
The day the money came in. Becomes paid_at when this payment settles the invoice, a single payment of the whole amount included. Today when omitted.
transaction_idstringoptional
Your reference for the money received, such as a transfer number. Kept on the payment the invoice records, which the Dashboard shows in its history; the API does not return it.
payment_methodstringoptional
Kept the same way, for your records.
notesstringoptional
Kept the same way, for your records.

An amount below the total adds to amount_paid and sets the status to partial. Call it again for the next instalment: the amounts accumulate, and the invoice flips to paid with a paid_at the moment they reach the total.

curl https://api.wajub.com/invoices/inv_LRQqYvlhrgUOE225KMKU/mark-paid \
  -H "Authorization: sk.xxxxxxxxxxxxxxxx…" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 178875, "payment_date": "2026-09-20" }'

The invoice has to be open: sent, viewed, overdue or partly paid. A draft answers 400, since it was never sent nor numbered: send it first. An invoice already paid answers 400 too, and keeps its paid_at; so do a cancelled and a refunded one, with 400 This invoice cannot be marked as paid, and a quote or an estimate, with 400 Only an invoice can be marked as paid. An amount of zero or less answers 422.

The PDF

Once sent, an invoice has a PDF anyone holding its id can download, without a key: the copy your customer files. A draft answers 404, as its page does.

The invoice as a PDF
https://api.wajub.com/i/inv_LRQqYvlhrgUOE225KMKU/pdf

It is written in the invoice's language. Add ?locale=fr or ?locale=en to have it in the other one. This is the link the invoice page offers, so share it the way you share the page.

Cancelling

POST /invoices/{id}/cancel sets the status to cancelled, stamps cancelled_at, and closes the public page. Anything already paid, cancelled or refunded answers 400 This invoice cannot be cancelled.

Editing and deleting are both closed on an invoice that has been partially paid. Cancelling closes it without refunding what was collected. Refunding in full what was paid on it online, with refunds on the underlying payments, turns it refunded instead, unless part of it was recorded with mark-paid.

What did you think of this content?