e-invoicing2026-07-09

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.

GetPay Engineering Team
Updated: 2026-07-30

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:

  1. Build the accounting document from the tenant's invoice, item, supplier, and buyer records.
  2. Confirm every party identifier is attached to the correct role and schemeID.
  3. Reconcile every InvoiceLine.LineExtensionAmount and line TaxTotal.
  4. Reconcile LegalMonetaryTotal to the invoice subtotal, tax, and total.
  5. Stringify the finished object once; do not edit it after hashing.
  6. 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 valueUBL JSON destinationGetPay behaviour
invoice.invoice_numberInvoice[0].ID[0]._Tenant-side document number
invoice.issue_dateIssueDate[0]._Expected as YYYY-MM-DD
fixed standard typeInvoiceTypeCode[0]_ is 01; GetPay emits unsigned listVersionID: "1.0" until XAdES signing is implemented
fixed filing currencyDocumentCurrencyCode[0]._MYR
supplier.tinsupplier PartyIdentificationschemeID: "TIN"
supplier.registrationsupplier PartyIdentificationschemeID: "BRN"
supplier.sst_registrationsupplier PartyIdentificationOptional schemeID: "SST"
supplier.msic_code + business activityIndustryClassificationCodeRequired five-digit code paired with its non-empty name
buyer TINcustomer PartyIdentificationSupplied value, invoice value, then general-public fallback
buyer address + phonecustomer PostalAddress + ContactFrozen from the selected customer onto the invoice before filing
item.quantityInvoicedQuantity[0]._Standard path uses unitCode: "XUN"
item.amountline LineExtensionAmount[0]._Tax-exclusive line amount
item.unit_pricePriceAmount[0]._Unit price in MYR
fixed fallback classificationline CommodityClassificationCode 022 with listID: "CLASS" until a more specific product classification is stored
line amount + taxline ItemPriceExtension + TaxTotalIncludes the line tax subtotal, category and OTH tax scheme
invoice.tax_amountdocument TaxTotalAlso allocated across lines
invoice.subtotal and discountline extension, allowance and tax-exclusive totalsGross line total less document allowance produces the tax-exclusive total
invoice.totaltax-inclusive and payable totalsBoth 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:

  • service becomes tax category 02;
  • sales becomes 01;
  • exempt becomes E; and
  • any other tax_type throws 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:

  1. Request an access token with client_id, client_secret, grant_type: "client_credentials", and scope: "InvoicingAPI".
  2. Respect the returned expires_in value. GetPay caches the token but refreshes when 60 seconds or less remain.
  3. Put one or more four-field submission documents inside { "documents": [...] }.
  4. Send that JSON body to the document-submission operation with Content-Type: application/json and Authorization: Bearer <token>.
  5. Record the returned submissionUid, every accepted document's uuid and invoiceCodeNumber, and any rejected document's returned error code and message.
  6. Read document details by UUID to distinguish Submitted, Valid, Invalid, and Cancelled.

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

Direct LHDN MyInvois Submission Engine

Ready to automate your Malaysian e-invoicing & bookkeeping?

GetPay handles 100% compliant e-invoices, multi-bank reconciliation, and statutory payroll out of the box.

Get Started Free

Related Articles