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. /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_id": "00000000-0000-0000-0000-000000000000",
"customer_name": "Amina Traoré",
"customer_email": "[email protected]",
"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,
"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. The response's customer_id refers to the same customer under another identifier: ignore it and 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 the Dashboard sends its reminders.
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 and of the PDF. Defaults to the preferred language of the customer customer_id names, else your account's language (**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. 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.xxxxxxxxxxxxxxxx…" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_name": "Traoré & Fils",
    "customer_email": "[email protected]",
    "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.

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 are ignored: they are validated and have no effect. 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.

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.

A status, payment_status, 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. 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. 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. It accepts a reason and does not store it. 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?