Skip to content

Demo: This is a demo template by Ruji Labs. The business, people, products, prices and reviews on this page are fictional and do not represent any real company.

sahkit_

~/billing on main $ sahkit --help

Catch e-Invoice rejections before MyInvois does

Sahkit is an API that checks each e-Invoice against 64 MyInvois rules in about 40 ms. When something is wrong, it tells you the exact field and how to fix it, in plain English. When it is right, one more call submits it and hands you the QR code.

npm install @sahkit/node

1,000 free validations a month. No card. Sandbox keys in 30 seconds.

zsh: sahkit validate
200 OKregion: my-central-1p50 38 ms

Teams using Sahkit

Billing, POS and ERP teams that ship e-Invoices with Sahkit:

  • kedaiborong
  • Lorong.pay
  • TANJUNG/ERP
  • resit.io
  • Pasar Pintar
  • KAYUHAN

2.1M

invoices checked in August

312

Malaysian dev teams

94%

fewer MyInvois rejections after week one

A rejected e-Invoice should not cost you an evening

When an e-Invoice fails at LHDN, you often get a code, a field path and very little else. Your team then digs through the SDK docs, guesses, resubmits and waits again. Meanwhile the customer is on WhatsApp asking where their invoice is.

Sahkit runs the same checks before you submit, plus the ones that only fail in production: TINs that do not match the BRN, MSIC codes with a missing digit, tax totals that are off by one sen after rounding.

  • Errors name the field, the value and the fix
  • Same JSON shape for sandbox and live
  • Your invoice format in, MyInvois UBL out
A typical rejection today
"error": {
  "code": "BadArgument",
  "message": "Invalid value",
  "target": "Invoice.AccountingCustomerParty",
  "details": [ null ]
}

Which value? Which rule? Sent at 11:48 pm, rejected at 11:49 pm, fixed the next morning.

What Sahkit tells you
"code": "TIN_BRN_MISMATCH",
"path": "buyer.tin",
"message": "TIN C25845632010 is not linked to BRN 201901044521.",
"fix": "Ask the buyer to confirm their TIN, or use the general TIN for walk-in sales."

Caught in 38 ms, before the invoice ever left your server. Show the fix text straight to your user if you like.

Your first validation takes five minutes

One endpoint, one JSON body, one answer. The same request works from a shell, a Laravel job or a Lambda function.

  1. step 1/3

    Get a sandbox key

    Sign up with your work email. Your sk_test_ key is on the first screen, with 12 sample TINs that pass and fail on purpose.

  2. step 2/3

    Send one invoice

    Post the invoice in your own shape. Sahkit maps it to MyInvois UBL 2.1 and runs every check.

  3. step 3/3

    Fix, then submit

    Show the fix text to your user, or apply it in code. Call /submit on a clean result and store the UUID and QR.

curl https://api.sahkit.example/v1/invoices/validate \
  -H "Authorization: Bearer sk_test_sk9mQ2xR7vK4pLw3" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "INV-2026-00417",
    "issued_at": "2026-09-26T10:42:00+08:00",
    "supplier": { "tin": "C21584563200", "brn": "202401012345", "msic": "4711" },
    "buyer":    { "tin": "C25845632010", "brn": "201901044521" },
    "lines": [
      { "description": "Kopi O beg 1kg", "qty": 12, "unit_price": 18.50,
        "classification": "022", "tax_type": "01", "tax_rate": 10 }
    ],
    "currency": "MYR"
  }'
import Sahkit from "@sahkit/node";

const sahkit = new Sahkit(process.env.SAHKIT_KEY);

const result = await sahkit.invoices.validate({
  number: "INV-2026-00417",
  issuedAt: "2026-09-26T10:42:00+08:00",
  supplier: { tin: "C21584563200", brn: "202401012345", msic: "4711" },
  buyer: { tin: "C25845632010", brn: "201901044521" },
  lines: [{ description: "Kopi O beg 1kg", qty: 12, unitPrice: 18.5,
    classification: "022", taxType: "01", taxRate: 10 }],
});

