Skip to content

E-Invoice Submission Mechanics

You are the person who gets asked “did that invoice reach LHDN, and if not, where is it?” — usually by a customer who wants the validated e-invoice, sometimes by an auditor who wants last month’s submissions in a file. By the end of this page you will know what BigLedger actually stores between a finalised document and LHDN, be able to find any one document in those records in under a minute, export a month for whoever is asking, and read the monthly discrepancy report without guessing at what “missing” means. Reading it takes about fifteen minutes; there is nothing to configure.

The routing rules — which pool a document lands in and why — are on Pools and queues and Consolidated e-invoice. This page is about the records those rules produce.

Meet GadgetSphere

GadgetSphere Sdn Bhd (GS) runs 22 retail branches. In March 2026 they finalised about 88,000 cash bills, almost all of them below RM 10,000 and to walk-in shoppers, so they were reported inside consolidated e-invoices — one per branch per month, split every 200 receipts. The distribution arm (GSD) issued 310 invoices to corporate buyers, each submitted individually. Four of those 310 are sitting in the Individual Pool because the buyer’s tax identification number (TIN) was never captured. That is the month we will trace.

The record that goes to LHDN is not your invoice

When a document passes the mandatory-field check, BigLedger does not send the document. It builds a separate record — the to-IRB header — and sends that. It is a frozen copy: the document numbers, the branch, the buyer and supplier as they were at that moment (name, TIN, identity type and number, address, contact), the totals in the document currency and the rate, the billing period for a consolidated e-invoice. Then, as LHDN answers, the same record collects what came back: the submission and document identifiers LHDN assigned, the unique reference number, the validation link and QR code, the digital signature, and the document status (Submitted, Valid, Invalid, Cancelled).

The record lives in bl_fi_my_einvoice_to_irb_hdr, and it is the single most populated e-invoice table on the platform: 58 of 90 tenants hold rows, about a million of them. You see it as Internal Submission → To IRB E-Invoice in the My E-Invoice Admin applet, and every row of the Master List is one of these headers.

Two consequences follow from “frozen copy”, and both cause support tickets:

  • Correcting the customer record after submission changes nothing here. Fix the TIN on the customer and the e-invoice still carries the old one. The only route that rebuilds the record is Save and Resubmit on the to-IRB row (or on the pool row, if it never got this far), which copies your corrections back to the document and creates a new header — see Cancel and correct.
  • The document is told where its record is, once. At creation, BigLedger writes the header’s identifier and LHDN reference back onto the sales document (the To IRB fields on the document header). If those fields are blank on a finalised document, no to-IRB record exists for it: it is in a pool, or it never entered e-invoicing at all.

Every line of the document becomes a to-IRB line in bl_fi_my_einvoice_to_irb_line — one per document line, with the five things LHDN insists on per line: the classification code, the item name, the unit price, the taxable-type code and the tax amount. A document with any of the five blank on any line does not get a header at all; it goes to a pool with the reason attached.

The to-IRB header has its own running number, which is not your invoice number. Your invoice number travels on the record as Doc No (from the document’s first server number). When you search To IRB E-Invoice for a document, search by Doc No.

The link back to your documents

A to-IRB header points at the document it was built from. That is enough for an individual submission — one header, one document — but a consolidated e-invoice is one header built from hundreds of receipts, and the header can only point at one of them. So BigLedger keeps a second table, the match link (bl_fi_my_einvoice_to_irb_match_gen_doc_hdr_link): one row per (to-IRB header, document) pair.

  • For GSD’s 306 individually submitted invoices, there are 306 link rows, each duplicating what the header already says.
  • For branch GS-KV-01, whose 4,000 March receipts were consolidated by branch, there are 20 consolidated headers (200 receipts each) and 4,000 link rows. The link rows are how you find out which receipts are inside which consolidated e-invoice. The Discrepancies Report drills into them when you open a consolidated line; the API exposes them at …/e-invoice/to-irb-match-gen-doc-hdr-links.

