Country-compliant tax invoices: UAE FTA vs Saudi ZATCA requirements
If you're shipping a SaaS or e-commerce platform in the Gulf, "just generate a PDF invoice" is not enough. The UAE's Federal Tax Authority (FTA) and Saudi Arabia's Zakat, Tax and Customs Authority (ZATCA) both have specific rules about what a tax invoice must contain, how it must look, and — in Saudi's case — what machine-readable data must be embedded. Get it wrong and your customer's invoices get rejected during a tax audit.
This post compares the two regimes from a developer's perspective and shows how to structure your invoice generation so a single codebase can serve both.
The high-level difference
UAE FTA is essentially a content compliance regime: the invoice is a human-readable document, and as long as the required fields are present in Arabic and/or English, you're fine. PDFs are accepted.
ZATCA (Saudi) is a structured e-invoicing regime under the Fatoora system. Invoices must be generated in a specific XML format (UBL 2.1), include a QR code with TLV-encoded data, and — for Phase 2 (integration phase) — be cryptographically signed and cleared/reported through the ZATCA platform before reaching the buyer.
So: UAE = compliant PDF. KSA = compliant XML + a PDF/A-3 with the XML embedded, plus a QR code.
Required fields: UAE FTA
For a standard tax invoice (supplies above AED 10,000 or B2B), FTA requires:
- The words "Tax Invoice" clearly displayed
- Supplier name, address, and TRN (Tax Registration Number)
- Buyer name, address, and TRN (if registered)
- Sequential invoice number
- Date of issue and date of supply (if different)
- Description of goods/services
- Unit price, quantity, discount, taxable amount
- VAT rate and VAT amount per line (in AED)
- Total amount payable in AED
- If reverse charge applies, a statement saying so
Simplified invoices (B2C, below AED 10,000) drop the buyer details and per-line VAT breakdown.
A minimal data shape:
tstype UAEInvoice = { type: "tax" | "simplified"; invoiceNumber: string; issueDate: string; // ISO supplyDate?: string; supplier: { name: string; address: string; trn: string }; buyer?: { name: string; address: string; trn?: string }; lines: Array<{ description: string; quantity: number; unitPrice: number; // AED vatRate: number; // typically 0.05 discount?: number; }>; currency: "AED"; reverseCharge?: boolean; };
Required fields: ZATCA (Saudi Arabia)
ZATCA distinguishes between Standard Tax Invoices (B2B/B2G) and Simplified Tax Invoices (B2C). Both phases require:
- Seller name, VAT number, and address
- Invoice issue date and time (timestamp matters)
- Invoice total with and without VAT
- VAT amount
- A QR code on the visible PDF (mandatory for simplified, required for standard in Phase 2)
The QR code is TLV-encoded (Tag-Length-Value) Base64 with five mandatory fields for Phase 1:
txtTag 1: Seller name Tag 2: VAT registration number Tag 3: Timestamp (ISO 8601) Tag 4: Invoice total (with VAT) Tag 5: VAT total
Phase 2 adds three more tags (hash of the XML invoice, ECDSA signature, public key).
Here's a minimal TLV encoder:
tsfunction tlv(tag: number, value: string): Buffer { const v = Buffer.from(value, "utf8"); return Buffer.concat([Buffer.from([tag, v.length]), v]); } function zatcaQR(inv: { sellerName: string; vatNumber: string; timestamp: string; total: string; vat: string; }): string { const buf = Buffer.concat([ tlv(1, inv.sellerName), tlv(2, inv.vatNumber), tlv(3, inv.timestamp), tlv(4, inv.total), tlv(5, inv.vat), ]); return buf.toString("base64"); }
That Base64 string goes into a QR code on the invoice PDF.
Phase 2: the XML and signing
For Phase 2 (rolled out in waves since 2023), each invoice must be:
- Generated as UBL 2.1 XML
- Hashed (SHA-256)
- Signed with an ECDSA certificate issued by ZATCA
- For B2B: cleared through the ZATCA API before sending to the buyer (you get back a signed XML)
- For B2C: reported to ZATCA within 24 hours
- The PDF given to the buyer must be PDF/A-3 with the signed XML embedded as an attachment
A typical clearance call:
httpPOST https://gw-fatoora.zatca.gov.sa/e-invoicing/core/invoices/clearance/single Content-Type: application/json Accept-Language: en Clearance-Status: 1 Authorization: Basic <base64(binarySecurityToken:secret)> { "invoiceHash": "...", "uuid": "...", "invoice": "<base64-encoded UBL XML>" }