ZATCA E-Invoicing Integration Guide¶
Saudi Arabia's Zakat, Tax and Customs Authority (ZATCA) requires all B2B and B2C invoices to be electronically reported via the Fatoora platform.
Invora handles the full ZATCA lifecycle: CSR generation, compliance verification, production onboarding, invoice signing, QR code embedding, and clearance/reporting submission.
Environments¶
| Environment | Purpose | Invoice Validation |
|---|---|---|
PHASE1 |
Phase 1 (generation only) | Local XML generation, no ZATCA submission |
PHASE2_SANDBOX |
Development testing | Simulated ZATCA responses, no real validation |
PHASE2_SIMULATION |
Pre-production testing | Real ZATCA validation, no legal effect |
PHASE2_PRODUCTION |
Live compliance | Legally binding, real clearance/reporting |
Onboarding Flow¶
Prerequisites¶
- Register at ZATCA Fatoora Portal
- Generate a one-time password (OTP) from the portal
- Have your organization's tax details configured via
SettingsService.UpdateSelfParty()
Step 1: Initiate Onboarding¶
grpcurl -d '{
"otp": "123456",
"environment": "ZATCA_ENVIRONMENT_PHASE2_SIMULATION"
}' gateway.invora.app:443 invora.regulations.zatca.v1.ZatcaRegulationService/InitiateOnboarding
This returns a server-sent stream of progress events. Each event represents a completed (or failed) onboarding step:
{"step": "ZATCA_ONBOARDING_STEP_CSR_GENERATION", "disposition": "ZATCA_DISPOSITION_ISSUED", "requestId": "req-abc-123", "message": "CSR generated successfully", "isFinal": false}
{"step": "ZATCA_ONBOARDING_STEP_COMPLIANCE_CSID", "disposition": "ZATCA_DISPOSITION_ISSUED", "requestId": "req-def-456", "message": "Compliance CSID issued", "isFinal": false}
{"step": "ZATCA_ONBOARDING_STEP_PRODUCTION_CSID", "disposition": "ZATCA_DISPOSITION_ISSUED", "requestId": "req-ghi-789", "message": "Production CSID issued", "isFinal": true}
Three steps execute sequentially:
- CSR Generation — Invora generates a Certificate Signing Request with your organization's details
- Compliance CSID — ZATCA issues a compliance certificate; Invora submits a test invoice for validation
- Production CSID — ZATCA issues the production certificate for live invoicing
If any step fails, the stream emits an event with disposition: ZATCA_DISPOSITION_REJECTED or ZATCA_DISPOSITION_ERROR plus structured errors:
{
"step": "ZATCA_ONBOARDING_STEP_COMPLIANCE_CSID",
"disposition": "ZATCA_DISPOSITION_REJECTED",
"requestId": "req-def-456",
"message": "Compliance check failed",
"errors": [
{"code": "COMPLIANCE_CHECK_FAILED", "category": "COMPLIANCE", "message": "Tax ID format invalid for SA"},
{"code": "MISSING_FIELD", "category": "COMPLIANCE", "message": "Seller address city is required"}
],
"isFinal": true
}
Step 2: Check Progress (if stream closed early)¶
If the client disconnected during onboarding, poll progress:
grpcurl gateway.invora.app:443 invora.regulations.zatca.v1.ZatcaRegulationService/GetOnboardingProgress
Returns:
{
"currentStep": "ZATCA_ONBOARDING_STEP_PRODUCTION_CSID",
"lastDisposition": "ZATCA_DISPOSITION_ISSUED",
"completed": true,
"history": [/* all progress events */]
}
Step 3: Verify Integration Status¶
grpcurl gateway.invora.app:443 invora.regulations.zatca.v1.ZatcaRegulationService/GetIntegrationStatus
Returns:
{
"onboarded": true,
"environment": "ZATCA_ENVIRONMENT_PHASE2_SIMULATION",
"details": {
"complianceCsidExpiry": "2027-01-15T00:00:00Z",
"productionCsidExpiry": "2027-01-15T00:00:00Z",
"isCompliant": true,
"totalSubmitted": 0,
"totalAccepted": 0,
"totalRejected": 0
}
}
Submitting Invoices¶
Once onboarded, invoices are automatically submitted to ZATCA when frozen.
Create and Freeze¶
# Create a draft invoice
grpcurl -d '{
"changes": {
"content": {
"invoice": {
"invoiceLine": [...]
}
}
},
"freezeImmediately": true
}' gateway.invora.app:443 invora.documents.v2.DocumentsService/Create
When freezeImmediately: true, the regulation pipeline runs during the Create call:
1. Invoice is validated against ZATCA rules
2. XML is canonicalized and signed with the CSID certificate
3. QR code is generated with TLV-encoded data
4. Invoice is submitted to ZATCA for clearance (B2B) or reporting (B2C)
5. Response includes regulation_metadata with ZATCA's acceptance/rejection
Check Submission Status¶
In the response, regulation_metadata.entries["zatca"] contains:
{
"status": "REGULATION_SUBMISSION_STATUS_ACCEPTED",
"artifactHash": "sha256:abc123...",
"artifactContentType": "application/xml",
"zatca": {
"status": "ZATCA_SUBMISSION_STATUS_CLEARED",
"clearanceId": "CLR-2026-001",
"invoiceHash": "sha256:def456...",
"zatcaReferenceId": "REF-789",
"submittedAt": "2026-04-29T10:30:00Z"
}
}
List Regulation Status for a Document¶
grpcurl -d '{"documentKey": "INV-2026-001"}' \
gateway.invora.app:443 invora.regulations.v2.RegulationInfoService/ListDocumentRegulations
Returns lightweight summaries without artifact bytes — use for dashboards and list views.
Download Signed XML¶
grpcurl -d '{"regulationId": "zatca", "documentKey": "INV-2026-001"}' \
gateway.invora.app:443 invora.regulations.v2.RegulationInfoService/GetArtifact
Returns the ZATCA-signed XML and its hash for archival.
QR Code Decoding¶
Decode QR codes from printed or received invoices:
grpcurl -d '{"input": ["AXZWYW..."]}' \
gateway.invora.app:443 invora.regulations.zatca.v1.ZatcaRegulationService/ExplainQrCode
Returns structured TLV data: seller name, VAT number, timestamp, totals, signature, and hash.
ZATCA Submission Models¶
| Invoice Type | Model | Behavior |
|---|---|---|
| B2B (tax invoice) | Clearance | Blocking — ZATCA must clear before invoice is valid |
| B2C (simplified invoice) | Reporting | Async — ZATCA is notified within 24 hours |
Both models are handled automatically by Invora based on the invoice type code in the UBL document. No API-side configuration needed.
Retry Failed Submissions¶
If ZATCA rejects or times out:
grpcurl -d '{"regulationId": "zatca", "documentKey": "INV-2026-001"}' \
gateway.invora.app:443 invora.regulations.v2.RegulationInfoService/RetrySubmission
For manual overrides (out-of-band corrections):
grpcurl -d '{
"regulationId": "zatca",
"documentKey": "INV-2026-001",
"newStatus": "REGULATION_SUBMISSION_STATUS_ACCEPTED",
"reason": "Manually cleared via ZATCA portal ticket #12345"
}' gateway.invora.app:443 invora.regulations.v2.RegulationInfoService/OverrideSubmissionStatus
Common Issues¶
| Symptom | Cause | Fix |
|---|---|---|
| OTP expired | OTP valid for 1 hour | Generate new OTP from ZATCA portal |
| Compliance CSID rejected | Seller details don't match ZATCA records | Update SelfParty with correct legal name, tax ID, address |
| Invoice clearance rejected | Calculation errors or missing fields | Check errors in regulation_metadata, fix document, retry |
| QR code invalid | Document was modified after signing | Frozen documents are immutable — this indicates corruption |