This is why the link table is bigger than the header table — 7.1 million rows against one million, across the same 58 tenants.

Three columns on the link are never written. amount_matched, amount_signum and matching_status exist in the table and are commented out in every constructor with the note “requirements not clarified”. They are always empty. A report or an API query that filters on matching_status returns nothing — that is not an unmatched month, it is a column nobody fills. The three date columns on the link all hold the document’s transaction date, not the e-invoice date.

There is a third link, bl_fi_my_einvoice_batch_pool_to_irb_link (52 of 90 tenants, 6.3 million rows), written when the Batch Pool is consolidated: it ties each pool row to the consolidated header it went into. You do not normally read it; the Batch Pool screen uses it to show that a PROCESSED row went somewhere.

The chain, drawn from the foreign keys

Every arrow below is a foreign key in the schema (child points at parent — “holds the key of”). The submission history is drawn dashed because it is a partitioned table with no declared foreign keys; it stores the header’s identifier as a plain column.

    flowchart LR
  GD["bl_fi_generic_doc_hdr<br/>your invoice or cash bill"]
  GDL["bl_fi_generic_doc_line"]
  PQ["…generic_doc_einvoice_posting_queue<br/>entry gate at FINAL"]
  BP["…batch_pool_doc_hdr<br/>Batch Pool row"]
  IRB["bl_fi_my_einvoice_to_irb_hdr<br/>the e-invoice record"]
  IRBL["bl_fi_my_einvoice_to_irb_line"]
  LINK["…to_irb_match_gen_doc_hdr_link<br/>one row per document inside"]
  BPL["…batch_pool_to_irb_link"]
  SQ["…gen_doc_to_irb_submission_queue"]
  CQ["…consolidated_submission_queue"]
  VQ["…gen_doc_to_irb_validation_queue"]
  HIST["…gen_doc_to_irb_submission_history<br/>monthly partitions"]
  EM["bl_fi_einvoice_printable_email_thread"]
  RR["…request_rejection_internal_doc_hdr"]
  CANQ["…gen_doc_to_irb_cancellation_queue"]

  GDL --> GD
  PQ --> GD
  BP --> GD
  IRB --> GD
  IRBL --> IRB
  IRBL --> GDL
  LINK --> IRB
  LINK --> GD
  BPL --> BP
  BPL --> IRB
  SQ --> IRB
  SQ --> GD
  CQ --> IRB
  VQ --> IRB
  HIST -.-> IRB
  EM --> IRB
  RR --> IRB
  RR --> GD
  CANQ --> IRB
  CANQ --> RR
  

Read it from the middle: everything that happens after the mandatory-field check hangs off the to-IRB header. The queues (submission, consolidated, validation, cancellation) are transient — a row exists while the work is pending and is deleted or archived when it is done. The lines, the links, the history, the e-mail thread and the rejection request are permanent.

Statuses: three columns, one story

The header carries three status columns, and the moment a header is handed to the submission queue all three read IN_QUEUE. From there the applet’s processors move them: Submitted once LHDN has accepted the payload, then Valid or Invalid when the validation poll comes back, Cancelled after a rejection request succeeds. Consolidated headers start at IN_CONSOLIDATED_QUEUE instead. The full vocabulary and which processor writes each value are in the applet page’s lifecycle section.

For reading the screen, one rule is enough: IN_QUEUE means BigLedger has the record and LHDN has not seen it yet. It is not an error. If it is still IN_QUEUE tomorrow, open the submission queue row and read the error on it — nothing retries by itself.

The submission history: what was actually sent

Every attempt to send a header to LHDN writes a row to bl_fi_my_einvoice_gen_doc_to_irb_submission_history — the request body as sent, the response as received, the error if there was one, and the LHDN identifiers if it succeeded. This is the audit trail, and it is what you open when someone asks “what exactly did we send?”. The screen is Internal Submission → Submission History.

