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
| Position | Applet / system | Why |
|---|---|---|
| Module | E-Commerce, E-Invoice | A storefront feature that writes into the e-invoice pipeline. |
| Placed from | CP Commerce Admin | Website → Layout Instance → Nodes → Widget ID → EINVOICE WIDGET. |
| Runs in | The Customer Portal web site and mobile app (wavelet-cp-commerce) | The widget itself. |
| Upstream | POS General, Shopping Cart | The cash bill or sales invoice the shopper is asking about, and the pool row it produced. |
| Downstream | MY E-Invoice For Customer And Supplier | The request lands as a pool update and a submission-queue row that the operator sees there. |
| Buyer records | Customer | A signed-in shopper’s identification, TIN and e-invoice address are written back to their customer entity; a guest’s are not. |
| Sibling | MY E-Invoice Portal | The 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.
| Tab | Shown to | What it does |
|---|---|---|
| Request E-Invoice | Everyone | The search form, the result card and the request button. |
| Requested E-Invoices | Signed-in shoppers; guests only when the page URL carries key and email | What the shopper has already asked for, with status counters. |
| E-Invoice Cancellation | Signed-in shoppers only | Cancellation (rejection) requests raised and their outcome. |
| E-Invoice History | Signed-in shoppers only | Everything, 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
hideByPinparameter 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 back | What the shopper sees |
|---|---|
| A pool row that has not been submitted | The result card with a Request button. |
| A record that is already submitted, or has no pool row left | An 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 matching | A 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
| Path | What the shopper sees |
|---|---|
| Signed in | A modal — a tick, “Submission Successful”, “Thank you requesting for e-invoice. Your request will be processed.”, with Request More and Check Status buttons. |
| Guest | A 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:
| Counter | The column it counts | What it means |
|---|---|---|
| In Queue | The to-IRB header’s einvoice_document_status = IN_QUEUE or Submitted, plus pool rows whose queue_status = FAILED | Either 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. |
| Validated | einvoice_document_status = Valid | LHDN accepted it. |
| Validation Error | einvoice_document_status = Invalid — nothing else | LHDN examined the e-invoice and refused it on content. A request that BigLedger never sent is not counted here. |
| Cancelled / Requested for Rejection | Cancelled; the rejection request’s own status | As 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
| Prerequisite | Where | Why |
|---|---|---|
| The hostname carries a CP Commerce public key | Host Name record for the storefront’s domain, extension parameter SYS_AKN_WEB_CP_COMMERCE_PUBLIC_KEY | Without 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 GUID | Same record, SYS_DEFAULT_TENANT and SYS_AKN_WEB_CP_COMMERCE_CMS_WEBSITE_HDR_GUID | The 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 pools | MY E-Invoice For Customer And Supplier | The 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 search | Printable format on the selling document | The PIN searched here is the pool row’s pin_code. |
| An outgoing e-mail sender is configured | Messaging channel for the tenant | The 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.
| Parameter | What it controls | Default | Effect when changed |
|---|---|---|---|
hideByPin | Whether the By Pin search sub-tab is offered | false — the tab is shown | Set true on a storefront whose receipts carry no PIN, so shoppers are not sent down a search that can never match. |
rejectionReasons | The radio list in the cancellation-reason pop-up | Wrong buyer details, Wrong invoice details, Others | Replaces the list entirely. Only the literal value Others opens the free-text box, so keep it in the list. |
einvoiceNoDisplayConfig | Which document number is displayed as the invoice number | server_doc_1 | Accepts server_doc_1, client_doc_1…client_doc_5, running_no, doc_reference. If the chosen field is empty on a record, the widget falls back to the invoice number. |
exportPdfPublicAccess | Whether the Export button appears on the search result for an unauthenticated visitor | false | Set true to let anyone who can produce the invoice number plus PIN (or amount and date) download the rendered e-invoice PDF. |
style | Declared as colour, font and background | — | Inert. The values are read into component fields that no template binds; the widget’s colours are hard-coded. |
voucherDetailsLayoutRoute | — | — | Inert. 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:
| Call | Endpoint kind | Permission check |
|---|---|---|
| Search for the receipt | Authenticated tenant endpoint | None. The token only has to parse. |
| Verify TIN | Authenticated tenant endpoint | None. |
| Request, signed in | Authenticated tenant endpoint | The caller must be a login entity of the buyer entity named on the request. |
| Request, guest | Anonymous tenant endpoint | None. The website GUID must exist. |
| Guest tracking list | Authenticated tenant endpoint | None beyond a valid website GUID and both access_key and email present. |
| Export the PDF | Authenticated tenant endpoint | The 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.
| Capability | Guest | Signed-in shopper |
|---|---|---|
| Search for a receipt | Yes | Yes |
| Request an e-invoice | Yes | Yes |
| Have the identity saved to a customer record | No | Yes |
| See Requested E-Invoices | Only through the e-mailed tracking link | Yes |
| See E-Invoice Cancellation and E-Invoice History | No | Yes |
| Request a cancellation | No — the effect chain is gated on an authorised session | Yes |
| Export the PDF from the search result | Only when exportPdfPublicAccess is true | Only when exportPdfPublicAccess is true |
| Export the PDF from the details pop-up | Through the tracking link | Yes |
Fields
Search — By Date Amount
| Field | Meaning | Required | Notes |
|---|---|---|---|
| Cash Bill/Invoice No. | The document number printed on the receipt | Yes | Matched against the pool row’s server_doc_1, or the to-IRB header’s document reference. |
| Amount | The transaction total | Yes | Free text; no numeric validator on the form. |
| Transaction Date | The date on the receipt | Yes | Restricted by the request window above. Sent as an ISO timestamp in Asia/Kuala_Lumpur. |
Search — By Pin
| Field | Meaning | Required | Notes |
|---|---|---|---|
| Cash Bill/Invoice No. | As above | Yes | |
| Pin | The PIN printed on the receipt | Yes | Matched 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
| Field | Meaning | Required | Notes / validation |
|---|---|---|---|
| Name | The buyer name that goes on the e-invoice | Yes | Also sent as the buyer’s customer code on the guest path. |
| Identification Type | The kind of identification document | Yes | Drop-down, exactly four values: PASSPORT, NRIC, BRN, ARMY. Nothing else can be chosen. |
| Identification Number | The identification document number | Yes | Must contain no whitespace — “ID must not contain spaces”. |
| TIN | Tax Identification Number | Yes | Must contain no whitespace — “TIN must not contain any space”. See Verify TIN below. |
| SST Registration | Sales and Service Tax registration number | No | The only optional identity field. |
| Where the e-invoice notification goes | Yes | On the guest path this is also where the tracking link is sent, and the address the tracking list is keyed on. | |
| Contact Number | Buyer phone | Yes | Must contain no - — “Contact Number no must not contain “-””. Placeholder shows the +60… form. |
| Address Line 1 | First line of the e-invoice address | Yes | |
| Address Line 2–5 | Further address lines | No | |
| Postal Code | Postcode | Yes | |
| Mobile Number | Second phone | No | Must contain no -. |
| City | City | Yes | |
| Country | Country | Yes | Drop-down from the country master; defaults to Malaysia. |
| State | State | Yes | Drop-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 field | Signed-in shopper — written to the customer entity | Guest — sent in the request payload |
|---|---|---|
| Name | name | name, customer_code |
| Identification Type | id_type and einvoice_id_type | einvoice_id_type |
| Identification Number | id_no and einvoice_id_value | einvoice_id_value |
| TIN | einvoice_tax_id_no | einvoice_tax_id_no |
| SST Registration | sst_number | sst_number |
email | email | |
| Contact Number | phone | phone |
| Address block | An entry in addresses_json flagged as the default e-invoice address | A 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 method | When | Result |
|---|---|---|
PROCESS_NORMAL_FLOW | The pool row’s queue status is anything but SUCCESS | The request is queued. |
PROCESS_CONSOLIDATED_QUEUE | A batch pool row already submitted, whose to-IRB header sits in the consolidated queue | The request is queued and a consolidated-queue rebuild is queued after it. |
NO_PROCESS | A single-general or individual pool row already succeeded, or a batch pool succeeded with nothing in a consolidated queue | HTTP 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.
| Pool | What changes | The answer to “individual or consolidated” |
|---|---|---|
| Batch pool | The 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 pool | The 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 pool | The 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
| Symptom | Cause | Fix |
|---|---|---|
| Every search fails, with an authorisation error or nothing at all | The 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 token | Add 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 holding | The 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 number | Try 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 selected | The date picker is limited to the current month, or the current plus previous month up to the company’s einvoice_request_ext_period day | This 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 against | Expected 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 again | They 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 button | Only 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 words | Treat 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-up | Verify TIN has not returned a positive result for the exact TIN, identification type and identification number now in the form | Press 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 number | Both reject any whitespace, including a trailing space pasted from a document | Remove spaces. The same applies to - in the phone and mobile fields. |
| Request for Rejection does nothing in list view | The 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 form | Switch 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 profile | The profile save builds the billing-address array from the shipping addresses when both exist | Recorded 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-mail | The 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 swallowed | Look the request up on the merchant side by invoice number. Recorded as a product finding. |
| The widget cannot be configured from CP Commerce Admin | It is registered in the shared catalogue with no parameter form, and the applet has no editor for it | Edit the layout JSON on the node’s Json Params tab. Recorded as a product finding. |
Related documentation
- CP Commerce Admin applet — the console this widget is placed from.
- Push Notification Configuration — the other CP Commerce feature documented screen by screen.
- E-Invoice module — how the Malaysian e-invoice pipeline fits together.
- E-Commerce module — the storefront this widget lives in.