What Is ZATCA E-Invoicing?
Saudi Arabia’s Zakat, Tax and Customs Authority (ZATCA) mandates electronic invoicing for all VAT-registered businesses. The rollout has two phases:
Phase 1 — Generation (Dec 2021): Invoices must be generated and stored electronically in a structured format. No more handwritten or image-based invoices. A QR code is required on simplified (B2C) invoices.
Phase 2 — Integration (Jan 2023+): Invoices must be reported to (standard/B2B) or cleared by (simplified/B2C) ZATCA’s Fatoora platform in near real-time via API. Cryptographic stamping and XML submission become mandatory in waves.
If you’re building invoicing software for the Saudi market, you need to handle both phases.
Official reference: ZATCA E-Invoicing Developer Portal
Technical Requirements
Every ZATCA-compliant invoice must satisfy:
| Requirement | Detail |
|---|---|
| Format | UBL 2.1 XML (Universal Business Language) |
| UUID | Each invoice needs a unique UUID v4 |
| Sequential numbering | Invoice Counter Value (ICV), monotonically increasing |
| Previous invoice hash | SHA-256 hash chain linking each invoice to the previous one |
| QR Code | TLV-encoded, Base64 string (Phase 1: 5 fields, Phase 2: 8 fields) |
| Digital signature | ECDSA with secp256k1 curve, X.509 certificate from ZATCA |
| Cryptographic stamp | ZATCA returns a signed stamp on clearance |
QR Code Structure (TLV Encoding)
The QR code is a Base64-encoded byte array using Tag-Length-Value format:
| Tag | Field | Example |
|---|---|---|
| 1 | Seller name | "AzizWares" |
| 2 | VAT registration number | "300000000000003" |
| 3 | Timestamp (ISO 8601) | "2026-02-27T12:00:00Z" |
| 4 | Invoice total (with VAT) | "115.00" |
| 5 | VAT amount | "15.00" |
| 6 | XML hash (SHA-256) | (hex bytes) |
| 7 | ECDSA signature | (DER bytes) |
| 8 | Public key | (DER bytes) |
Tags 1–5 are required for Phase 1. Tags 6–8 are added in Phase 2.
Code Example: TLV Encoding in Python
import base64
from datetime import datetime
def tlv_encode(tag: int, value: str | bytes) -> bytes:
if isinstance(value, str):
value = value.encode('utf-8')
# Note: single-byte length works for tags 1-5 (all < 256 bytes).
# For Phase 2 tags (signature/key), use proper ASN.1-style length encoding.
if len(value) > 127:
length_bytes = len(value).to_bytes(2, 'big')
return bytes([tag, 0x82]) + length_bytes + value
return bytes([tag, len(value)]) + value
def generate_qr_base64(
seller: str,
vat_number: str,
timestamp: datetime,
total: str,
vat_amount: str,
) -> str:
tlv = b''
tlv += tlv_encode(1, seller)
tlv += tlv_encode(2, vat_number)
tlv += tlv_encode(3, timestamp.isoformat() + 'Z')
tlv += tlv_encode(4, total)
tlv += tlv_encode(5, vat_amount)
return base64.b64encode(tlv).decode('ascii')
qr = generate_qr_base64(
seller="AzizWares",
vat_number="300000000000003",
timestamp=datetime(2026, 2, 27, 12, 0, 0),
total="115.00",
vat_amount="15.00",
)
print(qr)
Dart Equivalent (for Flutter Apps)
import 'dart:convert';
import 'dart:typed_data';
Uint8List tlvEncode(int tag, String value) {
final bytes = utf8.encode(value);
return Uint8List.fromList([tag, bytes.length, ...bytes]);
}
String generateZatcaQr({
required String seller,
required String vatNumber,
required String timestamp,
required String total,
required String vatAmount,
}) {
final tlv = BytesBuilder();
tlv.add(tlvEncode(1, seller));
tlv.add(tlvEncode(2, vatNumber));
tlv.add(tlvEncode(3, timestamp));
tlv.add(tlvEncode(4, total));
tlv.add(tlvEncode(5, vatAmount));
return base64Encode(tlv.toBytes());
}
Common Developer Pitfalls
1. Timestamp format: ZATCA requires ISO 8601 with timezone. Using 2026-02-27 12:00:00 instead of 2026-02-27T12:00:00Z will cause rejection in Phase 2.
2. Arabic text in TLV: The seller name must be UTF-8 encoded. If your seller name is "شركة عزيز", the byte length in the TLV L field must reflect the UTF-8 byte count (which is longer than the character count). Off-by-one here corrupts the entire QR payload.
3. Sandbox vs production: ZATCA’s sandbox (gw-fatoora.zatca.gov.sa) uses different compliance certificates than production. Your sandbox CSR won’t work in production — you must onboard with a real Hardware Security Module (HSM) or ZATCA-approved solution.
4. Hash chain breaks: Each invoice’s hash depends on the previous invoice’s hash. If you regenerate or skip an invoice, the chain breaks and the entire batch fails validation.
5. Certificate renewal: ZATCA compliance certificates expire. Build renewal logic into your system — don’t discover this in production.
How Mufawtir Handles This
Mufawtir is AzizWares’ invoicing app built specifically for ZATCA compliance. It handles TLV-encoded QR generation, sequential numbering, and ZATCA-compliant invoice formatting out of the box — so you don’t have to build the plumbing yourself. If you need a ready-made solution instead of building from scratch, check it out.
FAQ
Q: Do I need Phase 2 compliance right now? A: ZATCA is rolling out Phase 2 in waves based on revenue thresholds. Check ZATCA’s official wave schedule — but building for Phase 2 from the start saves you a painful migration.
Q: Can I use JSON instead of XML? A: No. ZATCA mandates UBL 2.1 XML. The Fatoora API only accepts XML payloads. You can use JSON internally, but the submission must be XML.
Q: What QR code library should I use?
A: Any library that generates standard QR codes works — qrcode in Python, qr_flutter in Dart. The ZATCA-specific part is the payload (TLV-encoded Base64 string), not the QR rendering itself.
Q: Is the ZATCA sandbox reliable for testing? A: It’s functional but can be slow and occasionally down. Always build retry logic and don’t rely on it for CI/CD pipelines. Cache valid responses for local development.
Q: Do freelancers need ZATCA e-invoicing? A: If you’re VAT-registered in Saudi Arabia (revenue above SAR 375,000), yes. The threshold applies regardless of business size.