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, 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.

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 editable
DELETE /invoices/{id}Soft delete one, only while it is editable
POST /invoices/{id}/sendMove it to sent 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 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.

Response · an invoice with one taxed line
{
"id": "inv_LRQqYvlhrgUOE225KMKU",
"invoice_number": "INV-202609-0001",
"document_type": "invoice",
"status": "sent",
"customer_id": "9d1f0c7a-4b2e-4f61-9d3a-7c8e5b21a940",
"customer_name": "Amina Traoré",
"customer_email": "amina@example.cm",
"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",
"subtotal": 300000,
"discount_amount": 0,
"tax_amount": 57750,
"total": 357750,
"amount_paid": 0,
"amount_due": 357750,
"payment_type": "full",
"is_recurring": false,
"recurring_interval": null,
"notes": null,
"terms": null,
"footer": null,
"sent_at": "2026-09-12T09:14:02+00:00",
"paid_at": null,
"cancelled_at": null,
"items": [
{
"id": "0199f3c2-8b41-7a6e-9d02-4c1e7f83ab55",
"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 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.

POSThttps://api.wajub.com/invoices
customer_namestringrequired
Who the invoice is addressed to. Required even when you pass customer_id.
customer_idstring (uuid)optional
An existing customer. Its name, email, company, phone and address are copied onto the invoice and override the customer_* fields you sent.
customer_emailstringoptional
Where the Dashboard sends the invoice and its reminders. The API sends nothing.
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
Three letters. XAF, XOF, NGN and the rest of the supported list.
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. Read the warning below before you touch it.

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, or the amount when the type is fixed.
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.

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.

subtotal   = Σ (quantity × unit_price)
tax_amount = Σ line tax_amount
total      = subtotal + tax_amount
amount_due = max(0, total − amount_paid)

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.

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.

amountintegerrequired
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 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.

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.
customer_idstring (uuid)optional
One customer.
date_fromdateoptional
Lower bound on invoice_date.
date_todateoptional
Upper bound on invoice_date.
due_date_fromdateoptional
Lower bound on due_date.
due_date_todateoptional
Upper bound on due_date.
amount_minintegeroptional
Lower bound on total.
amount_maxintegeroptional
Upper bound on total.
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.
sort_direnumoptionaldefault : desc
asc or desc.
per_pageintegeroptionaldefault : 25
Between 1 and 100.
curl -G https://api.wajub.com/invoices \
  -H "Authorization: sk.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
  -d status=overdue \
  -d customer_id=9d1f0c7a-4b2e-4f61-9d3a-7c8e5b21a940

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, 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.

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.

POSThttps://api.wajub.com/invoices/{id}/mark-paid
amountintegeroptional
What you received. Omit it and the full total is recorded.
payment_datedateoptional
Used as paid_at when this payment clears the balance.
transaction_idstringoptional
Accepted and discarded.
payment_methodstringoptional
Accepted and discarded.
notesstringoptional
Accepted and discarded.

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.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.

What did you think of this content?