LHDN MyInvois API Payload Guide: JSON Schema, Field Validation & Direct Transmission
Build a MyInvois UBL 2.1 JSON payload from real invoice fields, validate parties, tax and totals, then hash, Base64-encode and submit it safely.
TL;DR (Key Takeaways)
- •GetPay emits the JSON variant of a UBL 2.1 Invoice: namespace keys, one Invoice array, supplier and customer parties, itemised InvoiceLine entries, TaxTotal, and LegalMonetaryTotal.
- •The current standard-invoice mapper uses document type 01, unsigned list version 1.0 and MYR; buyer TIN falls back to EI00000000010 only when no buyer TIN is supplied.
- •Validate by document flow: standard invoices allocate tax to lines exactly, self-billed invoices enforce identity and sen rules, and consolidated invoices reject unknown tax types instead of guessing an LHDN category.
- •For direct transmission, stringify once, hash those exact UTF-8 bytes with SHA-256, Base64-encode the same string, and send the resulting document envelope without automatically retrying the submission POST.
What does a valid MyInvois JSON payload contain?
A GetPay standard invoice is a UBL 2.1 Invoice represented as JSON. The root contains three namespace identifiers—_D, _A, and _B—and an Invoice array. The first invoice object then contains the document identity, issue date and time, document type, currency, supplier, buyer, lines, tax total, and legal monetary total.
Use this order as a practical pre-submission checklist:
- Build the accounting document from the tenant's invoice, item, supplier, and buyer records.
- Confirm every party identifier is attached to the correct role and
schemeID. - Reconcile every
InvoiceLine.LineExtensionAmountand lineTaxTotal. - Reconcile
LegalMonetaryTotalto the invoice subtotal, tax, and total. - Stringify the finished object once; do not edit it after hashing.
- Wrap the string as a submission document and send it with a tenant-specific access token.
This is the complete shape emitted by GetPay's ordinary-invoice mapper, with illustrative values:
{
"_D": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
"_A": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
"_B": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
"Invoice": [
{
"ID": [{ "_": "INV-1001" }],
"IssueDate": [{ "_": "2026-07-29" }],
"IssueTime": [{ "_": "00:00:00Z" }],
"InvoiceTypeCode": [{ "_": "01", "listVersionID": "1.0" }],
"DocumentCurrencyCode": [{ "_": "MYR" }],
"AccountingSupplierParty": [{
"Party": [{
"IndustryClassificationCode": [{ "_": "62010", "name": "Business activity" }],
"PartyIdentification": [
{ "ID": [{ "_": "SUPPLIER_TIN", "schemeID": "TIN" }] },
{ "ID": [{ "_": "SUPPLIER_REGISTRATION", "schemeID": "BRN" }] },
{ "ID": [{ "_": "SUPPLIER_SST", "schemeID": "SST" }] }
],
"PartyName": [{ "Name": [{ "_": "Example Supplier Sdn. Bhd." }] }],
"PostalAddress": [{
"AddressLine": [{ "Line": [{ "_": "Example address" }] }],
"CityName": [{ "_": "Kuala Lumpur" }],
"PostalZone": [{ "_": "50000" }],
"CountrySubentityCode": [{ "_": "14" }],
"Country": [{ "IdentificationCode": [{ "_": "MYS", "listID": "ISO3166-1", "listAgencyID": "6" }] }]
}],
"PartyLegalEntity": [{ "RegistrationName": [{ "_": "Example Supplier Sdn. Bhd." }] }],
"Contact": [{ "Telephone": [{ "_": "+60312345678" }] }]
}]
}],
"AccountingCustomerParty": [{
"Party": [{
"PartyIdentification": [
{ "ID": [{ "_": "BUYER_TIN", "schemeID": "TIN" }] },
{ "ID": [{ "_": "BUYER_REGISTRATION", "schemeID": "BRN" }] }
],
"PartyName": [{ "Name": [{ "_": "Example Buyer" }] }],
"PostalAddress": [{
"AddressLine": [{ "Line": [{ "_": "2 Buyer Street" }] }],
"CityName": [{ "_": "Kuala Lumpur" }],
"PostalZone": [{ "_": "50450" }],
"CountrySubentityCode": [{ "_": "14" }],
"Country": [{ "IdentificationCode": [{ "_": "MYS", "listID": "ISO3166-1", "listAgencyID": "6" }] }]
}],
"PartyLegalEntity": [{ "RegistrationName": [{ "_": "Example Buyer" }] }],
"Contact": [{ "Telephone": [{ "_": "+60387654321" }], "ElectronicMail": [{ "_": "buyer@example.com" }] }]
}]
}],
"InvoiceLine": [{
"ID": [{ "_": "1" }],
"InvoicedQuantity": [{ "_": 2, "unitCode": "XUN" }],
"LineExtensionAmount": [{ "_": 200, "currencyID": "MYR" }],
"TaxTotal": [{
"TaxAmount": [{ "_": 12, "currencyID": "MYR" }],
"TaxSubtotal": [{
"TaxableAmount": [{ "_": 200, "currencyID": "MYR" }],
"TaxAmount": [{ "_": 12, "currencyID": "MYR" }],
"Percent": [{ "_": 6 }],
"TaxCategory": [{
"ID": [{ "_": "02" }],
"TaxScheme": [{ "ID": [{ "_": "OTH", "schemeID": "UN/ECE 5153", "schemeAgencyID": "6" }] }]
}]
}]
}],
"Item": [{
"Description": [{ "_": "Implementation service" }],
"CommodityClassification": [{
"ItemClassificationCode": [{ "_": "022", "listID": "CLASS" }]
}]
}],
"Price": [{ "PriceAmount": [{ "_": 100, "currencyID": "MYR" }] }],
"ItemPriceExtension": [{ "Amount": [{ "_": 200, "currencyID": "MYR" }] }]
}],
"TaxTotal": [{
"TaxAmount": [{ "_": 12, "currencyID": "MYR" }],
"TaxSubtotal": [{
"TaxableAmount": [{ "_": 200, "currencyID": "MYR" }],
"TaxAmount": [{ "_": 12, "currencyID": "MYR" }],
"TaxCategory": [{
"ID": [{ "_": "02" }],
"TaxScheme": [{ "ID": [{ "_": "OTH", "schemeID": "UN/ECE 5153", "schemeAgencyID": "6" }] }]
}]
}]
}],
"LegalMonetaryTotal": [{
"LineExtensionAmount": [{ "_": 200, "currencyID": "MYR" }],
"TaxExclusiveAmount": [{ "_": 200, "currencyID": "MYR" }],
"TaxInclusiveAmount": [{ "_": 212, "currencyID": "MYR" }],
"PayableAmount": [{ "_": 212, "currencyID": "MYR" }]
}]
}
]
}
The sample demonstrates structure, not a universal set of values. The supplier MSIC code and its business-activity name are required; the SST PartyIdentification and party email are optional. Do not submit the illustrative identifiers.
How do source fields map into UBL 2.1?
GetPay's standard mapper makes the source-to-payload relationship explicit:
| Source value | UBL JSON destination | GetPay behaviour |
|---|---|---|
invoice.invoice_number | Invoice[0].ID[0]._ | Tenant-side document number |
invoice.issue_date | IssueDate[0]._ | Expected as YYYY-MM-DD |
| fixed standard type | InvoiceTypeCode[0] | _ is 01; GetPay emits unsigned listVersionID: "1.0" until XAdES signing is implemented |
| fixed filing currency | DocumentCurrencyCode[0]._ | MYR |
supplier.tin | supplier PartyIdentification | schemeID: "TIN" |
supplier.registration | supplier PartyIdentification | schemeID: "BRN" |
supplier.sst_registration | supplier PartyIdentification | Optional schemeID: "SST" |
supplier.msic_code + business activity | IndustryClassificationCode | Required five-digit code paired with its non-empty name |
| buyer TIN | customer PartyIdentification | Supplied value, invoice value, then general-public fallback |
| buyer address + phone | customer PostalAddress + Contact | Frozen from the selected customer onto the invoice before filing |
item.quantity | InvoicedQuantity[0]._ | Standard path uses unitCode: "XUN" |
item.amount | line LineExtensionAmount[0]._ | Tax-exclusive line amount |
item.unit_price | PriceAmount[0]._ | Unit price in MYR |
| fixed fallback classification | line CommodityClassification | Code 022 with listID: "CLASS" until a more specific product classification is stored |
| line amount + tax | line ItemPriceExtension + TaxTotal | Includes the line tax subtotal, category and OTH tax scheme |
invoice.tax_amount | document TaxTotal | Also allocated across lines |
invoice.subtotal and discount | line extension, allowance and tax-exclusive totals | Gross line total less document allowance produces the tax-exclusive total |
invoice.total | tax-inclusive and payable totals | Both document totals use the total |
Arrays and the inner _ value are intentional features of this JSON representation. Flattening ID: [{ _: "INV-1001" }] to ID: "INV-1001" changes the schema. Likewise, schemeID, currencyID, listVersionID, listID, and listAgencyID are attributes, not free-form labels.
Which validations should run before direct submission?
Validation must match the document flow. GetPay does not pretend that one shallow “JSON valid” check proves every e-Invoice is correct.
You can paste a standard invoice document into the free MyInvois JSON validator to get field-level required-field, TIN-format, and totals findings before signup. It performs a bounded in-memory format check and does not send the payload to LHDN.
For a standard invoice, the mapper fixes type 01, unsigned version 1.0, currency MYR, and supplier country MYS. It requires the supplier TIN, BRN, MSIC plus activity description, registered address and contact phone; SST registration and email remain optional. A missing buyer TIN can fall back to EI00000000010; a known registered buyer should retain its actual TIN. Versions 1.1 and 1.3 require XAdES signature structures, so GetPay must not label its unsigned document as either signed variant.
Tax must foot exactly. GetPay apportions the rounded document tax across positive line amounts using a largest-remainder method. Each allocation is non-negative and the sum of line tax amounts equals the rounded document tax in sen. Zero or negative-weight lines receive zero tax; if no line has positive weight, the whole tax is parked on the last line rather than dividing by zero.
Other flows add stricter guards. The self-billed individual path requires a positive voucher total whose lines sum exactly in sen. It validates the supplier identification type, TIN shape when supplied, country relationship, address, telephone in E.164 form, MSIC length, Malaysian state and postcode formats, positive line amounts, and two-decimal precision. It emits type 11, not type 01, and reverses the accounting parties because the payee is the supplier and the tenant is the buyer.
For consolidated B2C, GetPay maps each source invoice to its own line. The source subtotal becomes LineExtensionAmount; its tax stays in that line's TaxTotal; and classification 004 is emitted as ItemClassificationCode with listID: "CLASS". The tax-type mapping is deliberately closed:
servicebecomes tax category02;salesbecomes01;exemptbecomesE; and- any other
tax_typethrows instead of guessing.
Those codes are the mapper's LHDN tax-category mapping, not arbitrary internal general-ledger codes. Document-level tax subtotals are grouped by the mapped category, while each line can carry its own percentage.
How is the payload hashed and Base64-encoded?
The UBL object is not posted directly. buildSubmissionDocument() creates one canonical string and derives both integrity fields from it:
const canonical = JSON.stringify(ublDoc)
const bytes = new TextEncoder().encode(canonical)
const hashBuf = await crypto.subtle.digest("SHA-256", bytes)
const documentHash = [...new Uint8Array(hashBuf)]
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("")
const document = Buffer.from(canonical, "utf8").toString("base64")
The resulting submission item has exactly four fields:
{
"format": "JSON",
"document": "BASE64_OF_THE_JSON_STRING",
"documentHash": "LOWERCASE_SHA256_HEX_OF_THE_SAME_UTF8_BYTES",
"codeNumber": "INV-1001"
}
Do not pretty-print, reorder, or mutate the object between these operations. LHDN receives the Base64-decoded document, so documentHash must describe those exact bytes. The codeNumber is GetPay's tenant-side invoice number for traceability; it is separate from the Base64 payload.
How does direct MyInvois transmission work?
GetPay selects one of two environments: sandbox uses the preproduction MyInvois API host, while production uses the production host. Production submissions use the tenant's own saved credentials. During first-run activation, a credentialless tenant can instead make one real preproduction filing through GetPay's platform-owned sandbox test taxpayer. That test token never represents the tenant's TIN, the sandbox document receives a separate collision-safe test number, and this managed rail can never fall back to production.
The direct flow is:
- Request an access token with
client_id,client_secret,grant_type: "client_credentials", andscope: "InvoicingAPI". - Respect the returned
expires_invalue. GetPay caches the token but refreshes when 60 seconds or less remain. - Put one or more four-field submission documents inside
{ "documents": [...] }. - Send that JSON body to the document-submission operation with
Content-Type: application/jsonandAuthorization: Bearer <token>. - Record the returned
submissionUid, every accepted document'suuidandinvoiceCodeNumber, and any rejected document's returned error code and message. - Read document details by UUID to distinguish
Submitted,Valid,Invalid, andCancelled.
Token minting and document-detail reads can retry with backoff because they do not create a filing. Document submission cannot. If the network response is lost, the document may already exist at LHDN; an automatic replay could create a second e-Invoice. Treat the outcome as unknown and reconcile from preserved submission evidence.
What about the LHDN XML invoice format?
UBL is often discussed through XML element names, but GetPay's implemented transport is the JSON variant. The concepts remain recognisable—AccountingSupplierParty, InvoiceLine, TaxTotal, and LegalMonetaryTotal—yet the serialisation rules differ. JSON arrays, _ value holders, and attribute-like keys such as currencyID are part of the actual object sent by this SDK path.
Therefore, do not copy an XML fragment into document, label it as JSON, or hash one representation while encoding another. If an integration begins from XML, it needs a separately verified XML-to-MyInvois-JSON transformation. GetPay's native path avoids that conversion by constructing the JSON UBL document from accounting records and validating the flow before direct transmission.
Pre-submission checklist
- Confirm the intended environment and either the tenant credential tuple or GetPay's configured platform test-taxpayer sandbox.
- Confirm document type and list version for the actual flow.
- Verify supplier and buyer roles, TIN, registration scheme, SST registration, MSIC, address, and contact fields.
- Compare every line quantity, unit price, tax-exclusive amount, and tax amount with the source invoice.
- Confirm line tax sums to document tax and subtotal plus tax agrees with total.
- Confirm consolidated tax types are recognised; never substitute a guessed category.
- Stringify once, then hash and Base64-encode the same UTF-8 bytes.
- Preserve
submissionUid, UUIDs, rejected-document evidence, and status responses. - Never automatically retry a timed-out document-submission POST.
Frequently Asked Questions
Does the MyInvois API accept JSON or only XML?
GetPay directly submits the JSON variant of a UBL 2.1 Invoice. Its submission envelope sets format to JSON and carries a Base64-encoded JSON document. XML and JSON express related UBL concepts, but GetPay does not convert or submit an XML string in this flow.
Which TIN does GetPay use when a B2C buyer has no TIN?
For an ordinary invoice, GetPay uses the supplied buyer TIN, then the invoice customer TIN, and finally EI00000000010 as the general-public fallback. That fallback must not replace a known registered buyer's TIN.
How must documentHash relate to document?
Both must come from the same JSON.stringify result. GetPay calculates a lowercase SHA-256 hexadecimal digest from the string's UTF-8 bytes and Base64-encodes that exact same string into document.
Should a timed-out MyInvois document submission be retried automatically?
No. A lost response does not prove that LHDN rejected the document; it may already have been filed. GetPay bounds the submit request with a timeout but does not automatically retry that mutating POST.
Sources & Ground Truth
Ready to automate your Malaysian e-invoicing & bookkeeping?
GetPay handles 100% compliant e-invoices, multi-bank reconciliation, and statutory payroll out of the box.
Related Articles
The Complete Malaysian SME Accounting & LHDN e-Invoicing Blueprint 2026
A practical 2026 blueprint for Malaysian SME accounting, MyInvois e-Invoicing, statutory payroll journals, net-settlement reconciliation, SST and entity controls.
Multi-Currency Invoicing & Foreign Exchange Gain/Loss Double-Entry Journaling
A Malaysia-focused guide to multi-currency invoices, realized and unrealized foreign exchange gains or losses, month-end revaluation, settlement journals, and MyInvois currency fields.
Form EA & Form E Generation: Employer Year-End Statutory Payroll Reporting
A practical Malaysian employer guide to Form EA, e-CP8D and e-E: close payroll, separate employees from contractors, reconcile annual totals, generate reports and file in the correct order.