Skip to content

Storefront E-Invoice Request

Overview

A shopper who paid at a branch or on the web store can ask for their own Malaysian LHDN e-invoice from the storefront itself — no account required. They type the cash-bill number with either the PIN printed on the receipt or the amount and date, confirm it is their transaction, fill in their identification number and tax identification number (TIN), and the sale is taken out of the merchant’s monthly consolidated e-invoice and submitted to LHDN as an individual one in their name.

That path is a widget in the Cross Platform Commerce storefront app (wavelet-cp-commerce, the Ionic/Capacitor web and mobile Customer Portal), not a screen in an applet. It is placed on a Customer Portal page from CP Commerce Admin, and everything it does runs against five backend endpoints and one queue processor, E_INVOICE_CP_COM_REQUEST_QUEUE_PROCESSOR.

This page documents the storefront path only. The merchant’s side of the same pipeline — the pools, the submission and validation queues, the cancellation requests — is the MY E-Invoice For Customer And Supplier applet. The equivalent request flow for a tenant-side buyer account, in a different applet and a different repository, is the MY E-Invoice Portal applet.

Where it fits

PositionApplet / systemWhy
ModuleE-Commerce, E-InvoiceA storefront feature that writes into the e-invoice pipeline.
Placed fromCP Commerce AdminWebsite → Layout Instance → Nodes → Widget ID → EINVOICE WIDGET.
Runs inThe Customer Portal web site and mobile app (wavelet-cp-commerce)The widget itself.
UpstreamPOS General, Shopping CartThe cash bill or sales invoice the shopper is asking about, and the pool row it produced.
DownstreamMY E-Invoice For Customer And SupplierThe request lands as a pool update and a submission-queue row that the operator sees there.
Buyer recordsCustomerA signed-in shopper’s identification, TIN and e-invoice address are written back to their customer entity; a guest’s are not.
SiblingMY E-Invoice PortalThe same request, made from a tenant-side buyer account instead of a storefront.

Screens and menus

There is no menu. The widget renders wherever the layout puts it, usually on a page whose code is E-Invoice — the tracking e-mail the backend sends links to https://<website code>/page/E-Invoice.

The widget draws a four-item left rail on desktop and a four-item tab strip on mobile. Three of the four are hidden unless the shopper is signed in. Below 550 px, and between 768 px and 1120 px, the rail labels shorten — Request E-Invoice becomes Request, E-Invoice Cancellation becomes Cancellation, and so on.

TabShown toWhat it does
Request E-InvoiceEveryoneThe search form, the result card and the request button.
Requested E-InvoicesSigned-in shoppers; guests only when the page URL carries key and emailWhat the shopper has already asked for, with status counters.
E-Invoice CancellationSigned-in shoppers onlyCancellation (rejection) requests raised and their outcome.
E-Invoice HistorySigned-in shoppers onlyEverything, including cancelled.

A Help Centre row sits at the bottom of the rail. It is a label with no click handler and no destination.

Request E-Invoice

Two search modes, as sub-tabs:

  • By Date Amount (the default) — Cash Bill/Invoice No., Amount, Transaction Date.
  • By Pin — Cash Bill/Invoice No., Pin. Hidden when the widget’s hideByPin parameter is true.

The transaction-date picker does not accept arbitrary dates; see The request window below.

The result is one card (grid view) or one row (list view), showing invoice number, transaction date, currency and amount. Which number is shown as “invoice number” is controlled by the widget’s einvoiceNoDisplayConfig parameter.

Three outcomes:

What came backWhat the shopper sees
A pool row that has not been submittedThe result card with a Request button.
A record that is already submitted, or has no pool row leftAn amber banner — “This invoice has already been submitted. No further requests can be made.” — above a read-only summary of Request Status, Submission Status, Validation Status, Submission Date and Validation Date. No Request button.
Nothing matchingA red box with the API’s message, or “Search failed. Please try again.”

When the widget’s exportPdfPublicAccess parameter is true and the record’s validation status is Valid, an Export button appears on this screen — for anyone who ran the search, signed in or not.