if (result.status === "rejected") {
  for (const e of result.errors) console.log(e.path, e.fix);
} else {
  await sahkit.invoices.submit(result.id); // sends it on to MyInvois
}
import os
from sahkit import Sahkit

sahkit = Sahkit(os.environ["SAHKIT_KEY"])

result = sahkit.invoices.validate(
    number="INV-2026-00417",
    issued_at="2026-09-26T10:42:00+08:00",
    supplier={"tin": "C21584563200", "brn": "202401012345", "msic": "4711"},
    buyer={"tin": "C25845632010", "brn": "201901044521"},
    lines=[{"description": "Kopi O beg 1kg", "qty": 12, "unit_price": 18.50,
            "classification": "022", "tax_type": "01", "tax_rate": 10}],
)

if result.status == "rejected":
    for e in result.errors:
        print(e.path, e.fix)
else:
    sahkit.invoices.submit(result.id)  # sends it on to MyInvois
response422 · 38 ms
HTTP/1.1 422 Unprocessable Entity
x-request-id: req_7Hq2mV9cLkP4
x-sahkit-latency: 38ms

{
  "id": "val_01J8ZK4Q7M2WX9",
  "object": "validation",
  "status": "rejected",
  "document": "INV-2026-00417",
  "checks": { "run": 64, "passed": 62, "failed": 2 },
  "errors": [
    {
      "code": "MSIC_FORMAT",
      "path": "supplier.msic",
      "message": "MSIC code must be 5 digits. Got \"4711\".",
      "fix": "Use 47111 (retail, mainly food and drinks)."
    },
    {
      "code": "TIN_BRN_MISMATCH",
      "path": "buyer.tin",
      "message": "TIN C25845632010 is not linked to BRN 201901044521.",
      "fix": "Ask the buyer to confirm their TIN, or use the general TIN for walk-in sales."
    }
  ],
  "ready_to_submit": false,
  "livemode": false
}

Base URL https://api.sahkit.example/v1. Every response carries x-request-id; paste it in a support ticket and we can see the exact call.

Built for the people who get paged when invoices fail

Sahkit does the boring, exact work of e-Invoicing so your app can stay focused on selling. Every feature below is in the free tier.

  • 64 checks in one call

    Schema, TIN against BRN, 5-digit MSIC, classification and tax type codes, line totals, rounding to the sen and date windows. Everything MyInvois checks, plus what it only tells you after the fact.

    POST /v1/invoices/validate

  • Fixes, not error codes

    Each error comes with a field path and a fix sentence you can show a cashier or accounts clerk as-is. Add ?lang=ms and the messages come back in Bahasa Melayu.

    errors[].fix · ?lang=en|ms

  • Your JSON in, UBL 2.1 out

    Send invoices the way your app already stores them. A mapping file turns your field names into the MyInvois format, so you never hand-write UBL again.

    PUT /v1/mappings/{name}

  • Submit with a retry queue

    Clean invoices go on to MyInvois in one call. If the upstream is slow or rate-limited, we hold the invoice and retry for up to 72 hours with the same idempotency key. No double submissions.

    Idempotency-Key header

  • Consolidated and self-billed

    Roll up a month of walk-in cash sales into one consolidated e-Invoice by the 7th. Issue self-billed invoices for foreign suppliers and agents with the right buyer and seller swap.

    type: consolidated | self_billed

  • Seven-year audit trail

    Every request, response, UUID and QR is kept for seven years in Malaysian data centres. Search by invoice number or request ID and export a CSV for your auditor.

    GET /v1/documents?number=

Fast enough to sit inside your checkout

Validation runs in Kuala Lumpur and Cyberjaya with a warm copy of every code list, so a check adds less time than a card terminal beep. We publish the numbers, including the bad days.

all systems operational
last checked 26 Sep 2026, 10:45 MYT

latency, last 30 days

p5038 ms
p95112 ms
p99240 ms
uptime, 90 days99.98%
requests, August14.6M
regionsmy-central-1
my-central-2

validate API, p50 over the last 24 hours

spike 09:00 to 11:00 = month-end batch runs

Scale: 0 to 70 ms.

90-day uptime

