A ZATCA-compliant invoice carries three cryptographic artefacts: a cryptographic stamp (an XAdES enveloped signature over the XML), a CSID (the digital certificate that identifies the solution which stamped it), and a QR code encoding nine TLV fields in Base64. Who produces them depends on the invoice type — and that is the detail most implementations get wrong.
For simplified invoices your own solution stamps the XML and builds the QR code. For standard invoices ZATCA does both at clearance and hands them back to you. Build the QR generator for B2C and you will need it; build it for B2B and you have written code that will disagree with the authoritative document.
Engineering context, not tax advice. The authority here is ZATCA's Electronic Invoice Security Features Implementation Standard; the figures below are from version 1.2 (19 May 2023). Check the version in force before you implement.
Onboarding: how a solution gets its identity
An E-Invoice Generation Solution (EGS) cannot stamp anything until it has been onboarded and issued a Cryptographic Stamp Identifier — a digital certificate that links the solution unit to a trusted third party able to confirm the identity of the taxpayer and uniquely identify that unit.
The mechanics:
- The solution generates a key pair and a PKCS#10 Certificate Signing Request containing at least the certificate CN and the public key.
- The CSR is signed with the private key as proof of possession.
- ZATCA's technical CA issues the certificate. Certificates are SHA-256 with ECDSA, key length P-256.
- Thereafter the solution authenticates to ZATCA's APIs using OAuth 2.0 Basic Authentication (RFC 6749), where the client ID is the digital certificate issued at onboarding.
That last point catches teams out. Your API credential is your certificate. There is no separate API key you can rotate independently, and a certificate problem presents as a 401, not as a certificate error.
The private key is the whole security model
ZATCA's requirement is unusually direct, and worth quoting in substance: the private key associated with the CSID should be generated by the solution so that it may not be viewed or copied during system initialisation. Export of the key would enable theft of the solution's identity, and must be blocked by the vendor using a software or hardware key vault.
Read that as a design constraint, not a recommendation. It rules out the pattern most teams reach for first — generate the key somewhere convenient, drop the PEM in config, ship it to every till. If your architecture has a step where a human could copy the key file, it does not meet the standard.
It also shapes multi-device deployments. Each solution unit has its own identity, so a chain of twenty branches is twenty onboardings, twenty key pairs, twenty certificates to renew — and twenty independent invoice chains. Plan the fleet management before the first device, because retrofitting key custody across a deployed estate is miserable.
The cryptographic stamp
For XML invoices the stamp is an XAdES digital signature, per ETSI EN 319 132-1:
- Enveloped — the signature is a sub-element of the document it signs.
- Signature level B-B.
- Digest algorithm SHA-256 throughout.
- Signature algorithm ECDSA, on P-256.
Two practical notes. Canonicalisation is part of the signed structure, and it is where subtle failures live: if your XML serialiser normalises whitespace, attribute order or namespace prefixes differently between signing and submission, the signature will not verify over the bytes ZATCA receives. Sign the canonical form and transmit exactly what you signed.
And because the signature is enveloped, the act of signing changes the document. Anything that touches the XML afterwards — a pretty-printer, an encoding conversion, a framework that re-serialises on the way out — invalidates it.
The QR code: nine tags, TLV, Base64
The QR code is Base64-encoded, up to 700 characters, and carries its fields in Tag-Length-Value form.
| Tag | Field |
|---|---|
| 1 | Seller's name |
| 2 | VAT registration number of the seller |
| 3 | Timestamp of the invoice, ISO 8601 (e.g. 2022-02-21T12:13:57Z) |
| 4 | Invoice total (with VAT) |
| 5 | VAT total (business term BT-110) |
| 6 | Hash of the XML invoice |
| 7 | ECDSA signature of the XML hash |
| 8 | ECDSA public key extracted from the signing private key |
| 9 | For simplified invoices and their notes: the ECDSA signature of the cryptographic stamp issued by ZATCA's technical CA |
Tags 1 and 2 predate Phase 2 — tag 1 enforced from 4 December 2021, tag 2 from 1 January 2023. Tags 6 to 9 are the Phase 2 additions, and tag 9 exists only for simplified invoices.
The encoding is precise and easy to get subtly wrong:
1. Start with an empty byte array.
2. For each value, write the tag in one byte, then the length as an unsigned 8-bit integer in one byte, then the value as UTF-8 bytes.
3. For tags 1–5 the length is the length of the UTF-8 byte array. For tag 6 the length is 32 bytes — the SHA-256 hash is raw bytes, not a hex string.
4. Base64-encode the assembled byte array.
5. Render the QR image from that Base64 string.
Three failure modes follow directly from that spec. Length is measured in bytes, not characters — an Arabic seller name will have more bytes than characters, and a length computed from string length produces a QR code that decodes as garbage after the first non-ASCII field. A length above 255 cannot be expressed in one byte, which is a real constraint on long seller names. And tag 6 holds 32 raw bytes, not the 64-character hex representation — encoding the hex string is a common and silent error, because the QR still scans.
A documented inconsistency worth knowing about. ZATCA's own materials have carried conflicting tag IDs for the ECDSA signature and public key across document versions, and the point has been raised on ZATCA's Fatoora developer community. Take the numbering from the version of the Security Features Implementation Standard you are implementing against, validate against the sandbox, and do not trust a blog post's table — including this one — over the current standard.
The previous invoice hash
The chaining value is specified briefly: the hash of the previous invoice is generated by applying the same transform as the cryptographic stamp and taking SHA-256.
"The same transform" is the operative phrase. The hash is not over your raw file — it is over the canonicalised form the signature machinery produces. Teams that hash the bytes they happen to have on disk produce a chain that fails validation for reasons that look unrelated to hashing.
Every document needs its previous-invoice-hash field populated — it appears in ZATCA's validation messages as KSA-13. The first document in a chain has no predecessor, so the standard defines a starting value; take it from the current standard and confirm it against the sandbox rather than from a code sample. What happens around chain boundaries — a new device, a restored database, a redeployment — is covered in the invoice counter and hash chain.
Do not build the B2B QR code
Worth repeating because it is expensive: for standard tax invoices, ZATCA generates the QR code string at clearance along with its stamp, and taxpayers are expected simply to visualise the QR code from the string returned. The cleared XML that comes back is the authoritative document — stamp, QR and all. Store it as received, render from it, and give the buyer that.
Your TLV encoder exists for simplified invoices. That is the whole of its job.
For how the two flows differ, see clearance versus reporting; for the validation rules the XML has to satisfy, the ZATCA XML and its validation errors.
Frequently asked questions
What is a CSID?
A Cryptographic Stamp Identifier — a digital certificate issued during onboarding that links an E-Invoice Solution unit to a trusted third party able to confirm the taxpayer's identity and uniquely identify that unit.
What signature format does ZATCA use?
XAdES, enveloped, level B-B, per ETSI EN 319 132-1, with SHA-256 digests and ECDSA on P-256.
How do I authenticate to the API?
OAuth 2.0 Basic Authentication as in RFC 6749, with the onboarding certificate as the client ID.
How many characters can the QR code hold?
It is Base64-encoded with up to 700 characters.
Why does my QR code decode incorrectly after the seller name?
Almost always because the TLV length was computed from character count rather than UTF-8 byte count. Arabic text makes this visible immediately.
Should tag 6 contain the hex hash?
No. It is the raw SHA-256 value, 32 bytes.
Do I need a separate certificate per till or branch?
Each solution unit is onboarded and identified separately, so yes — and each maintains its own chain. Plan key custody and renewal as fleet operations.
Can I store the private key in application config?
No. The standard requires the key to be generated so it cannot be viewed or copied, with export blocked by a software or hardware key vault.
Sources
- ZATCA — Electronic Invoice Security Features Implementation Standard, v1.2 (PDF) — CSR profile, XAdES level, TLV table, previous invoice hash
- ZATCA — Guide to Developing a FATOORA Compliant QR Code (PDF)
- ZATCA — Detailed Guidelines for E-Invoicing (PDF) — key export prevention, B2B QR generation
- Fatoora Developer Community — conflicting QR code tag IDs
- ETSI EN 319 132-1 — XAdES digital signatures