Country-compliant tax invoices: UAE FTA vs Saudi ZATCA requirements
If you're shipping a billing system across the Gulf, you'll quickly discover that "just generate a PDF invoice" is a much bigger problem than it sounds. The UAE and Saudi Arabia both enforce VAT-compliant invoices, but the rules differ in important ways — field requirements, QR code formats, Arabic content, and (in KSA) full e-invoicing integration with the tax authority.
This post walks through what backend developers actually need to implement for each.
The shared baseline: VAT invoices
Both countries enforce a 5% (UAE) / 15% (KSA) VAT regime and require invoices in a structured format. At minimum, every tax invoice in both jurisdictions must include:
- The phrase "Tax Invoice" (in English and/or Arabic)
- Supplier name, address, and VAT registration number (TRN in UAE, VAT number in KSA)
- Buyer name and address (and VAT number if registered)
- Sequential invoice number
- Issue date (and supply date if different)
- Line items with description, quantity, unit price
- Taxable amount, VAT rate, VAT amount, total
That's the easy part. The divergence starts with formatting and submission.
UAE FTA requirements
The UAE Federal Tax Authority (FTA) currently requires a "tax invoice" for B2B and a "simplified tax invoice" for B2C under AED 10,000. Key developer-relevant points:
- Currency: Amounts must be shown in AED. If you bill in another currency, you must include the AED equivalent and the exchange rate used.
- Language: Arabic is required by law, though in practice the FTA accepts English invoices. Safe move: include both.
- TRN format: 15 digits, validated against the FTA portal.
- Rounding: To the nearest fils (2 decimal places).
The UAE is rolling out a Peppol-based e-invoicing mandate (phased through 2026), but for now most businesses still issue PDF tax invoices. A typical generation flow looks like this:
tsimport { kamy } from "@kamydev/sdk"; const pdf = await kamy.documents.render({ template: "uae-tax-invoice", data: { supplier: { name: "Acme FZ-LLC", trn: "100123456700003", address: "Dubai Internet City, Dubai, UAE", }, buyer: { name: "Client Co", trn: "100987654300003", }, invoiceNumber: "INV-2025-0142", issueDate: "2025-02-14", currency: "AED", lineItems: [ { description: "API subscription", qty: 1, unitPrice: 500.00, vatRate: 0.05 }, ], subtotal: 500.00, vatAmount: 25.00, total: 525.00, }, });
Saudi ZATCA requirements
ZATCA (Zakat, Tax and Customs Authority) is stricter. Saudi Arabia mandates e-invoicing in two phases:
- Phase 1 (Generation) — live since Dec 2021. All invoices must be generated electronically in a structured format, include a TLV-encoded QR code, and be in Arabic (English optional, alongside Arabic).
- Phase 2 (Integration) — rolling out in waves. Invoices must be submitted as XML (UBL 2.1) to ZATCA's Fatoora platform, cryptographically signed, and cleared (B2B) or reported within 24 hours (B2C).
For Phase 1 simplified invoices (B2C), the QR code is mandatory and must encode the following fields using TLV (Tag-Length-Value) encoding, then Base64:
- Seller name
- VAT registration number
- Timestamp (ISO 8601)
- Invoice total (with VAT)
- VAT total
Here's a minimal TLV encoder:
tsfunction tlv(tag: number, value: string): Buffer { const valueBuf = Buffer.from(value, "utf8"); return Buffer.concat([ Buffer.from([tag, valueBuf.length]), valueBuf, ]); } function buildZatcaQr(data: { sellerName: string; vatNumber: string; timestamp: string; invoiceTotal: string; vatTotal: string; }): string { const buf = Buffer.concat([ tlv(1, data.sellerName), tlv(2, data.vatNumber), tlv(3, data.timestamp), tlv(4, data.invoiceTotal), tlv(5, data.vatTotal), ]); return buf.toString("base64"); }
That Base64 string is then rendered as a QR code on the invoice PDF. Phase 2 adds tags 6–8 for the invoice hash, ECDSA signature, and public key — these come from ZATCA's compliance API after onboarding.
Generating both from one system
The pragmatic approach: keep one canonical invoice schema and branch on the issuing country to pick the right template, language, and QR logic.
tsasync function renderInvoice(invoice: Invoice) { const isKSA = invoice.country === "SA"; const qrCode = isKSA ? buildZatcaQr({ sellerName: invoice.supplier.name, vatNumber: invoice.supplier.vatNumber, timestamp: invoice.issueDate, invoiceTotal: invoice.total.toFixed(2),