DEV Community

Vege Su
Vege Su

Posted on Edited on

I got a 400 from the official MyInvois Postman collection — here is the fix

LHDN SANDBOX PLAYBOOK — VERIFIED OCT 2026

If you're building against Malaysia's LHDN MyInvois API, you probably did exactly what the docs said: imported the official Postman collection, registered a sandbox app, and hit POST /connect/token with Basic Auth as shipped.

You got this back:

HTTP/1.1 400 Bad Request
Enter fullscreen mode Exit fullscreen mode

No body. No hint. The auth method itself is the problem: the token endpoint rejects Basic Auth, no matter which credentials you send. The fix is embarrassingly simple — credentials go in the POST body:

curl -X POST 'https://preprod-api.myinvois.hasil.gov.my/connect/token' \
  -d 'grant_type=client_credentials' \
  -d 'client_id=<YOUR_CLIENT_ID>' \
  -d 'client_secret=<YOUR_CLIENT_SECRET>' \
  -d 'scope=InvoicingAPI'
Enter fullscreen mode Exit fullscreen mode

That returns 200 with an RS256 JWT (expires_in: 3600).

The environment matrix nobody draws

"MyInvois sandbox" (preprod) is not one host. Credentials are not portable between these:

What you're doing Host
Portal login / app registration preprod-mytax.hasil.gov.my
Taxpayer profile registration preprod-profile.hasil.gov.my
API calls preprod-api.myinvois.hasil.gov.my
Token (intermediary flow) preprod-identity.hasil.gov.my

The trap: the register button on the preprod landing page silently sends you to the production profile page. Register there, call the preprod API, and you'll collect 403 invalid_client all day. The credentials are correct. The environment isn't.

The second 400 that isn't your fault

The docs tell you to call:

GET /api/v1.0/taxpayer/validate/{TIN}
Enter fullscreen mode Exit fullscreen mode

It returns 400 — IdType/IdValue required. Those required query params are documented only in a FAQ entry:

GET /api/v1.0/taxpayer/validate/{TIN}?idType=NRIC&idValue=<IC_NUMBER>
Enter fullscreen mode Exit fullscreen mode

Verified behavior: correct TIN + IC pair → 200 with an empty body (empty = valid, don't panic). Mismatched pair → 404. Missing params → 400.

Three more evening-eaters

  • PIN email never arrives (Gmail, web signup). Use the MyTax app e-KYC instead — passport + selfie, PIN issued instantly.
  • Individual taxpayer form forces SST / Tourism Tax fields even with no SST obligation. No clean fix found — I got through with placeholder values; verify this one yourself.
  • preprod occasionally stalls ~20s or dies with a TLS broken pipe (curl (35)). Build retries into your client (--retry 2 --retry-all-errors).

What's still genuinely undocumented: the intermediary flow

The portal lets you register an app as an Intermediary — one app submitting invoices on behalf of other taxpayers (onbehalfof on token issuance). The actual mechanics (claim decoding, the taxpayer consent flow, what the portal allows vs. what the API enforces) are scattered across three doc pages and one FAQ answer. If you're building multi-tenant — a SaaS, a service bureau, an accounting integration — that's where your week goes.

I'm packaging the verified request/response templates for the whole flow, an error-code field guide, and the intermediary deep-dive into a developer toolkit. I'll announce it here when it ships.

If you'd rather not spend an evening rediscovering these 400s, get notified at launch: https://myinvois-toolkit.carrd.co

Everything above was verified against LHDN preprod in October 2026. Found a cleaner fix? Comment below — I genuinely want to know.

Top comments (0)