The table is partitioned by month on the row’s creation date. Partitions are created on demand — the first write in a month creates that month’s partition and every remaining month of the year — so you will see child tables named …_y2026m03 and so on. This is machinery, not something you manage, but it explains two things: why a database-level count of the parent looks different from the sum of its children on some tools, and why the partitions appear in only the tenants that were submitting in that month (12 tenants had a February 2025 partition; 50 have one for every month of 2026).

Export files: the month in a file

Two exports live on the Export tab of To IRB E-Invoice, and one more on Submission History. Each produces a file that BigLedger keeps — the file itself in the platform file store, and a record of it in bl_fi_my_einvoice_to_irb_export_file_hdr (39 of 90 tenants; about 1,100 files) or bl_fi_my_einvoice_submission_history_export_file_hdr (24 of 90).

ExportFormatWhat is in it
To IRB E-Invoice → Export → CSVCSVOne row per to-IRB header matching your filter, 38 columns in a fixed order: the to-IRB running number and e-invoice datetime; your document numbers (Doc No, branch, second and third server numbers, company code, reference); process status and submission type; transaction date and amount; billing frequency and period; the five client document numbers; buyer name, identity number and TIN; supplier name, identity number and TIN; the LHDN submission and document identifiers; e-invoice version, type and code; the original e-invoice reference (for credit and debit notes); the LHDN unique reference number; and the two issue timestamps — BigLedger’s and LHDN’s
To IRB E-Invoice → Export → PDFPDF (a zip)The LHDN-format printable of each header in your filter, zipped
Submission History → ExportCSVThe same shape, from the history rows — every attempt, including failed ones

Three things to know before you click:

  1. One at a time. Each export kind runs one job at a time per tenant. Ask for a second CSV while one is building and you get “Please wait until the current file finishes before generating additional files” — the first request is not lost, and the second is simply not queued. Wait for the row to show DONE, then ask again.
  2. A failed export is still a row. If the file could not be built, the row shows FAILED with the reason in its error message. It does not disappear and it is not retried.
  3. The CSV is the record as it is now. It reads the to-IRB header, so a header that was later cancelled shows Cancelled today even if it was Valid when the month closed. For “what was true on the 7th”, export on the 7th and keep the file.

For the auditor’s request — every GSD invoice submitted in March with its LHDN reference — filter To IRB E-Invoice to company GSD and March, export CSV, and hand over the file. The unique reference number and validation link columns are what the auditor wants; the QR code is not in the CSV, it is on the PDF printable.

The discrepancy report: what “missing” means

Monthly Report → Discrepancies Report → Create takes a company and a date range and stores a report header (bl_fi_einvoice_discrepancies_report_hdr) with one line per section (…_report_line: a section code, a document count, an amount and the list of document identifiers behind it). Twenty-five of 90 tenants have run it. The tabs are described on the applet page; what that page does not spell out is the exact test behind each direction, and the exact test is what decides whether a line is a problem.

Exists in ERP, Missing in E-invoice. A document counts here when all of these hold: it is FINAL; it is not deleted; its transaction date is in the range; its document currency equals the company’s currency; and there is neither a to-IRB header pointing at it nor a match-link row naming it. Two things follow. Foreign-currency documents never appear on this side of the report — GSD’s USD purchases from a Singapore distributor are simply not examined, whatever their e-invoice state. And a document you marked Skip E-Invoice has no header and no link, so it does meet this test: the skipped documents are also totalled in their own bucket on the ERP Transaction Summary, but the discrepancy direction does not subtract them. A month in which you skipped twenty voided cash bills shows twenty “missing” documents that are not missing at all. Read the skipped bucket first, then the discrepancy.

Exists in E-invoice, Missing in ERP. A to-IRB header counts here when it has no document identifier of its own and no match-link rows — an e-invoice record that no longer points at anything. A healthy consolidated header has no document identifier but does have its links, so it is not caught; what this side finds is orphans, typically from a resubmission that replaced the links or a document deleted after submission. The same test is run again excluding consolidated headers, and a consolidated-only total is produced alongside, so you can tell whether consolidation explains the gap.