90 days agotoday
  • validate APIoperational38 ms
  • submit relayoperational212 ms
  • webhooksoperational1.4 s
  • dashboardoperational—

18 Sep Webhook delivery ran 14 minutes late after a queue node restarted. No events were lost; all were delivered in order.

Stop polling. We call you when LHDN answers

MyInvois can take seconds or minutes to accept a document. Sahkit watches it for you and sends a signed webhook the moment the status changes, with the UUID and QR link inside.

your appPOS / ERP / billingsahkitvalidate + queueMyInvoisLHDN platformPOSTsubmitstatussigned webhook
median time from submit to accepted: 6.2 s
events
document.validatedall checks passed, ready to submit
document.submittedsent to MyInvois, waiting for a result
document.acceptedUUID, long ID and QR link are ready
document.rejectedupstream said no; errors translated
document.cancelledcancelled inside the 72-hour window
batch.completeda bulk upload finished; summary attached

retries on any non-2xx, then parked for replay

  1. #1 1 min
  2. #2 5 min
  3. #3 30 min
  4. #4 2 h
  5. #5 6 h
  6. #6 24 h

Each delivery carries a Sahkit-Signature header: a timestamp and an HMAC-SHA256 of the body. Replay any event from the dashboard or with POST /v1/events/:id/replay.

POST /hooks/sahkitevent body
{
  "id": "evt_01J8ZM0B3RK7",
  "type": "document.accepted",
  "created": 1790412180,
  "data": {
    "document": "INV-2026-00417",
    "myinvois_uuid": "F9D425P6DS7D8IU",
    "long_id": "RZ2KDMN4VF7XJQ09BTYCWN5L1734",
    "validated_at": "2026-09-26T10:43:02+08:00",
    "qr_url": "https://api.sahkit.example/v1/documents/INV-2026-00417/qr.png"
  }
}
verify.jsExpress
import { verifyWebhook } from "@sahkit/node";

app.post("/hooks/sahkit", express.raw({ type: "application/json" }), (req, res) => {
  const event = verifyWebhook(req.body, req.headers["sahkit-signature"],
    process.env.SAHKIT_WEBHOOK_SECRET); // throws if the HMAC or timestamp is off

  if (event.type === "document.accepted") markInvoiceFinal(event.data.document);
  res.sendStatus(200);
});

Official SDKs for the stacks Malaysian teams actually run

Typed models, automatic retries with backoff, idempotency keys set for you, and webhook verification in one function. All open source under MIT.

languagepackageversioninstallruns on
Node.js@sahkit/nodev3.2.0$ npm install @sahkit/nodeNode 18+, Bun, Deno
Pythonsahkitv3.1.4$ pip install sahkitPython 3.9 to 3.13, async client
PHPsahkit/sahkit-phpv2.8.0$ composer require sahkit/sahkit-phpPHP 8.1+, Laravel service provider
Gosahkit-gov1.9.2$ go get sahkit.example/go/sahkitGo 1.21+, context-aware
Java / Kotlinmy.sahkit:sahkit-javav1.4.0$ implementation 'my.sahkit:sahkit-java:1.4.0'Java 17+, Spring Boot starter
.NETSahkit.Netv1.2.1$ dotnet add package Sahkit.Net.NET 8, typed models
  • sahkit CLI

    Validate a folder of JSON files, tail webhooks locally, replay events.

    brew install sahkit

  • OpenAPI 3.1 spec

    Generate your own client, or import it into Postman or Insomnia.

    GET /v1/openapi.json

  • Test TIN set

    12 sandbox TINs and BRNs that pass, mismatch or are suspended on purpose.

    docs/sandbox

What changed for teams that switched

  • rejections / month3,100 → 41

    "We had 3,100 rejected invoices in our first month on MyInvois. After we put Sahkit in front of submit, we had 41. Most of those were buyers with the wrong TIN on file, and the fix text told our cashiers what to ask."

    Farah Idayu
    Backend lead, Pasar Pintar (fictional)

  • integration time4 hours

    "The Laravel package took an afternoon. The part I did not expect to love is the mapping file: our invoice table has 40 columns with names from 2014, and we never had to rename one."

    Kumar Selvam
    CTO, Lorong.pay (fictional)

  • month-end close2 days → 20 min

    "Consolidated e-Invoices for 18 outlets used to be two days of Excel at month end. Now a cron job posts one batch on the 1st and the webhook tells us when LHDN has it."

    Tan Wei Ling
    Engineering manager, KAYUHAN (fictional)