The request pop-up — Update E-Invoice Profile

Pressing Request opens a modal titled Update E-Invoice Profile. It opens for every guest, and for a signed-in shopper whose customer record is missing any of: TIN, identification number, identification type, e-invoice identification type, e-invoice identification value, e-mail, phone, or a billing/shipping address flagged as the default e-invoice address with address line 1, city, state, country and postal code all filled. A signed-in shopper with all of those already on file skips the modal and the request is sent straight away.

After the request

PathWhat the shopper sees
Signed inA modal — a tick, “Submission Successful”, “Thank you requesting for e-invoice. Your request will be processed.”, with Request More and Check Status buttons.
GuestA page message — “Your request will be processed.”
Either, on failure“Request already made by a user.”

That last message is shown for every failure of the request call, not only for a duplicate — an authorisation failure or a network error produces the same words.

Requested E-Invoices, Cancellation and History

All three are the same list component with different filters, each with a grid/list toggle, a search box (Search Invoice No/Cash Bill No), an After/Before date range and a status drop-down.

  • Requested E-Invoices — counters for All Invoices, In Queue, Validated, Validation Error.
  • E-Invoice Cancellation — All Invoices, Requested for Rejection, Cancelled.
  • E-Invoice History — All Invoices, In Queue, Validated, Validation Error, Requested for Rejection, Cancelled.

Each counter is a separate query, and what each one reads decides what the shopper should do about it:

CounterThe column it countsWhat it means
In QueueThe to-IRB header’s einvoice_document_status = IN_QUEUE or Submitted, plus pool rows whose queue_status = FAILEDEither LHDN has not seen it yet, or it has it and has not answered — or BigLedger tried and could not build or send it. All three sit under one number.
Validatedeinvoice_document_status = ValidLHDN accepted it.
Validation Erroreinvoice_document_status = Invalidnothing elseLHDN examined the e-invoice and refused it on content. A request that BigLedger never sent is not counted here.
Cancelled / Requested for RejectionCancelled; the rejection request’s own statusAs named.

So Validation Error always means the document reached LHDN and came back refused — which is the case the shopper cannot re-request, because the pool row behind it is already SUCCESS (see Troubleshooting). A request that failed on BigLedger’s own side is folded into In Queue.

The guest version of Requested E-Invoices is a separate, simpler component: no counters, no filters, driven entirely by the key and email in the URL.

The details pop-up

The magnifier icon — always present on a grid card, and on a list row only in the requested, cancellation and history lists — opens a pop-up with invoice number, transaction date, amount and status. When the status is Valid it also renders the LHDN validation QR code, as a base64 PNG held on the record, and an Export button that downloads the rendered e-invoice as E-invoice-<company>-<document type>-<date>-<invoice no>.pdf. On an iOS or Android user-agent the file opens in a new tab instead of downloading, because mobile browsers do not honour the download attribute on blob URLs.

Cancellation

On a validated e-invoice, and only while a countdown of 72 hours from the validation date-time has not run out, a Request for Rejection button appears with the remaining time beside it. It opens a radio list of reasons — Wrong buyer details, Wrong invoice details, Others by default, overridable per widget — with a free-text box when Others is chosen. Once raised, the button is replaced by Requested for Rejection in red.

The countdown is drawn by the widget. Whether the backend independently refuses a late cancellation from this path is not established here.

Configuration

Before you can use it

