Invoice reference
The invoice object, the amounts it computes, and the eight calls that manage it.
An invoice is a document you issue, hand to a customer, and wait to be paid. Wajub stores the lines, computes the totals, hosts the page the customer pays on, and records what has been settled. It does not chase anybody and it does not debit anything: the whole product is a document with a public URL and a running balance. Billing & subscriptions walks through that cycle. This page is the reference for the object itself.
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. There is no PDF endpoint, no email endpoint, and no endpoint that charges a customer.
| Call | What it does |
|---|---|
GET /invoices | List, filter and sort your invoices |
POST /invoices | Create one, always as draft |
GET /invoices/{id} | Retrieve one |
PUT /invoices/{id} | Replace one, only while it is editable |
DELETE /invoices/{id} | Soft delete one, only while it is editable |
POST /invoices/{id}/send | Move it to sent and open its public page |
POST /invoices/{id}/mark-paid | Record a payment you collected elsewhere |
POST /invoices/{id}/cancel | Close it out |
{id} is the id the API returns, the inv_ string. The internal UUID is accepted too, but nothing
ever shows it to you, so use the inv_ one.
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.
invoice_number is generated per team and resets every month: INV-, the year and month, then a
four-digit counter. 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.
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 or through a payment made on the hosted page.
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.
https://api.wajub.com/invoicescustomer_namestringrequiredcustomer_id.customer_idstring (uuid)optionalcustomer_* fields you sent.customer_emailstringoptionalcustomer_company_namestringoptionalcustomer_phonestringoptionalcustomer_addressstringoptionalitemsarrayrequiredinvoice_datedaterequireddue_datedateoptionalinvoice_date.payment_terms_daysintegeroptionaldue_date to that many days from today, and only when due_date is absent. Counted from today, not from invoice_date.currencystringrequiredXAF, XOF, NGN and the rest of the supported list.is_recurringbooleanoptionaldefault : falserecurring_intervalenumoptionaldaily, weekly, monthly, quarterly or yearly.payment_typeenumoptionaldefault : fullfull, split or milestone. Instalments are covered below.payment_schedulesarrayoptionalpayment_type is not full.notesstringoptionaltermsstringoptionalfooterstringoptionaltemplate_idstring (uuid)optionaldocument_typeenumoptionaldefault : invoiceinvoice, quote or estimate. Read the warning below before you touch it.Each entry in items is its own object.
namestringrequireddescriptionstringoptionalquantitynumberrequiredunit_priceintegerrequiredunitstringoptionalhour or day. Cosmetic.tax_ratenumberoptionaldiscount_typeenumoptionalpercentage or fixed.discount_valuenumberoptionalfixed.curl https://api.wajub.com/invoices \
-H "Authorization: sk.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
-H "Content-Type: application/json" \
-d '{
"customer_name": "Traoré & Fils",
"customer_email": "amina@example.cm",
"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 whole numbers
Every money column on an invoice and on its lines is an integer. unit_price, subtotal,
discount_amount, tax_amount, total and amount_paid all hold whole units of the currency, and
there is no minor unit to fall back on: 150000 means one hundred and fifty thousand francs, not
fifteen hundred.
A fractional amount fails after validation, not during it
unit_price, quantity, tax_rate and discount_value are validated as numeric, so
"unit_price": 1500.50 passes validation cleanly. The write then fails, because the column cannot
hold it, and you get a 500 rather than a 422 telling you which field was wrong. The same happens
when a rate does not divide the line: 19.25% of 1000 is 192.5, and that request dies. Keep every
line so that its price, its discount and its tax all land on whole units.
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_amountTax 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.
subtotal = Σ (quantity × unit_price)
tax_amount = Σ line tax_amount
total = subtotal + tax_amount
amount_due = max(0, total − amount_paid)A line discount lowers the tax but not the invoice total
Look at the two sums above. The invoice subtotal re-multiplies quantity by price and never
subtracts discount_amount, while the tax it adds is the one each line computed after its
discount. On the example higher up, the second line reports total: 90000, and the invoice still
counts 100000 for it. Until that is fixed, keep discounts out of the API and price the line at what
you actually want to charge.
There is no invoice-wide discount
Discounts are per line. The create call also accepts has_global_discount, global_discount_type
and global_discount_value, and those three do nothing at all: they are validated, then dropped
before the write because no column carries them. They return no error either, so an invoice sent with
a 15% global discount is stored at full price and looks like it worked.
Quotes and estimates
document_type accepts quote and estimate, and the document is created.
A quote created through the API cannot be read back
Every other invoice call filters on document_type = invoice. A quote or an estimate is therefore
absent from GET /invoices, and GET, PUT, DELETE, /send, /mark-paid and /cancel on its
id all answer 404 Invoice not found. It exists, it is billable against nothing, and only the
Dashboard can see it. Leave document_type alone and raise quotes in the Dashboard.
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.
amountintegerrequiredlabelstringoptionalInstallment 1 or Milestone 1.due_datedateoptionalThe 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 writes status: "paid", a paid_at and
the transaction_uid back onto that entry, 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.
You can set a schedule but you cannot read it back
The invoice object returns payment_type and never payment_schedules. Which instalments exist,
which are paid and which transaction settled each one are only visible in the Dashboard. Keep your
own copy of the schedule you sent if you need to follow it from your side.
Listing
GET /invoices returns your live invoices, newest first, twenty-five at a time.
https://api.wajub.com/invoicessearchstringoptionalstatusenumoptionalcustomer_idstring (uuid)optionaldate_fromdateoptionalinvoice_date.date_todateoptionalinvoice_date.due_date_fromdateoptionaldue_date.due_date_todateoptionaldue_date.amount_minintegeroptionaltotal.amount_maxintegeroptionaltotal.payment_statusenumoptionalunpaid, partially_paid or fully_paid, read from amount_paid against total.sort_byenumoptionaldefault : created_atinvoice_date, due_date, total, amount_paid, status or created_at.sort_direnumoptionaldefault : descasc or desc.per_pageintegeroptionaldefault : 25curl -G https://api.wajub.com/invoices \
-H "Authorization: sk.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
-d status=overdue \
-d customer_id=9d1f0c7a-4b2e-4f61-9d3a-7c8e5b21a940The list is cursor-paginated on the default sort. Ask for a custom sort_by or sort_dir and it
falls back to page numbers, because a cursor needs a stable order to walk.
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: the old
rows are deleted and recreated, which means every items[].id changes.
An invoice stops being editable the moment money touches it
PUT and DELETE both answer 400 This invoice cannot be edited (paid, cancelled, or has payments). as soon as the status is paid, cancelled or refunded, or as soon as amount_paid
is above zero. A partially paid invoice is already frozen. Cancel it and issue a replacement.
recurring_interval can be changed on update; the recurring_frequency alias accepted at creation
is not, so use recurring_interval everywhere. Deleting is a soft delete: the row stays, the invoice
number stays taken, and nothing returns it to you again.
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.
https://api.wajub.com/invoices/{id}/mark-paidamountintegeroptionaltotal is recorded.payment_datedateoptionalpaid_at when this payment clears the balance.transaction_idstringoptionalpayment_methodstringoptionalnotesstringoptionalAn 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.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
-H "Content-Type: application/json" \
-d '{ "amount": 178875, "payment_date": "2026-09-20" }'A cancelled or refunded invoice refuses the call with 400 This invoice cannot be marked as paid.
Cancelling
POST /invoices/{id}/cancel sets the status to cancelled, stamps cancelled_at, and closes the
public page. It accepts a reason and does not store it. Anything already paid, cancelled or
refunded answers 400 This invoice cannot be cancelled.
Cancelling is the only exit for an invoice that has been partially paid, since editing and deleting are both closed by then. It does not refund what was collected: use refunds on the underlying payment for that.