For GadgetSphere’s March: the report for GSD shows 4 documents, RM 61,300, under Exists in ERP, Missing in E-invoice — the four invoices in the Individual Pool — and nothing in the other direction. That is the report working. The report for GS shows 0 in both directions, and the ERP Transaction Summary shows 88,400 cash bills of which 88,400 are in consolidated e-invoices. That is a closed month.

Procedure: trace one document from FINAL to LHDN

My E-Invoice Admin → Internal Submission → To IRB E-Invoice

A GSD customer says they never received the validated e-invoice for invoice GSD-INV-2603-0187, RM 18,400, dated 12 March. Under a minute, in this order:

1. Look at the document first. Open the invoice. If its To IRB reference is filled in, a record exists — go to step 2. If it is blank, there is no record: check the three pools for the invoice (it is RM 18,400 with a corporate buyer, so the Individual Pool is where a missing TIN would have parked it), and if it is in none of them, confirm the company’s e-invoice status was ENABLED when it was finalised — a document finalised while the switch was off is never queued and never will be.

2. Find the record. In To IRB E-Invoice, search Doc No GSD-INV-2603-0187. Read the status: Valid means LHDN accepted it and the validation link and QR are on the record — the customer’s PDF was e-mailed by the printable queue, and the Email Dashboard shows the thread; resend from there. Invalid means LHDN rejected it — the validation response on the record says why, and the fix is Save and Resubmit. IN_QUEUE means it has not been sent — open the submission queue row for the error.

3. If it is consolidated, find it inside. A GS cash bill will not have its own header. Its Doc No leads to a match-link row, and that row’s header is the consolidated e-invoice it was reported in — open the consolidated line in the Discrepancies Report drill-down, or query the link endpoint by document identifier. The customer gets the consolidated e-invoice’s reference, not one of their own; if they needed an individual e-invoice they had to ask for it at the till, before the month’s consolidation ran.

4. Read what was sent. Submission History, same Doc No. The request body is the exact payload; the response is LHDN’s exact answer. This is the row to quote when you open a ticket with LHDN or with BigLedger support.

5. Export if asked. Filter, Export tab, CSV or PDF, wait for DONE, download from the row.

What success looks like

Take yesterday’s largest individual invoice. In under a minute you can open its to-IRB record from Doc No, its status reads Valid, the validation link on the record opens LHDN’s page for it, and Submission History shows exactly one attempt. Take one branch’s consolidated e-invoice for last month: the number of match-link rows behind it equals the number of receipts it claims to contain (200, for a full one). Run the Discrepancies Report for last month: both discrepancy directions are zero, or every line on them is one you can name — the four invoices in the Individual Pool, the twenty skipped voids.

Common mistakes

What people doWhat actually happensDo this instead
Fix the customer’s TIN and wait for the e-invoice to updateNothing updates; the to-IRB record is a frozen copySave and Resubmit on the pool row or the to-IRB row
Filter the link table or the API on matching_statusAlways empty — the column is never writtenUse the header’s status; the link only tells you which documents are inside
Treat an empty Exists in ERP, Missing in E-invoice as proof the month is completeForeign-currency documents were never examinedCheck the foreign-currency documents separately, from the document listing
Treat every line under Exists in ERP, Missing in E-invoice as unreported revenueSkipped documents meet the same testRead the skipped bucket on the ERP Transaction Summary and subtract it
Click Export twice because nothing happenedThe second request is refused with a message; the first is still buildingWait for the row to show DONE
Search Submission History for a document that is still in a poolNothing to find — history is written only when a payload is sentSearch the pools; the document has not reached the history yet
Search To IRB E-Invoice by the to-IRB running number, expecting the invoice numberThey are different numbersSearch by Doc No

Related documentation