PrerequisiteWhereWhy
The hostname carries a CP Commerce public keyHost Name record for the storefront’s domain, extension parameter SYS_AKN_WEB_CP_COMMERCE_PUBLIC_KEYWithout it the storefront sends a blank Authorization header and every step of this flow returns 403 — see below.
The hostname carries a default tenant and a website GUIDSame record, SYS_DEFAULT_TENANT and SYS_AKN_WEB_CP_COMMERCE_CMS_WEBSITE_HDR_GUIDThe tenant code goes on every call; the website GUID is the path parameter on the guest request and guest tracking endpoints, and both reject an unknown one.
The company is configured for e-invoicing and its sales documents reach the poolsMY E-Invoice For Customer And SupplierThe search looks the shopper’s receipt up in the batch, single-general and individual pools. A document that never reached a pool cannot be found.
Receipts carry a PIN, if you want the By Pin searchPrintable format on the selling documentThe PIN searched here is the pool row’s pin_code.
An outgoing e-mail sender is configuredMessaging channel for the tenantThe guest tracking link is an e-mail. With no custom sender the platform default is used.

Placing the widget

CP Commerce Admin → Website → (select website) → Layout Instance → (select layout) → Nodes → (select or add a node) → Main → Widget ID.

The Widget ID drop-down is built by enumerating the shared widget catalogue, so the entry reads EINVOICE WIDGET and its stored value is e-invoice. The storefront resolves that value to the widget’s lazy-loaded module.

Widget parameters

The widget reads six parameters from its node’s widgetPlaceHolderConfig.param. Four of them work and two are inert. None of the six has a field in CP Commerce Admin: the widget is registered in the catalogue with params: false and the node editor has no parameter form for it, so the only way to set them is to edit the layout’s raw JSON on the node’s Json Params tab — which edits the whole layout instance, not the single node. This is recorded as a product finding.

ParameterWhat it controlsDefaultEffect when changed
hideByPinWhether the By Pin search sub-tab is offeredfalse — the tab is shownSet true on a storefront whose receipts carry no PIN, so shoppers are not sent down a search that can never match.
rejectionReasonsThe radio list in the cancellation-reason pop-upWrong buyer details, Wrong invoice details, OthersReplaces the list entirely. Only the literal value Others opens the free-text box, so keep it in the list.
einvoiceNoDisplayConfigWhich document number is displayed as the invoice numberserver_doc_1Accepts server_doc_1, client_doc_1client_doc_5, running_no, doc_reference. If the chosen field is empty on a record, the widget falls back to the invoice number.
exportPdfPublicAccessWhether the Export button appears on the search result for an unauthenticated visitorfalseSet true to let anyone who can produce the invoice number plus PIN (or amount and date) download the rendered e-invoice PDF.
styleDeclared as colour, font and backgroundInert. The values are read into component fields that no template binds; the widget’s colours are hard-coded.
voucherDetailsLayoutRouteInert. Declared on the widget’s config class and read nowhere; it is a leftover from the vouchers widget, whose config class it was copied from.

The public key is the whole guest path

The storefront has no anonymous session of its own. On start-up it looks its own hostname up and stores the hostname’s SYS_AKN_WEB_CP_COMMERCE_PUBLIC_KEY as the Authorization header for every call it makes until someone signs in, at which point the user’s own token replaces it.

That matters because of how the backend endpoints in this flow are declared:

CallEndpoint kindPermission check
Search for the receiptAuthenticated tenant endpointNone. The token only has to parse.
Verify TINAuthenticated tenant endpointNone.
Request, signed inAuthenticated tenant endpointThe caller must be a login entity of the buyer entity named on the request.
Request, guestAnonymous tenant endpointNone. The website GUID must exist.
Guest tracking listAuthenticated tenant endpointNone beyond a valid website GUID and both access_key and email present.
Export the PDFAuthenticated tenant endpointThe caller’s subject must exist as a login principal on the tenant.

So four of the six need a token and none of them needs the shopper’s. A blank Authorization header is treated as no token at all and falls through to an access-key check that a browser does not satisfy, so a storefront whose hostname record is missing the public key shows the search form and fails every search with an authorisation error. That is the single most likely cause of “the e-invoice page does not work”.

The request window

The transaction-date picker in the By Date Amount search only accepts dates inside a window derived from the company’s einvoice_settings_json.einvoice_request_ext_period:

  • No value set — only dates in the current calendar month are selectable.
  • A day-of-month value, and today is on or before that day — dates from the start of last month onwards are selectable.
  • A day-of-month value, and today is past it — only dates from the start of this month onwards.

