Skip to content
Devix Open Source
Laravel package v1.0.0 MIT Stable

Laravel ZATCA

The Saudi ZATCA e-invoice QR payload in PHP — read as well as written, and byte-identical to the browser.

composer require devix-labs/laravel-zatca
PHP Laravel
Laravel ZATCA

Live demo coming soon

Encode and decode the ZATCA QR payload in PHP and Laravel, with twelve reason codes instead of a boolean. Byte-correct TLV so Arabic seller names work; a value too long for a one-byte length refused rather than silently wrapped; and amounts formatted to match JavaScript exactly — which neither number_format nor sprintf manages, and which matters because the amount string is what gets hashed and stamped. A SaudiVatNumber validation rule, a facade, and zero dependencies. Held to the same generated vectors as the @devix/e-invoice-qr JavaScript package.

What you get

It reads codes, not just writes them

Nothing else in any registry decodes a ZATCA payload and says whether it is valid. That is what you need when a customer sends a screenshot of an invoice that will not scan.

The same bytes as the browser

The JavaScript twin generates one set of vectors that is written into both packages, and both test suites read that file. Neither is tested against the other's opinion.

Amounts PHP gets wrong by default

number_format(0.015, 2) gives 0.02 and sprintf('%.2f', 0.125) gives 0.12; JavaScript gives 0.01 and 0.13. The amount string is what gets hashed, so this rounds the exact decimal the way ECMAScript specifies.

Too long is refused, not wrapped

The TLV length holds one byte. The dominant implementation formats it with %02X, which wraps past 255 and corrupts the payload in silence — 128 Arabic characters is enough.

A signed timestamp is left alone

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

A validation rule for the form

SaudiVatNumber, or the saudi_vat_number string rule, with a message that says which part failed rather than “invalid”.

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. Only ZATCA's own lookup can confirm registration.

No dependencies, not even a QR library

The payload is a string; you already have a way to draw one. Nothing is pulled in, and nothing is chosen for you.

Laravel ZATCA — overview

Why it exists

Saudi Arabia mandates a QR code on every invoice, and invoicing happens on a server, so PHP is where this work lives. salla/zatca has 487 thousand installs and is the de facto answer for producing one.

But not one package, in any registry, reads a payload back. They all encode and stop. When a customer sends a screenshot of an invoice that will not scan, or a receiving system has to check what arrived, or an auditor asks what is actually in the code, there is nothing to reach for.

What it does differently

  • It decodes, and it checks. Zatca::decode() and Check::payload() beside Zatca::encode(), with twelve reason codes rather than a boolean — the same shape as the rest of our regional validators.
  • It agrees with the browser, byte for byte. The JavaScript twin @devix/e-invoice-qr generates one set of vectors that is written into both packages, and both test suites read that file. Neither is tested against the other's opinion.
  • Amounts are formatted correctly, which neither PHP builtin manages. number_format pre-rounds with a fuzz correction and turns 0.015 into 0.02; sprintf rounds half to even and turns 0.125 into 0.12. JavaScript produces 0.01 and 0.13. Since the amount string is what gets hashed and stamped in phase two, a hundredth of a riyal is the difference between a valid stamp and an invalid one — so Amount::format() expands the double to its exact decimal and rounds the string half-away-from-zero, as ECMAScript specifies.
  • A value over 255 bytes is refused, with the tag and size named, instead of wrapping through a %02X format and producing a payload that decodes to nonsense. 128 Arabic characters is enough to trigger that elsewhere.
  • A signed timestamp is never reformatted, because rewriting an ISO string that a hash covers invalidates the stamp.
  • Zero dependencies, including no QR library — the payload is a string, and you already have a way to draw one.

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. It needs real credentials to test against and has consequences when it is wrong. salla/zatca does it and is where that work has been done; the two sit together well — sign with theirs, encode and check with this.

A UAE FTA code. There isn't one. The UAE programme is Peppol-based, with no counterpart to the ZATCA TLV QR.

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 at each step. It needs real credentials to test against and a half-tested version would be worse than none. salla/zatca does the signing, and the two sit together perfectly well: sign with theirs, encode and check with this.

Why not just use number_format for the amounts?

Because it does not agree with the browser. number_format pre-rounds with a fuzz correction and turns 0.015 into 0.02; sprintf rounds half to even and turns 0.125 into 0.12; JavaScript's toFixed gives 0.01 and 0.13. Since the amount string is what gets hashed and stamped, a hundredth of a riyal is the difference between a valid stamp and an invalid one.

How do I know it really matches the JavaScript package?

The vectors are generated once, by the JavaScript package, and written into both. Both test suites read that same file, so neither side is tested against the other's opinion. Eleven invoices covering Arabic and mixed scripts, emoji, long names, offsets and rounding edges.

Which QR library should I use to draw it?

Whichever you already have — the payload is just a base64 string. This package has no dependencies and does not choose one for you. In a browser, @devix/qr-code is two lines.

Is there a UAE FTA version?

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.