How I Wired LHDN's e-Invoice Mandate Into a Multi-Tenant ERP — Without Trusting Any One Path to Do It Alone
I was recently deep in Urusentra, the construction ERP I built solo for Malaysian SMEs, when LHDN's e-Invoice mandate stopped being a "later" problem and became a "now" one. Every business on the platform would eventually need to submit invoices, credit notes, debit notes, and refunds to LHDN's MyInvois system, get them validated asynchronously, and live inside a strict document lifecycle where…
The core challenge of the e-Invoice mandate was its complexity, not just in the technical implementation, but also in understanding and adhering to LHDN's strict specifications. The mandate required a multi-tenant ERP system to submit invoices, credit notes, debit notes, and refunds to LHDN's MyInvois system, with asynchronous validation and strict document lifecycle management. This made the task not only a technical challenge but also a matter of serious compliance and transaction accuracy.
Urusentra, the ERP built for Malaysian SMEs, became a platform where every business would eventually need to comply with the e-Invoice mandate. This meant that my responsibility extended to designing the entire process, including the OAuth layer, payload construction, submission and polling lifecycle, TIN validation, and Celery orchestration to tie everything together.
One of the key constraints of the e-Invoice integration was the format of the payload, which was UBL 2.1 dressed in a JSON costume. Each leaf value had to be wrapped as {"_": value}, and the document was a complex series of single-item arrays nested inside single-item arrays. While getting the wrapping discipline right was relatively straightforward, maintaining this discipline across different document types was a challenge, as nobody consistently checked the wrapping for every document type.
Multi-tenancy added another layer of complexity, as each business had its own MyInvois Client ID and secret, its own environment setting (Sandbox or Production), and its own submission history. This meant that a single global token was not feasible, as it would lead to potential authentication issues across different tenants and environments.
Each tenant needed its own token, scoped per company and per environment, to avoid any risk of one tenant's submission using another tenant's credentials, which could have disastrous consequences.
LHDN's validation process was asynchronous, returning a 202 Accepted response instead of an immediate "valid" or "invalid" status. Instead, it expected the submission to be polled for validation, which required patience and careful design to avoid being flagged as abusive traffic. The system had to be built to handle polling in a way that balanced the need for immediate validation with LHDN's guidelines against excessive traffic.
The downstream processes of an Invoice were also tightly coupled with LHDN's validation process. A Credit Note could not reference an invoice until it had been validated by LHDN, requiring a lineage reference to the LHDN-assigned UUID of the invoice, which only became available after successful validation. Submitting a Credit Note against an unvalidated invoice would result in failure, highlighting the importance of maintaining strict lineage and validation checks throughout the process.
One of the most critical aspects of the design was token handling. LHDN tokens were valid for only 60 minutes, necessitating a careful balance between cache duration and refresh rates. Initially, the plan was to cache tokens for the full hour and refresh upon receiving a 401 response. However, this approach was deemed risky due to the short time tokens were valid, particularly when requests were made at the very edge of their validity.
To mitigate this risk, the decision was made to cache tokens for 55 minutes instead, providing a five-minute buffer to account for potential expirations. This decision was crucial to ensure that token refreshes could be managed promptly, minimizing the risk of authentication failures and ensuring a smooth user experience.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written; read the original for the full account.