Pay per request, in ringgit

A request is one validate or one submit call. Webhooks, retries, replays and the dashboard are free. Sandbox calls never count.

Prices exclude 8% SST. Billed monthly by FPX, DuitNow or card.

  • plan: free

    RM 0forever

    1,000 live requests / month

    hard stop at the limit

    • Unlimited sandbox requests
    • All 64 checks + submit
    • Webhooks and SDKs
    • Community forum, 2-day email reply
    Get a free key
  • plan: build

    most teams

    RM 49per month

    20,000 requests included

    then RM 0.004 per request

    • Everything in free
    • Consolidated + self-billed
    • 3 API keys per environment
    • Email support, next working day
    Start on Build
  • plan: scale

    RM 390per month

    250,000 requests included

    then RM 0.0025 per request

    • Everything in build
    • 99.95% uptime SLA, credits if missed
    • Two-region failover
    • Slack channel, 2-hour reply 9am to 9pm
    Start on Scale

0requests per month1M

free
over cap
build
RM 209.00
scale
RM 390.00

> Build is cheapest at this volume. Over 5 million a month? Ask for volume pricing.

Shipped this quarter

We ship every week and never break v1 of the API. Breaking changes get a new version and 12 months of overlap.

Read the full changelog
  1. v3.2addedConsolidated e-Invoice validation for up to 10,000 lines per batch.
  2. v3.1addedSelf-billed invoices, with buyer and seller swapped for you.
  3. sdkchangedPython SDK 3.1: async client and Python 3.13 support.
  4. v3.0addedWebhook replay from the dashboard and the API.
  5. apifixedTax totals on 3-decimal unit prices now round the same way as MyInvois.

Questions developers ask first

Something else? Our engineers answer on WhatsApp and email from 9am to 9pm, Monday to Saturday.

[email protected]
+60 12-000 0000

Ask an engineer
Is Sahkit part of LHDN or MyInvois?

No. Sahkit is an independent API that sits between your app and MyInvois. We check invoices against the published rules and code lists, and submit them using your own taxpayer credentials. LHDN remains the only party that validates an e-Invoice.

Do I still need my own MyInvois client ID and secret?

Yes, for live submissions. You add them once in the dashboard and we store them encrypted. Validation alone works without them, so you can try Sahkit before you register as an intermediary.

What happens when MyInvois is slow or down?

Validation keeps working, because it runs on our side. Submit calls return 202 and join a queue. We retry for up to 72 hours with the same idempotency key, and send a webhook when each document is accepted or rejected.

Where is my invoice data stored?

In two data centres in Malaysia, encrypted at rest. We keep documents for seven years, the same period you must keep tax records. You can export or delete them from the dashboard at any time, subject to that retention rule.

Can I show your error messages to my own users?

Yes, that is what the fix field is for. Messages are short, in plain English or Bahasa Melayu, and never include internal codes your cashier would not understand.

How does the free tier work?

You get 1,000 live requests every month and unlimited sandbox calls, with every feature switched on. When you reach the limit, live calls return 429 until the 1st. No card needed.

Can I cancel or change plans?

Any time, from the dashboard. Upgrades apply straight away and are pro-rated. Downgrades apply on your next billing date. There is no annual contract.

Validate your first invoice before your coffee cools

Free sandbox key, 1,000 live requests a month, every feature on. Most teams send their first clean e-Invoice the same afternoon.

$ sahkit login

✓ no card ✓ sandbox in 30 s ✓ cancel any time

Demo only

This button is part of a demo template. Nothing was sent, booked or charged, and there is no real business behind this page.

Want a page like this for your business?

QR code: Darus Ishak, Ruji Labs contact cardScan or tap to get the contact card of Darus Ishak, Ruji Labs.Open contact card ↗