Overview
ZATCA Phase 2 — also called the Integration Phase or Fatoora — moves Saudi e-invoicing from offline-generated PDFs into a real-time, cryptographically-signed, government-cleared workflow. Every standard tax invoice (B2B) must be cleared by ZATCA before it is delivered to the buyer; every simplified invoice (B2C) must be reported within 24 hours. The technical heart is UBL 2.1 XML, XAdES B-B signatures, and the Fatoora API. Failure to clear means the invoice has no legal value.
Who this applies to
- Every VAT-registered taxpayer in KSA.
- Roll-in is in waves: largest taxpayers first, smaller ones in subsequent waves, with a four-month notice per wave.
- Exemptions: non-resident VAT-payers and a narrow list of exempt sectors.
Architecture overview
[ERP] → invoice generator → UBL XML → XAdES sign with CSID → POST /clearance
↓
[ZATCA Fatoora]
↓
signed XML returned
+ QR + cleared-marker
↓
PDF/A-3 to buyer
XML archived 6 years
Step 1: UBL 2.1 invoice structure
ZATCA mandates a constrained UBL 2.1 profile (PINT-KSA). Minimum essential elements:
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ProfileID>reporting:1.0</cbc:ProfileID>
<cbc:ID>INV-2026-00187</cbc:ID>
<cbc:UUID>3cf5ee18-ee25-44ea-a444-2c37ba7f28be</cbc:UUID>
<cbc:IssueDate>2026-05-27</cbc:IssueDate>
<cbc:IssueTime>14:25:43</cbc:IssueTime>
<cbc:InvoiceTypeCode name="0100000">388</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>SAR</cbc:DocumentCurrencyCode>
<cbc:TaxCurrencyCode>SAR</cbc:TaxCurrencyCode>
<cac:AdditionalDocumentReference>
<cbc:ID>ICV</cbc:ID>
<cbc:UUID>187</cbc:UUID>
</cac:AdditionalDocumentReference>
<cac:AdditionalDocumentReference>
<cbc:ID>PIH</cbc:ID>
<cac:Attachment>
<cbc:EmbeddedDocumentBinaryObject mimeCode="text/plain">
NWZl...prev-invoice-hash
</cbc:EmbeddedDocumentBinaryObject>
</cac:Attachment>
</cac:AdditionalDocumentReference>
<!-- Seller, buyer, lines, tax breakdown ... -->
</Invoice>
Two values are critical and easy to get wrong:
- ICV (Invoice Counter Value) — monotonically increasing per CSID. Reset only by ZATCA.
- PIH (Previous Invoice Hash) — base64 of SHA-256 of the previous invoice's signed XML. Without this, the chain breaks.
Step 2: XAdES B-B signing
The signature is XAdES Basic with attached signature properties:
<ext:UBLExtensions>
<ext:UBLExtension>
<ext:ExtensionURI>urn:oasis:names:specification:ubl:dsig:enveloped:xades</ext:ExtensionURI>
<ext:ExtensionContent>
<sig:UBLDocumentSignatures>
<sac:SignatureInformation>
<ds:Signature Id="signature">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2006/12/xml-c14n11"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
<!-- references, transforms ... -->
</ds:SignedInfo>
<ds:SignatureValue>BASE64...</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>BASE64 CSID...</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
</sac:SignatureInformation>
</sig:UBLDocumentSignatures>
</ext:ExtensionContent>
</ext:UBLExtension>
</ext:UBLExtensions>
Use ECDSA on secp256k1 with SHA-256 hash. Sign the canonicalised XML, then embed the signature back.
Step 3: Fatoora API endpoints
Sandbox: https://gw-fatoora.zatca.gov.sa/e-invoicing/developer-portal/
Production: https://gw-fatoora.zatca.gov.sa/e-invoicing/core/
POST /invoices/clearance/single ← standard B2B
POST /invoices/reporting/single ← simplified B2C
POST /compliance/csids ← CSID onboarding
GET /compliance/csids/{id} ← CSID status
POST /compliance/csids/renew ← CSID renewal
Sample clearance request:
curl -X POST 'https://gw-fatoora.zatca.gov.sa/e-invoicing/core/invoices/clearance/single' \
-H 'Accept: application/json' \
-H 'Accept-Language: en' \
-H 'Accept-Version: V2' \
-H 'Clearance-Status: 1' \
-H 'Authorization: Basic <BASE64(csid:csid_secret)>' \
-H 'Content-Type: application/json' \
--data '{
"invoiceHash": "BASE64SHA256",
"uuid": "3cf5ee18-ee25-44ea-a444-2c37ba7f28be",
"invoice": "BASE64_UBL_XML"
}'
Step 4: QR code
For both B2B and B2C the QR (TLV-encoded base64) carries:
- Seller name.
- VAT registration number.
- Timestamp.
- Invoice total (with VAT).
- VAT total.
- Hash of invoice XML.
- ECDSA signature.
- Public key.
- Certificate signature.
For B2B the TLV includes the cryptographic stamp from ZATCA's clearance response.
Step 5: Common error codes
| Code | Meaning | Fix | |---|---|---| | BR-KSA-15 | ICV not monotonic | Persist ICV per CSID per device, single writer | | BR-KSA-26 | PIH does not match previous hash | Persist PIH after every successful submission | | BR-S-08 | Tax category mismatch | Check VAT category mapping in your product master | | ZATCA-INV-008 | XML schema validation failed | Run against the published XSD before sending | | ZATCA-CRT-005 | Signature invalid | Re-sign after canonicalisation, not before |
Common gotchas
- Generating XML, signing, then re-serialising — the re-serialise changes whitespace and breaks the signature. Sign last.
- ICV not persisted across restarts — gaps and duplicates appear.
- Timezone confusion — ZATCA expects Saudi time but ISO 8601 with offset; missing offset fails validation.
- Buyer VAT number empty for a "registered customer" — auto-rejection.
Verification
- Schema validation against the published XSD.
- ICV chain integrity check across the last 1000 invoices.
- Signature verification against your own CSID.
- End-to-end test in sandbox with 30 different scenarios (standard, debit note, credit note, etc.).
- Storage of cleared invoices for 6 years minimum.
Conclusion
ZATCA Phase 2 is not an export feature; it is a real-time integration with the tax authority. Build the integration with idempotency, persistence, and observability — your finance team will thank you, and so will your auditor.
Comments
0 total · 0 threads