Docs / API reference

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.

MethodPathScope
GET/documentsdocuments:read
GET/documents/{id}documents:read
POST/documentsdocuments:draft
POST/documents/{id}/issuedocuments:issue
POST/documents/{id}/senddocuments:send
GET/documents/{id}/pdfdocuments:read
POST/documents/{id}/voiddocuments: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:

json
{
  "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
}
FieldTypeDescription
idstringDocument ID (UUID).
typestringSee Document types.
statusstringSee Statuses.
numberstring or nullAllocated on issue, for example INV-26-0042. null for drafts.
contactId, contactstring, objectThe customer or supplier, with a short contact summary.
issueDate, dueDate, validUntildate or nullvalidUntil applies to quotations and proformas.
reference, titlestring or nullCustomer PO or your reference, and a document title.
subtotalintegerSum of line subtotals after line discounts, in sen.
discountTotalintegerLine discounts plus the document discount, in sen.
taxTotalintegerTotal tax, in sen.
roundingAdjustmentintegerCash rounding, in sen. May be negative.
totalintegerAmount payable, in sen.
paidAmount, creditedAmount, balanceintegerConfirmed payments, issued credit notes against the invoice, and total − paidAmount − creditedAmount (never below 0), in sen.
linesarrayLine items. Each carries its own computed lineSubtotal, taxAmount and lineTotal in sen.
paymentsarrayPayments recorded against this document.
receiptsarrayReceipts issued for those payments: id, number, issueDate, total.
relatedarrayCredit notes, revisions and conversions that point at this document.
einvoiceStatusstringnone, queued, submitted, valid, invalid or cancelled. See e-Invoice.
einvoiceUuid, einvoiceLongIdstring or nullLHDN identifiers once validated.
einvoiceValidatedAt, einvoiceCancelDeadlinetimestamp or nullValidation time and the end of the 72-hour cancellation window.
einvoiceErrorsarray or nullLHDN validation errors: code, message and, where given, target and propertyPath.
voidabilityobject{ "ok": true }, or { "ok": false, "reason": … } with not_issued, already_void or einvoice_window_closed.
hostedUrlstring or nullThe customer's page to view, download and pay. null for drafts.
pdfUrlstring or nullThe PDF endpoint for this document. null for drafts.
hasProposalBodybooleanWhether a quotation has proposal text. The text itself is not returned.
idempotencyKeystring or nullThe key the draft was created with.
issuedAt, sentAt, firstViewedAt, acceptedAt, voidedAttimestamp or nullLifecycle times.

Document types

typeDocumentCan be drafted through the API
quotationQuotationYes
proformaProforma invoiceYes
delivery_orderDelivery orderYes
invoiceInvoiceYes
credit_noteCredit noteYes
debit_noteDebit noteYes
refund_noteRefund noteYes
self_billed_invoiceSelf-billed invoiceYes (Pro plan or Pro trial; otherwise 422 plan_limit)
consolidated_invoiceConsolidated invoiceYes (Pro plan or Pro trial; otherwise 422 plan_limit)
receiptReceiptNo, created from payments

Statuses

statusMeaning
draftNot issued. No number, editable, not visible to the customer.
issuedNumber allocated, content frozen, PDF rendered.
sentIssued and sent to the customer.
accepted, declined, expired, convertedQuotation and proforma outcomes. converted means turned into an invoice or billing schedule.
partially_paid, paidInvoice payment progress.
voidCancelled. Kept for the record, never deleted.

List documents

http
GET /api/v1/documents

Returns documents newest first, without lines, plus sums across every document that matches your filters (not just this page).

Query parameterTypeDescription
typestringOne document type, for example invoice.
statusstringOne or more statuses, comma-separated: issued,sent,partially_paid.
contactIdstringOnly this contact's documents.
searchstringMatches number, reference, title, contact name or company name.
from, todateIssue date range, inclusive (YYYY-MM-DD).
openOnlybooleantrue for documents with status issued, sent or partially_paid.
overdueOnlybooleantrue for open documents whose due date has passed.
limit, offsetintegerSee Pagination.
bash
curl "https://app.invoa.io/api/v1/documents?type=invoice&overdueOnly=true" \
  -H "Authorization: Bearer $INVOA_API_KEY"