This is a client-side restriction on the date picker. The search endpoint itself applies no date window.

Feature visibility / permissions

The widget gates its own features on one thing: whether the session holds an auth token. There are no client-side permission definitions for it, and no permission code is checked on the shopper’s behalf anywhere in this flow.

CapabilityGuestSigned-in shopper
Search for a receiptYesYes
Request an e-invoiceYesYes
Have the identity saved to a customer recordNoYes
See Requested E-InvoicesOnly through the e-mailed tracking linkYes
See E-Invoice Cancellation and E-Invoice HistoryNoYes
Request a cancellationNo — the effect chain is gated on an authorised sessionYes
Export the PDF from the search resultOnly when exportPdfPublicAccess is trueOnly when exportPdfPublicAccess is true
Export the PDF from the details pop-upThrough the tracking linkYes

Fields

Search — By Date Amount

FieldMeaningRequiredNotes
Cash Bill/Invoice No.The document number printed on the receiptYesMatched against the pool row’s server_doc_1, or the to-IRB header’s document reference.
AmountThe transaction totalYesFree text; no numeric validator on the form.
Transaction DateThe date on the receiptYesRestricted by the request window above. Sent as an ISO timestamp in Asia/Kuala_Lumpur.

Search — By Pin

FieldMeaningRequiredNotes
Cash Bill/Invoice No.As aboveYes
PinThe PIN printed on the receiptYesMatched against the pool row’s pin_code.

The endpoint rejects anything that is neither (invoice number + PIN) nor (invoice number + date + amount) with “Invalid request. Provide either (invoice_no + pin_no) OR (txn_date + txn_amount + invoice_no).”, and returns “No matching eInvoice record found for the given criteria.” when nothing matches.

Update E-Invoice Profile

FieldMeaningRequiredNotes / validation
NameThe buyer name that goes on the e-invoiceYesAlso sent as the buyer’s customer code on the guest path.
Identification TypeThe kind of identification documentYesDrop-down, exactly four values: PASSPORT, NRIC, BRN, ARMY. Nothing else can be chosen.
Identification NumberThe identification document numberYesMust contain no whitespace — “ID must not contain spaces”.
TINTax Identification NumberYesMust contain no whitespace — “TIN must not contain any space”. See Verify TIN below.
SST RegistrationSales and Service Tax registration numberNoThe only optional identity field.
EmailWhere the e-invoice notification goesYesOn the guest path this is also where the tracking link is sent, and the address the tracking list is keyed on.
Contact NumberBuyer phoneYesMust contain no -“Contact Number no must not contain “-””. Placeholder shows the +60… form.
Address Line 1First line of the e-invoice addressYes
Address Line 2–5Further address linesNo
Postal CodePostcodeYes
Mobile NumberSecond phoneNoMust contain no -.
CityCityYes
CountryCountryYesDrop-down from the country master; defaults to Malaysia.
StateStateYesDrop-down, filtered by the chosen country.

Verify TIN. The Update button is disabled until the Verify TIN button has returned a positive result for the exact combination of TIN, identification type and identification number currently in the form. The hint under the TIN field reads Verified in green or Not valid in red. Changing any of the three values after verifying invalidates the result and disables Update again.

What the identity becomes

Form fieldSigned-in shopper — written to the customer entityGuest — sent in the request payload
Namenamename, customer_code
Identification Typeid_type and einvoice_id_typeeinvoice_id_type
Identification Numberid_no and einvoice_id_valueeinvoice_id_value
TINeinvoice_tax_id_noeinvoice_tax_id_no
SST Registrationsst_numbersst_number
Emailemailemail
Contact Numberphonephone
Address blockAn entry in addresses_json flagged as the default e-invoice addressA buyer-address object on the request

A guest’s details are not saved anywhere as a customer record. They travel with the queue event and are written onto the document as the buyer JSON.

Lifecycle and effects

