Skip to content
Devix Open Source
Library v1.0.0 MIT Alpha

E-Invoice QR

ZATCA e-invoice QR codes and UAE FTA invoice helpers.

npm install @devix-labs/e-invoice-qr
Vanilla JS PHP Laravel WordPress
Write one, then read it back Open

The Saudi ZATCA QR payload, encoded and decoded. Byte-correct TLV, so an Arabic seller name produces a payload that scanners can actually read; amounts and timestamps formatted the one way that survives being hashed and stamped; a value too long for a one-byte length refused rather than silently wrapped. And the half nothing else in this space does: decode a payload back to its fields and check it, with reason codes rather than a boolean. Phase one and phase two tags, 2.6 kB, zero dependencies.

What you get

It reads codes, not just writes them

Every other package in this space encodes and stops. decodeInvoice() and check() give an auditor, a receiving system or a support engineer with a screenshot the answer they need.

The Arabic length trap, handled

The TLV length is bytes, not characters. 'سلة' is 3 characters and 6 bytes, and getting that wrong produces a payload no scanner can read. It is the commonest bug here, and it is a committed regression test.

Too long is refused, not wrapped

The length field holds one byte. The dominant implementation formats it with %02X, which wraps past 255 and corrupts the payload in silence. This throws, naming the tag and the size.

Amounts that survive being stamped

The amount string is what gets hashed. toLocaleString on a Saudi device returns Arabic-Indic digits and a thousands separator; formatAmount returns 115.00, always, and throws rather than guessing.

A signed timestamp is left alone

If the ISO string was part of what the hash covers, rewriting it invalidates the stamp. One already in ISO 8601 passes through byte for byte.

Reason codes, not a boolean

Twelve codes saying which field is wrong and what was there — the same shape as the rest of our regional validators, so one error surface covers them all.

Honest about the VAT number

Fifteen digits beginning and ending with 3, and that is all that can be verified — Saudi Arabia publishes no check digit. A library that implies more is overselling.

Phase two, all or nothing

Tags 6 to 9 for the cryptographic stamp, and a hash without a signature reported as incomplete rather than accepted as a stamped invoice.

E-Invoice QR — overview

Why it exists

Saudi Arabia mandates a QR code on every invoice, so this is not an optional nicety — an invoice without a correct one is a compliance problem.

The work has been done in PHP, where invoicing lives: salla/zatca has 487 thousand installs and is the de facto answer. On npm it has not: six packages, the two most used last published in 2021 and 2022, none above 2.8 thousand downloads a week. And not one package, in either registry, reads a payload back. They all encode and stop.

What it does differently

  • It decodes. decodeInvoice() and check() beside encodeInvoice(), so an auditor, a receiving system, or a support engineer holding a customer's screenshot can find out what is actually in a code and what a tax authority's scanner would say about it.
  • The byte-length trap is closed. The TLV length field counts bytes; a Saudi seller's name is Arabic and every letter is two of them. The reference PHP implementation carries a comment warning about this on the very method, because it is the bug everyone hits. Both of its Arabic and ASCII vectors are committed regression tests here.
  • A value over 255 bytes is refused, with the tag and the size named, instead of wrapping silently through a %02X format and producing a payload that decodes to nonsense.
  • Amounts and timestamps are produced one way. The strings are what get hashed and stamped in phase two, so toLocaleString — which on a Saudi device returns Arabic-Indic digits — must never come near them. A timestamp already in ISO 8601 is passed through byte for byte, because reformatting one that was signed invalidates the stamp.
  • Reason codes, in the same shape as the rest of our regional validators.
  • 2.6 kB, no dependencies, and it runs in a browser, which none of the PHP answers can.

Not in 1.0

Signing. The phase-two cryptographic stamp needs a CSR, a certificate from ZATCA's own authority, canonicalised XML, and ECDSA over its hash — through an onboarding flow with compliance checks at each step. It needs real credentials to test against and it has consequences when it is wrong. A half-tested signer would be worse than none, so this takes the hash, signature and key you already have and places them correctly.

A UAE FTA code. There isn't one. The UAE's programme is Peppol-based, with no counterpart to the ZATCA TLV QR, and inventing a format would be worse than leaving it out.

How it compares

Questions

Does it sign the invoice?

No, deliberately. The phase-two stamp means a CSR, a certificate from ZATCA's own authority, canonicalised XML and ECDSA over its hash, through an onboarding flow with compliance checks. That is a different product, it needs real credentials to test against, and a half-tested version would be worse than none. This takes the hash, signature and key you already have and puts them in the payload correctly.

Is there a UAE FTA version of this?

No. The UAE's programme is Peppol-based, with accredited service providers exchanging structured documents; there is no UAE counterpart to the ZATCA TLV QR code. Inventing a format would be worse than leaving it out, so there is nothing here for it.

Why does my Arabic seller name produce an unreadable code elsewhere?

Because the TLV length field counts bytes and the code counted characters. 'سلة' is three characters and six bytes; a length of 3 truncates the value and every tag after it shifts. Nothing here uses .length on a value.

How do I render the QR code itself?

The base64 string this returns is the QR code's content — any generator will do. Devix QR Code is two lines and has no dependency on this package, nor this on it.

What can a VAT number check actually prove?

That it is fifteen digits beginning and ending with 3. Saudi Arabia publishes no check digit, so no arithmetic distinguishes a real number from a well-formed invention. Only ZATCA's own lookup can confirm registration, and we would rather say so than let a passing result mean more than it does.