json
{
  "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

http
GET /api/v1/documents/{id}
bash
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

http
POST /api/v1/documents

Creates a draft. Invoa calculates line and document totals; you send prices and quantities only.

FieldTypeRequiredDescription
typestringYesAny document type except receipt.
contactIdstringYesThe customer or supplier.
linesarrayYes1 to 500 lines.
idempotencyKeystringRecommended8 to 128 characters, unique per document in your system. A retry with the same key returns the existing document instead of creating another.
issueDatedateNoDefault today in the company's timezone. Can also be set when issuing.
dueDatedate or nullNoInvoices default to issueDate plus the contact's defaultPaymentTermsDays, or 30 days.
validUntildate or nullNoQuotations and proformas default to issueDate plus 30 days.
referencestringNoCustomer PO or your reference, up to 100 characters.
titlestringNoUp to 200 characters.
introTextstringNoText above the lines, up to 5,000 characters.
notesstringNoUp to 5,000 characters.
termsstringNoUp to 10,000 characters.
proposalBodystringNoQuotations and proformas only. Markdown proposal shown before the price table, up to 200,000 characters.
documentDiscountobjectNo{ "type": "amount", "value": 5000 } (sen) or { "type": "percent", "value": 10 }, applied across lines before tax.
roundingAdjustmentintegerNoCash rounding in sen. Default 0.
currencystringNo3-letter code. Default MYR.
exchangeRatestringNoDecimal string, for example "4.215000". Default "1".
seriesIdstringNoNumber series to use on issue. Default: the company's default series for the type.
originalDocumentIdstringNoCredit, debit and refund notes: the invoice being corrected.
convertedFromIdstringNoThe quotation this document comes from.
templateIdstringNoPDF template, for example classic. Default: the company's template.
allowedPaymentMethodsarrayNoOnline payment methods offered on the payment page: fpx, fpx_b2b1, card, ewallet, duitnow_qr. Default: the company's setting.

Line fields

FieldTypeRequiredDescription
descriptionstringYes1 to 2,000 characters.
quantitynumberYesMore than 0, up to 4 decimal places, for example 1.5.
unitPriceintegerYesPrice per unit in sen, 0 or more.
taxTypestringYesSST_SERVICE, SST_SERVICE_6, SST_SALES_5, SST_SALES_10, EXEMPT or NOT_APPLICABLE. See Items.
itemIdstringNoCatalogue item the line came from.
unitstringNoUp to 20 characters. Default unit.
discountTypestringNonone (default), amount or percent.
discountValuenumberNoSen when discountType is amount; 0 to 100 when percent. Default 0.
taxRatePercentnumberNoOverride the tax type's rate, 0 to 100.
classificationCodestringNoLHDN classification code. Default 022.
notesstringNoUp to 2,000 characters.
bash
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 validation with details, for example a missing taxType on a line, a decimal unitPrice, type: "receipt", or proposalBody on an invoice.
  • 404 not_found if contactId, originalDocumentId or convertedFromId is not in this company.

Issue a document

http
POST /api/v1/documents/{id}/issue

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

FieldTypeRequiredDescription
issueDatedateNoOverrides the draft's issue date.
seriesIdstringNoNumber series to allocate from.

The body is optional.

bash
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_state if 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_limit if 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 validation if the draft has no lines.
  • 404 not_found.

Send a document

http
POST /api/v1/documents/{id}/send

Emails an issued document to the customer with a link to its hosted page and, by default, the PDF attached. An issued document becomes sent.

FieldTypeRequiredDescription
toarray of stringsNoRecipient emails. Default: the contact's email.
ccarray of stringsNoCopy recipients.
subjectstringNoUp to 200 characters. Default, for example: "Invoice INV-26-0042 from Lembah Bayu Travel Sdn. Bhd."
bodystringNoUp to 5,000 characters. Default: a short message with the amount and the hosted link.
attachPdfbooleanNoDefault true.
bash
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"] }'
json
{
  "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 validation if the contact has no email and you did not pass to.
  • 422 invalid_state if the document is still a draft.
  • 404 not_found.

Download the PDF

http
GET /api/v1/documents/{id}/pdf
Query parameterTypeDescription
inlinestring1 to return content-disposition: inline instead of attachment.
bash
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"
StatusResponse
200The PDF (application/pdf), named after the document number, for example INV-26-0042.pdf.
202The 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.
409invalid_state: drafts have no PDF. Issue the document first.

Void a document

http
POST /api/v1/documents/{id}/void

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

FieldTypeRequiredDescription
reasonstringYes3 to 500 characters.
bash
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_closed if the document is a validated e-invoice and more than 72 hours have passed since validation. Issue a credit note instead (POST /documents with "type": "credit_note" and originalDocumentId).
  • 422 invalid_state if 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 to void, such as an accepted or converted quotation.
  • 400 validation if reason is missing or too short.

Something unclear or out of date? Tell us at hello@invoa.io and we'll fix the page.