This is a request applet feature, not a document applet: nothing here writes a bl_fi_generic_doc_line, moves stock or posts to the ledger. It changes how an existing sales document is submitted to LHDN, and it writes one request-header row.

Step 1 — the endpoint decides whether there is anything to do. Before enqueuing, the controller reads the pool row named by the request and returns one of three process methods:

Process methodWhenResult
PROCESS_NORMAL_FLOWThe pool row’s queue status is anything but SUCCESSThe request is queued.
PROCESS_CONSOLIDATED_QUEUEA batch pool row already submitted, whose to-IRB header sits in the consolidated queueThe request is queued and a consolidated-queue rebuild is queued after it.
NO_PROCESSA single-general or individual pool row already succeeded, or a batch pool succeeded with nothing in a consolidated queueHTTP 400, “Request already made by someone or submitted as consolidated”.

A pool GUID that does not exist raises “Batch Pool Container Not Found”, “Single General Pool Container Not Found” or “Individual Pool Container Not Found”. A signed-in request with no buyer entity GUID is rejected with “Entity hdr guid is null”, and one whose caller is not a login entity of that buyer returns not-authorised.

Step 2 — the queue. The request is inserted into the tenant queue for EInvoiceCpComRequestQueueProcessor with the RUN_NOW strategy — queue code and name E_INVOICE_CP_COM_REQUEST_QUEUE_PROCESSOR, object type SYSTEM, and the processor’s own description: “This processor process batch bool to make e-invoice request from cp-commerce”. The event carries the pool GUIDs, the buyer entity GUID (signed in) or the whole guest buyer payload, the process method, the request’s Host header and the tenant code. For a guest the subject is the platform’s anonymous subject GUID.

Step 3 — what the processor does. It branches on which pool GUID is set.

PoolWhat changesThe answer to “individual or consolidated”
Batch poolThe buyer is written onto the pool and onto the generic document header, the pool is marked UNPROCESSED, and the generic document header’s einvoice_submission_type is set to INDIVIDUAL. The pool is then processed to a to-IRB submission-queue row, and the resulting to-IRB header GUID is written back to the document.Individual. A receipt that would otherwise have been swept into the merchant’s monthly consolidated e-invoice becomes an individual e-invoice in the shopper’s name. Where the receipt had already reached a consolidated queue, a consolidated-queue rebuild is queued so the consolidated document is reconstructed without it.
Single general poolThe buyer is written onto the pool row and the pool and its document are updated through the pool-to-IRB service.The submission the pool was already destined for, now carrying the buyer.
Individual poolThe same, on the individual pool row.As above.

If the batch-pool branch produces no submission-queue row, the request header is written with status FAILED rather than SUCCESS.

Step 4 — the request header. Either way one bl_fi_my_einvoice_request_hdr row is created, carrying the request status, the generic document GUID and server document type, transaction date and amount, the invoice number, the PIN, the to-IRB header GUID, the five server and five client document numbers, and any validation message. For a guest it additionally carries the e-mail address and a 12-character access key — the first 12 characters of a random UUID.

Step 5 — the guest e-mail. When the row has both an access key and an e-mail, the processor sends one message through Amazon SES, subject “Trace your e-invoice request status”. On SUCCESS the body says the request has been processed and offers a Track Your Request link; on FAILED it says the request failed and to contact the relevant authority. The link goes to the platform’s redirection endpoint, which checks that the key and e-mail match a request row and then redirects the shopper to https://<website code>/page/E-Invoice?key=…&email=…. The widget reads those two query parameters and shows the guest’s requested-invoice list.

What the merchant sees. The request surfaces on the merchant side as a changed pool row and a new submission-queue entry in MY E-Invoice For Customer And Supplier, with the buyer’s name, identification, TIN and address now on the document. There is no inbox of storefront requests, no approval step, and no notification to the merchant that a shopper asked.

