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
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()andCheck::payload()besideZatca::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-qrgenerates 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_formatpre-rounds with a fuzz correction and turns0.015into0.02;sprintfrounds half to even and turns0.125into0.12. JavaScript produces0.01and0.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 — soAmount::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
%02Xformat 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.
More from Devix
All resources →Laravel Phone
Laravel package
Phone validation, casts and a Blade component that agree with Phone Input exactly.
Laravel Validators
Laravel package
Validation rules for regional IDs, tax numbers and IBANs.
Laravel Cookie Consent
Laravel package
Consent banner middleware with a proof-of-consent log.