Guide
Getting started
composer require devix-labs/laravel-zatca
use Devix\Zatca\Zatca;
$payload = Zatca::encode([
'seller' => 'متجر ديفكس',
'vatNumber' => '300000000000003',
'timestamp' => now(),
'total' => 115.00,
'vatTotal' => 15.00,
]);
// 'ARPZhdiq2KzYsSDYr9mK2YHZg9izAg8zMDAwMDAwMDAwMDAwMDMD…'
That base64 string is the QR code's content. Render it with whatever QR library you already have — this package deliberately does not pick one for you, so it has no dependencies at all.
Laravel discovers the service provider, so a Zatca facade and a
SaudiVatNumber validation rule are available with no further setup.
Reading one back
This is the half nothing else in this space does, and it is the half you need when a customer sends a screenshot:
use Devix\Zatca\Check;
$result = Check::payload($payload, ['rate' => config('zatca.rate')]);
$result->ok; // false
$result->phase; // 1 or 2
$result->codes(); // ['vat-implausible']
$result->messages(); // ['vatTotal does not match the VAT rate (5.00, expected about 15.00)']
$result->invoice; // the decoded fields
Zatca::decode($payload);
// ['seller' => 'متجر ديفكس', 'vatNumber' => '3000…', 'timestamp' => '2025-03-01T10:00:00Z',
// 'total' => '115.00', 'vatTotal' => '15.00', 'phase' => 1, 'unknown' => []]
The same answers as the browser
There is a JavaScript twin, @devix-labs/e-invoice-qr,
and the two produce byte-identical payloads. That is not a claim in a README:
the vectors are generated once by the JavaScript package and written into both,
and both test suites read the same file. Neither is tested against the other's
opinion.
It matters because amounts are the one place the two languages disagree by default:
0.015 |
0.125 |
|
|---|---|---|
number_format($v, 2) |
0.02 |
0.13 |
sprintf('%.2f', $v) |
0.01 |
0.12 |
JavaScript toFixed(2) |
0.01 |
0.13 |
Amount::format($v) |
0.01 |
0.13 |
number_format pre-rounds with a fuzz correction; sprintf rounds half to even.
Neither matches. So Amount::format() expands the double to its exact decimal
and rounds the string half-away-from-zero, which is what ECMAScript specifies —
because 0.015 is really 0.0149999999999999994… and belongs below the
midpoint.
If the amount string is wrong by a hundredth, a phase-two hash is wrong, and the stamp is invalid.
Validating a VAT number in a form
use Devix\Zatca\Laravel\Rules\SaudiVatNumber;
$request->validate([
'vat_number' => ['required', new SaudiVatNumber()],
]);
Or as a string rule: 'vat_number' => 'required|saudi_vat_number'.
It checks fifteen digits beginning and ending with 3 — which is all Saudi Arabia publishes a rule for. There is no check digit, so nothing else can be verified without ZATCA's own lookup, and the docs say so rather than letting a pass mean more than it does.