Related applets

  • CP Commerce Admin — where the widget is placed on a page, and where every other storefront setting lives.
  • MY E-Invoice For Customer And Supplier — the merchant’s side of the pipeline this feeds: the pools, the submission and validation queues, the cancellation requests.
  • MY E-Invoice Portal — the same request made from a tenant-side buyer account rather than a storefront. Different applet, different repository, same backend tables.
  • Customer — where a signed-in shopper’s identification, TIN and default e-invoice address are stored and can be corrected.
  • POS General — the cash bill most storefront e-invoice requests are about, and where its PIN comes from.

Troubleshooting

SymptomCauseFix
Every search fails, with an authorisation error or nothing at allThe storefront’s hostname record has no SYS_AKN_WEB_CP_COMMERCE_PUBLIC_KEY, so the app sends a blank Authorization header and the backend treats it as no tokenAdd the public key extension parameter to the Host Name record for that domain. The app logs a configuration error naming Public Key on start-up when it is missing.
“No matching eInvoice record found for the given criteria.” on a receipt the shopper is holdingThe search is exact on all supplied values. Amount must match the pool row exactly; the date is taken as Asia/Kuala_Lumpur; the invoice number is matched against the document’s first server document numberTry the By Pin search instead, which matches on invoice number and PIN only. If the document never reached a pool, it will never be found here — check the posting queue on the merchant side.
The transaction date the shopper needs cannot be selectedThe date picker is limited to the current month, or the current plus previous month up to the company’s einvoice_request_ext_period dayThis is the configured request window. Widen it in the company’s e-invoice settings, or use the By Pin search, which has no date field.
“This invoice has already been submitted. No further requests can be made.”Either the record is already submitted, or the search matched a to-IRB header rather than a pool row, so there is no pool GUID left to request againstExpected once an e-invoice exists. If the shopper’s details are wrong on it, the remedy is a cancellation request, not a second request.
A request came back Validation Error and the shopper wants to correct it and ask againThey cannot. A pool row that was submitted reads queue_status = SUCCESS, and the controller returns NO_PROCESS for a SUCCESS row (MyEinvoiceRequestQueueService.checkInBatchPoolRequestAlreadyMade L71, checkInSingleGeneralPoolRequestAlreadyMade L106, checkIndividualPoolRequestAlreadyMade L120); the search then matches the to-IRB header and shows the amber banner with no Request buttonOnly the merchant can correct and resubmit it, from the pools or To IRB E-Invoice. A shopper whose request is still under In Queue because BigLedger never sent it can correct and request again — the pool row is not SUCCESS.
“Request already made by a user.”Shown for every failure of the request call — a genuine duplicate, an authorisation failure, and a network error all produce these wordsTreat it as “the request did not go through”, not as proof of a duplicate. Check the merchant-side pool row before telling the shopper it already exists.
Update stays greyed out in the profile pop-upVerify TIN has not returned a positive result for the exact TIN, identification type and identification number now in the formPress Verify TIN. If it returns Not valid, the combination was rejected upstream — the identification number and TIN must belong together.
The form will not accept a TIN or identification numberBoth reject any whitespace, including a trailing space pasted from a documentRemove spaces. The same applies to - in the phone and mobile fields.
Request for Rejection does nothing in list viewThe pop-up is opened as a different kind of dialog from the list view than from the grid view, and the component only works in the grid view’s formSwitch to grid view with the toggle at the top of the list. Recorded as a product finding.
A signed-in shopper’s other billing addresses disappear after they update their e-invoice profileThe profile save builds the billing-address array from the shipping addresses when both existRecorded as a product finding. Check and repair the customer’s addresses in the Customer applet after a shopper updates their profile.
A guest never receives the tracking e-mailThe e-mail is only composed for request statuses SUCCESS and FAILED; a status outside those two sends an empty message, and any SES failure is swallowedLook the request up on the merchant side by invoice number. Recorded as a product finding.
The widget cannot be configured from CP Commerce AdminIt is registered in the shared catalogue with no parameter form, and the applet has no editor for itEdit the layout JSON on the node’s Json Params tab. Recorded as a product finding.

Related documentation

Last updated on