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 QR 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.
const payload = encodeInvoice({
  seller: 'متجر ديفكس',
  vatNumber: '300000000000003',
  timestamp: '2025-03-01T10:00:00Z',
  total: '115.00',
  vatTotal: '15.00',

  xmlHash: 'NWZlYzViZGUxNjBmMTBjM2RjNWMxNGQzYzFmZjRmNjY=',
  signature: 'MEQCIDGJRnLhCHUOLLrJCXY3HcOG…',
  publicKey: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE…',
  certificateSignature: 'MEUCIQCvUPsgqRCXpHXnQSHcCfBqnAZMXCACjRlb…',
});

check(payload).phase;   // 2

Give all four 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 with the certificate's key — 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 of it would be worse than shipping none. What this does is take the hash, signature and key you already have and put them into the payload correctly — which is the part that is easy to get subtly wrong and easy to verify.

If you are looking for the signing side today, the PHP packages listed in the teardown do it, and they are where that work has been done.

Reading a stamped invoice

const decoded = decodeInvoice(payload);

decoded.phase;         // 2
decoded.xmlHash;       // the base64 hash, exactly as it was encoded
decoded.publicKey;

The values come back byte for byte. That matters here more than anywhere else: these strings are inputs to a signature check, and a library that trims, decodes or re-encodes them on the way past would make a valid stamp look invalid.

Verifying the signature itself is out of scope for the reason above — but you have the key, the hash and the signature in your hands, and crypto.subtle has the rest.

Is there a UAE equivalent?

No — and this is 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