Documents
Invoa API documents endpoints: list, retrieve and create draft quotations and invoices, then issue, email, download the PDF or void them.
Documents are quotations, proforma invoices, delivery orders, invoices, credit, debit and refund notes, and receipts. They all share one object and one set of endpoints; type tells them apart.
A document starts as a draft with no number. Issuing allocates the next number from the company's series, freezes the content and renders the PDF. Issued documents cannot be edited; they can be sent, paid, voided or corrected with a credit note.
| Method | Path | Scope |
|---|---|---|
GET | /documents | documents:read |
GET | /documents/{id} | documents:read |
POST | /documents | documents:draft |
POST | /documents/{id}/issue | documents:issue |
POST | /documents/{id}/send | documents:send |
GET | /documents/{id}/pdf | documents:read |
POST | /documents/{id}/void | documents:issue |
There is no endpoint to edit or delete a draft. Do that in the app, or create a new draft. Receipts are created by recording a payment, not drafted.
The document object
GET /documents/{id}, and every endpoint that returns a single document, returns this object:
{
"id": "d0c00000-0000-4000-8000-000000000042",
"tenantId": "7e0a0000-0000-4000-8000-000000000001",
"type": "invoice",
"status": "issued",
"number": "INV-26-0042",
"seriesId": "5e500000-0000-4000-8000-000000000001",
"contactId": "c0a10000-0000-4000-8000-000000000007",
"currency": "MYR",
"exchangeRate": "1.000000",
"issueDate": "2026-10-15",
"dueDate": "2026-11-14",
"validUntil": null,
"reference": "PO-7781",
"title": "Corporate retreat, Langkawi",
"introText": null,
"notes": null,
"terms": null,
"subtotal": 125000,
"discountTotal": 0,
"documentDiscountType": "none",
"documentDiscountValue": "0.00",
"taxTotal": 10000,
"roundingAdjustment": 0,
"total": 135000,
"paidAmount": 0,
"balance": 135000,
"convertedFromId": null,
"revisionOf": null,
"originalDocumentId": null,
"billingScheduleItemId": null,
"templateId": "classic",
"publicToken": "example-public-token",
"sentAt": null,
"firstViewedAt": null,
"acceptedAt": null,
"acceptedBy": null,
"einvoiceStatus": "none",
"einvoiceUuid": null,
"einvoiceLongId": null,
"einvoiceSubmissionUid": null,
"einvoiceValidatedAt": null,
"einvoiceCancelDeadline": null,
"einvoiceErrors": null,
"pdfFileId": "f1100000-0000-4000-8000-000000000042",
"issuedBy": null,
"issuedAt": "2026-10-15T02:14:09.000Z",
"voidedAt": null,
"voidReason": null,
"allowedPaymentMethods": null,
"idempotencyKey": "booking-1001-deposit",
"createdBy": null,
"createdAt": "2026-10-15T02:12:31.000Z",
"updatedAt": "2026-10-15T02:14:09.000Z",
"lines": [
{
"id": "11e00000-0000-4000-8000-000000000001",
"documentId": "d0c00000-0000-4000-8000-000000000042",
"seq": 1,
"itemId": "1e300000-0000-4000-8000-000000000003",
"description": "Langkawi corporate retreat, 3 days 2 nights",
"quantity": "1.0000",
"unit": "pax",
"unitPrice": 125000,
"discountType": "none",
"discountValue": "0.00",
"taxType": "SST_SERVICE",
"taxRate": "8.00",
"lineSubtotal": 125000,
"taxAmount": 10000,
"lineTotal": 135000,
"classificationCode": "022",
"notes": null
}
],
"contact": {
"id": "c0a10000-0000-4000-8000-000000000007",
"name": "Aina Rahman",
"companyName": "Teratai Biru Ventures Sdn. Bhd.",
"email": "aina@teratai-biru.example",
"phone": "+60123456789",
"tin": "C12345678900"
},
"payments": [],
"receipts": [],
"related": [],
"voidability": { "ok": true },
"hostedUrl": "https://app.invoa.io/d/example-public-token",
"pdfUrl": "https://app.invoa.io/api/v1/documents/d0c00000-0000-4000-8000-000000000042/pdf",
"hasProposalBody": false
}| Field | Type | Description |
|---|---|---|
id | string | Document ID (UUID). |
type | string | See Document types. |
status | string | See Statuses. |
number | string or null | Allocated on issue, for example INV-26-0042. null for drafts. |
contactId, contact | string, object | The customer or supplier, with a short contact summary. |
issueDate, dueDate, validUntil | date or null | validUntil applies to quotations and proformas. |
reference, title | string or null | Customer PO or your reference, and a document title. |
subtotal | integer | Sum of line subtotals after line discounts, in sen. |
discountTotal | integer | Line discounts plus the document discount, in sen. |
taxTotal | integer | Total tax, in sen. |
roundingAdjustment | integer | Cash rounding, in sen. May be negative. |
total | integer | Amount payable, in sen. |
paidAmount, creditedAmount, balance | integer | Confirmed payments, issued credit notes against the invoice, and total − paidAmount − creditedAmount (never below 0), in sen. |
lines | array | Line items. Each carries its own computed lineSubtotal, taxAmount and lineTotal in sen. |
payments | array | Payments recorded against this document. |
receipts | array | Receipts issued for those payments: id, number, issueDate, total. |
related | array | Credit notes, revisions and conversions that point at this document. |
einvoiceStatus | string | none, queued, submitted, valid, invalid or cancelled. See e-Invoice. |
einvoiceUuid, einvoiceLongId | string or null | LHDN identifiers once validated. |
einvoiceValidatedAt, einvoiceCancelDeadline | timestamp or null | Validation time and the end of the 72-hour cancellation window. |
einvoiceErrors | array or null | LHDN validation errors: code, message and, where given, target and propertyPath. |
voidability | object | { "ok": true }, or { "ok": false, "reason": … } with not_issued, already_void or einvoice_window_closed. |
hostedUrl | string or null | The customer's page to view, download and pay. null for drafts. |
pdfUrl | string or null | The PDF endpoint for this document. null for drafts. |
hasProposalBody | boolean | Whether a quotation has proposal text. The text itself is not returned. |
idempotencyKey | string or null | The key the draft was created with. |
issuedAt, sentAt, firstViewedAt, acceptedAt, voidedAt | timestamp or null | Lifecycle times. |
Document types
type | Document | Can be drafted through the API |
|---|---|---|
quotation | Quotation | Yes |
proforma | Proforma invoice | Yes |
delivery_order | Delivery order | Yes |
invoice | Invoice | Yes |
credit_note | Credit note | Yes |
debit_note | Debit note | Yes |
refund_note | Refund note | Yes |
self_billed_invoice | Self-billed invoice | Yes (Pro plan or Pro trial; otherwise 422 plan_limit) |
consolidated_invoice | Consolidated invoice | Yes (Pro plan or Pro trial; otherwise 422 plan_limit) |
receipt | Receipt | No, created from payments |
Statuses
status | Meaning |
|---|---|
draft | Not issued. No number, editable, not visible to the customer. |
issued | Number allocated, content frozen, PDF rendered. |
sent | Issued and sent to the customer. |
accepted, declined, expired, converted | Quotation and proforma outcomes. converted means turned into an invoice or billing schedule. |
partially_paid, paid | Invoice payment progress. |
void | Cancelled. Kept for the record, never deleted. |
List documents
GET /api/v1/documentsReturns documents newest first, without lines, plus sums across every document that matches your filters (not just this page).
| Query parameter | Type | Description |
|---|---|---|
type | string | One document type, for example invoice. |
status | string | One or more statuses, comma-separated: issued,sent,partially_paid. |
contactId | string | Only this contact's documents. |
search | string | Matches number, reference, title, contact name or company name. |
from, to | date | Issue date range, inclusive (YYYY-MM-DD). |
openOnly | boolean | true for documents with status issued, sent or partially_paid. |
overdueOnly | boolean | true for open documents whose due date has passed. |
limit, offset | integer | See Pagination. |
curl "https://app.invoa.io/api/v1/documents?type=invoice&overdueOnly=true" \
-H "Authorization: Bearer $INVOA_API_KEY"{
"data": [
{
"id": "d0c00000-0000-4000-8000-000000000042",
"type": "invoice",
"status": "sent",
"number": "INV-26-0042",
"contactId": "c0a10000-0000-4000-8000-000000000007",
"contactName": "Aina Rahman",
"contactCompany": "Teratai Biru Ventures Sdn. Bhd.",
"issueDate": "2026-09-01",
"dueDate": "2026-10-01",
"validUntil": null,
"reference": "PO-7781",
"title": "Corporate retreat, Langkawi",
"currency": "MYR",
"total": 135000,
"paidAmount": 0,
"balance": 135000,
"einvoiceStatus": "valid",
"publicToken": "example-public-token",
"createdAt": "2026-09-01T02:12:31.000Z",
"updatedAt": "2026-09-02T08:40:00.000Z",
"sentAt": "2026-09-01T02:20:00.000Z",
"issuedAt": "2026-09-01T02:14:09.000Z"
}
],
"pagination": { "total": 1, "limit": 50, "offset": 0, "has_more": false },
"sums": { "total": 135000, "paid": 0, "balance": 135000 }
}sums.balance counts only issued, non-void documents.
Retrieve a document
GET /api/v1/documents/{id}curl https://app.invoa.io/api/v1/documents/d0c00000-0000-4000-8000-000000000042 \
-H "Authorization: Bearer $INVOA_API_KEY"Returns { "data": <document object> }. Errors: 404 not_found.
Create a draft
POST /api/v1/documentsCreates a draft. Invoa calculates line and document totals; you send prices and quantities only.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Any document type except receipt. |
contactId | string | Yes | The customer or supplier. |
lines | array | Yes | 1 to 500 lines. |
idempotencyKey | string | Recommended | 8 to 128 characters, unique per document in your system. A retry with the same key returns the existing document instead of creating another. |
issueDate | date | No | Default today in the company's timezone. Can also be set when issuing. |
dueDate | date or null | No | Invoices default to issueDate plus the contact's defaultPaymentTermsDays, or 30 days. |
validUntil | date or null | No | Quotations and proformas default to issueDate plus 30 days. |
reference | string | No | Customer PO or your reference, up to 100 characters. |
title | string | No | Up to 200 characters. |
introText | string | No | Text above the lines, up to 5,000 characters. |
notes | string | No | Up to 5,000 characters. |
terms | string | No | Up to 10,000 characters. |
proposalBody | string | No | Quotations and proformas only. Markdown proposal shown before the price table, up to 200,000 characters. |
documentDiscount | object | No | { "type": "amount", "value": 5000 } (sen) or { "type": "percent", "value": 10 }, applied across lines before tax. |
roundingAdjustment | integer | No | Cash rounding in sen. Default 0. |
currency | string | No | 3-letter code. Default MYR. |
exchangeRate | string | No | Decimal string, for example "4.215000". Default "1". |
seriesId | string | No | Number series to use on issue. Default: the company's default series for the type. |
originalDocumentId | string | No | Credit, debit and refund notes: the invoice being corrected. |
convertedFromId | string | No | The quotation this document comes from. |
templateId | string | No | PDF template, for example classic. Default: the company's template. |
allowedPaymentMethods | array | No | Online payment methods offered on the payment page: fpx, fpx_b2b1, card, ewallet, duitnow_qr. Default: the company's setting. |
Line fields
| Field | Type | Required | Description |
|---|---|---|---|
description | string | Yes | 1 to 2,000 characters. |
quantity | number | Yes | More than 0, up to 4 decimal places, for example 1.5. |
unitPrice | integer | Yes | Price per unit in sen, 0 or more. |
taxType | string | Yes | SST_SERVICE, SST_SERVICE_6, SST_SALES_5, SST_SALES_10, EXEMPT or NOT_APPLICABLE. See Items. |
itemId | string | No | Catalogue item the line came from. |
unit | string | No | Up to 20 characters. Default unit. |
discountType | string | No | none (default), amount or percent. |
discountValue | number | No | Sen when discountType is amount; 0 to 100 when percent. Default 0. |
taxRatePercent | number | No | Override the tax type's rate, 0 to 100. |
classificationCode | string | No | LHDN classification code. Default 022. |
notes | string | No | Up to 2,000 characters. |
curl https://app.invoa.io/api/v1/documents \
-H "Authorization: Bearer $INVOA_API_KEY" \
-H "content-type: application/json" \
-d '{
"type": "invoice",
"contactId": "c0a10000-0000-4000-8000-000000000007",
"idempotencyKey": "booking-1001-deposit",
"issueDate": "2026-10-15",
"reference": "PO-7781",
"title": "Corporate retreat, Langkawi",
"lines": [
{
"itemId": "1e300000-0000-4000-8000-000000000003",
"description": "Langkawi corporate retreat, 3 days 2 nights",
"quantity": 1,
"unit": "pax",
"unitPrice": 125000,
"taxType": "SST_SERVICE"
}
]
}'Returns 201 Created with { "data": <document object> }. The draft has "status": "draft", "number": null, "hostedUrl": null, "pdfUrl": null and "voidability": { "ok": false, "reason": "not_issued" }, with totals already calculated ("total": 135000 here).
Errors:
400 validationwithdetails, for example a missingtaxTypeon a line, a decimalunitPrice,type: "receipt", orproposalBodyon an invoice.404 not_foundifcontactId,originalDocumentIdorconvertedFromIdis not in this company.
Issue a document
POST /api/v1/documents/{id}/issueIssues a draft: allocates the next number from the series, recalculates totals, freezes the content and queues the PDF. Issuing through the API applies immediately; there is no approval step.
| Field | Type | Required | Description |
|---|---|---|---|
issueDate | date | No | Overrides the draft's issue date. |
seriesId | string | No | Number series to allocate from. |
The body is optional.
curl -X POST https://app.invoa.io/api/v1/documents/d0c00000-0000-4000-8000-000000000042/issue \
-H "Authorization: Bearer $INVOA_API_KEY"Returns 200 OK with { "data": <document object> }, now with "status": "issued", a number, issuedAt, hostedUrl and pdfUrl. Sends the document.issued webhook.
Errors:
422 invalid_stateif the document is not a draft (for example, it is already issued), or if the issue date falls in a period your accountant has locked.422 plan_limitif the company has used its plan's invoices for this month, or the type is consolidated or self-billed and the company is not on Pro. On the Dormant plan nothing can be issued.400 validationif the draft has no lines.404 not_found.
Send a document
POST /api/v1/documents/{id}/sendEmails an issued document to the customer with a link to its hosted page and, by default, the PDF attached. An issued document becomes sent.
| Field | Type | Required | Description |
|---|---|---|---|
to | array of strings | No | Recipient emails. Default: the contact's email. |
cc | array of strings | No | Copy recipients. |
subject | string | No | Up to 200 characters. Default, for example: "Invoice INV-26-0042 from Lembah Bayu Travel Sdn. Bhd." |
body | string | No | Up to 5,000 characters. Default: a short message with the amount and the hosted link. |
attachPdf | boolean | No | Default true. |
curl -X POST https://app.invoa.io/api/v1/documents/d0c00000-0000-4000-8000-000000000042/send \
-H "Authorization: Bearer $INVOA_API_KEY" \
-H "content-type: application/json" \
-d '{ "cc": ["finance@lembahbayu.example"] }'{
"data": { "id": "d0c00000-0000-4000-8000-000000000042", "status": "sent", "number": "INV-26-0042", "sentAt": "2026-10-15T02:20:00.000Z" },
"sent": {
"to": ["aina@teratai-biru.example"],
"subject": "Invoice INV-26-0042 from Lembah Bayu Travel Sdn. Bhd.",
"jobId": "a1b2c3d4-0000-4000-8000-000000000001",
"hostedUrl": "https://app.invoa.io/d/example-public-token",
"whatsappLink": "https://wa.me/60123456789?text=Invoice%20INV-26-0042%20from%20Lembah%20Bayu%20Travel%20Sdn.%20Bhd.%0Ahttps%3A%2F%2Fapp.invoa.io%2Fd%2Fexample-public-token"
}
}data is the full document object (shortened above). sent.whatsappLink is a prefilled WhatsApp link to the contact's WhatsApp number or phone, for you to open and send yourself; it is null when the contact has no number. The email itself is sent in the background. Sends the document.sent webhook.
Errors:
400 validationif the contact has no email and you did not passto.422 invalid_stateif the document is still a draft.404 not_found.
Download the PDF
GET /api/v1/documents/{id}/pdf| Query parameter | Type | Description |
|---|---|---|
inline | string | 1 to return content-disposition: inline instead of attachment. |
curl -o INV-26-0042.pdf -w "%{http_code}\n" \
https://app.invoa.io/api/v1/documents/d0c00000-0000-4000-8000-000000000042/pdf \
-H "Authorization: Bearer $INVOA_API_KEY"| Status | Response |
|---|---|
200 | The PDF (application/pdf), named after the document number, for example INV-26-0042.pdf. |
202 | The PDF is still being rendered, usually just after issuing. The body is { "status": "rendering", "retryAfterSeconds": 3 } with a Retry-After: 3 header. Try again after that. |
409 | invalid_state: drafts have no PDF. Issue the document first. |
Void a document
POST /api/v1/documents/{id}/voidVoids an issued document. It keeps its number and stays in your records with status void. If the document is a validated e-invoice, Invoa also cancels it at LHDN in the background and sends the einvoice.cancelled webhook when done.
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | Yes | 3 to 500 characters. |
curl -X POST https://app.invoa.io/api/v1/documents/d0c00000-0000-4000-8000-000000000042/void \
-H "Authorization: Bearer $INVOA_API_KEY" \
-H "content-type: application/json" \
-d '{ "reason": "Issued to the wrong customer" }'Returns 200 OK with { "data": <document object> }, now with "status": "void", voidedAt and voidReason.
Errors:
422 einvoice_window_closedif the document is a validated e-invoice and more than 72 hours have passed since validation. Issue a credit note instead (POST /documentswith"type": "credit_note"andoriginalDocumentId).422 invalid_stateif the document is a draft, already void, an invoice with confirmed payments or issued credit notes (void those first), or in another status that cannot change tovoid, such as an accepted or converted quotation.400 validationifreasonis missing or too short.
Related
Something unclear or out of date? Tell us at hello@invoa.io and we'll fix the page.