Skip to content

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

  1. Register at ZATCA Fatoora Portal
  2. Generate a one-time password (OTP) from the portal
  3. 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:

  1. CSR Generation — Invora generates a Certificate Signing Request with your organization's details
  2. Compliance CSID — ZATCA issues a compliance certificate; Invora submits a test invoice for validation
  3. 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