Skip to content
Devix Open Source

Guide

Phase two

Saudi e-invoicing has two phases. Phase one is the five tags and a printed QR code. Phase two — the integration phase — adds a cryptographic stamp, and the payload carries four more tags:

Tag
xmlHash 6 Base64 SHA-256 of the signed XML invoice.
signature 7 The ECDSA signature, base64.
publicKey 8 The ECDSA public key, base64.
certificateSignature 9 ZATCA's signature over that key. Simplified invoices only.
$payload = Zatca::encode([
    'seller' => 'متجر ديفكس',
    'vatNumber' => '300000000000003',
    'timestamp' => '2025-03-01T10:00:00Z',
    'total' => '115.00',
    'vatTotal' => '15.00',

    'xmlHash' => $hash,
    'signature' => $signature,
    'publicKey' => $publicKey,
    'certificateSignature' => $certificateSignature,
]);

Check::payload($payload)->phase;   // 2

Give all of them and you get a phase two payload; give none and you get phase one. Give some and Check reports phase-2-incomplete, because a hash without a signature is not a stamped invoice — it is a broken one.

What this package does not do

It does not sign. Producing the stamp means generating a CSR, getting a certificate from ZATCA's own certificate authority, canonicalising the XML invoice, hashing it and signing that hash — through an onboarding flow with compliance checks at each step.

That is a different product. It needs real credentials to test against, it has consequences if it is wrong, and shipping a half-tested version would be worse than shipping none. What this does is take the hash, signature and key you already have and place them in the payload correctly, which is the part that is easy to get subtly wrong and easy to verify.

If you need the signing side today, salla/zatca does it and is where that work has been done. The two sit together perfectly well: sign with theirs, and encode and check with this.

Values are passed through byte for byte

$decoded = Zatca::decode($payload);
$decoded['xmlHash'];     // exactly as it was encoded
$decoded['publicKey'];

This matters more here than anywhere else. These strings are inputs to a signature check, and a library that trimmed, decoded or re-encoded them on the way past would make a valid stamp look invalid.

A timestamp that was signed must not be reformatted

If the ISO 8601 string was part of what the invoice hash covers, rewriting it invalidates the stamp. So a string already in ISO 8601 is passed through untouched — only a DateTimeInterface, or a string in some other shape, is formatted:

Timestamp::format('2025-03-01T13:00:00+03:00');   // unchanged, offset and all
Timestamp::format(now());                          // '2025-03-01T10:00:00Z'

Is there a UAE equivalent?

No — worth saying plainly, because the question comes up.

The UAE's e-invoicing programme is built on Peppol, with accredited service providers exchanging structured documents. There is no UAE counterpart to the ZATCA TLV QR code, so there is nothing here for it. If that changes, this package is the right place for it; until then, inventing a format would be worse than leaving it out.

Updated 15 Sep 2026