aghfatehi / laravel-zatca
Laravel ZATCA (Fatoora) package for Saudi e-invoicing Phase 1 & Phase 2 compliance. دمج الفاتورة الإلكترونية السعودية مع لارافيل - المرحلة الأولى والثانية لهيئة الزكاة والضريبة والجمارك
Requires
- php: ^8.1|^8.2|^8.3|^8.4
- ext-curl: *
- ext-dom: *
- ext-json: *
- ext-mbstring: *
- ext-openssl: *
- illuminate/console: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^9.0|^10.0|^11.0
Suggests
- endroid/qr-code: Alternative QR library (also ZATCA-compatible). Install with: composer require endroid/qr-code
- simplesoftwareio/simple-qrcode: REQUIRED for ZATCA-compatible QR codes. Install with: composer require simplesoftwareio/simple-qrcode
README
Laravel ZATCA (Fatoora) Package
Saudi Arabian e-Invoicing Compliance — Phase 1 & Phase 2
المرحلة الأولى والثانية للفاتورة الإلكترونية السعودية لهيئة الزكاة والضريبة والجمارك
By AL-AGHBARI Fatehi — فتحي الأغبري
ZATCA integration for Laravel — QR code generation, TLV encoding, invoice signing, clearance & reporting. دمج الفاتورة الإلكترونية مع لارافيل: المرحلة الأولى (QR) والمرحلة الثانية (التوقيع والإرسال) لهيئة الزكاة والضريبة والجمارك السعودية
Table of Contents
- Overview
- Features
- Version Matrix
- Installation
- Configuration
- Integration Scenarios
- Phase 1 -- QR Code Generation
- Phase 2 -- FATOORA API Integration
- QR Code Display on PDF / View
- API Routes
- Offline Mode & Queue Sync
- Events
- Artisan Commands
- Testing
- Security & Logging
- Project Map
- Support
Overview
laravel-zatca is a production-grade Laravel package for integrating with the ZATCA (Zakat, Tax and Customs Authority) e-invoicing system — also known as Fatoora — in the Kingdom of Saudi Arabia.
The package covers both phases of the ZATCA e-invoicing mandate:
| Phase | Description | Status |
|---|---|---|
| Phase 1 | Generate and display QR code on invoices (TLV Base64 format) | Production Ready |
| Phase 2 | Full compliance: CSR, Certificate, Signing, Clearance & Reporting via FATOORA API | Production Ready |
Flexible Integration
You can use this package in any of these modes:
- Phase 1 only — Just generate QR codes for display on PDF/View (no API calls)
- Phase 2 only — Full API integration (requires pre-existing Phase 1 QR or external QR generation)
- Both phases — Full lifecycle from QR → Signing → Submission
- Offline → Online — Generate QR codes offline, sync invoices via queue when online
What is Required vs Optional
For Phase 1 (QR generation only):
| Step | Required? |
|---|---|
Install package (composer require) |
Required |
Set ZATCA_PHASE and ZATCA_VAT_* in .env |
Required |
Call Zatca::phase1()->generateQrCodeText() in your controller |
Required |
| Display QR in your Blade view | Required |
| Publish config / views | Optional |
Install simplesoftwareio/simple-qrcode or endroid/qr-code for ZATCA-compatible QR |
Optional but recommended |
| Use Model Trait for automatic QR generation | Optional |
API Routes (/zatca/onboard, etc.) |
Optional — not needed |
| Offline Mode & Queue Sync | Optional — not needed |
| Events & Logging | Optional — not needed |
For Phase 2 (API integration):
| Step | Required? |
|---|---|
| Everything from Phase 1 | Required (if using both) |
Set ZATCA_PHASE=phase_2 or =both |
Required |
| Complete onboarding (keys + CSR + certificate) | Required |
Set ZATCA_CERTIFICATE, ZATCA_PRIVATE_KEY, ZATCA_SECRET |
Required |
Call Zatca::phase2()->signInvoice() + ->submitInvoice() |
Required |
| Publish migrations for audit logging | Optional |
| Use Queue for async sync | Optional |
API Routes (/zatca/onboard) |
Optional — alternative to CLI |
| Events & custom listeners | Optional |
Features
- Phase 1: TLV Base64 QR code (5 tags: Seller, VAT, Date, Total, Tax)
- Phase 2: UBL 2.1 XML invoice building & XAdES signing
- Phase 2: ECDSA secp256k1 key pair generation (OpenSSL)
- Phase 2: CSR generation for ZATCA compliance certificate
- Phase 2: Compliance check (Sandbox)
- Phase 2: Clearance & Reporting (Production)
- cURL-based HTTP client (no Guzzle dependency)
- Queue support for async invoice sync with retry logic
- Offline mode -- Generate signed XML locally, sync later
- Artisan commands for onboarding & syncing
- Event-driven architecture (InvoiceCleared, InvoiceReported, InvoiceFailed)
- PSR-4 autoloading, Service Provider auto-discovery
- Logging with PII masking, non-blocking design
- No UI/frontend assumptions -- Bring your own views
- Configurable phases via single
.envvariable
External References
This package implements technical specifications for e-invoicing. Below are links to the relevant standards and portals for your own compliance verification.
| Resource | Link |
|---|---|
| ZATCA Developer Portal (Sandbox) | https://sandbox.zatca.gov.sa |
| ZATCA Production Portal | https://zatca.gov.sa |
| E-invoicing regulations (Saudi Arabia) | https://zatca.gov.sa |
This package is built by implementing publicly available technical specifications. For official compliance requirements, always refer to ZATCA's documentation and consult with legal advisors.
Version Matrix
| Component | Version |
|---|---|
| PHP | ^8.1, ^8.2, ^8.3, ^8.4 |
| Laravel | ^9.0, ^10.0, ^11.0, ^12.0, ^13.0 |
| ZATCA API | V2 (2024+) |
| UBL Standard | 2.1 |
| Signature Algorithm | ECDSA secp256k1 + SHA-256 |
| XAdES | EPES v1.3.2 |
| QR Encoding | TLV Base64 (GS1-compatible) |
| OpenSSL | Required (for key & CSR generation) |
| cURL | Required extension |
Optional QR Dependencies
The built-in QR generator (SvgQrGenerator) produces visual-only output that is not compatible with the official ZATCA (Fatoora) app. For production use, you must install one of these:
| Package | Purpose | Install |
|---|---|---|
simplesoftwareio/simple-qrcode |
✅ ZATCA-compatible SVG QR | composer require simplesoftwareio/simple-qrcode |
endroid/qr-code |
✅ ZATCA-compatible QR (SVG/PNG) | composer require endroid/qr-code |
If one of these is installed, the Blade view uses it automatically. If neither is installed, the built-in fallback produces a QR image that will not be readable by the ZATCA app.
Installation
composer require aghfatehi/laravel-zatca
That's it for Phase 1 — TLV generation works immediately. For QR rendering, you must install simplesoftwareio/simple-qrcode or endroid/qr-code (see Optional QR Dependencies). For Phase 2 you also need OpenSSL installed on your server and a ZATCA developer account.
Publish Configuration
php artisan vendor:publish --tag=zatca-config
Publish Migrations (Optional — for Phase 2 API & audit logging)
Publish and run the migrations only if you are using the optional Phase 2 API routes (onboarding, invoice clearance/reporting):
php artisan vendor:publish --tag=zatca-migrations php artisan migrate --path=/database/migrations/2024_01_01_000001_create_zatca_certificates_table.php php artisan migrate --path=/database/migrations/2024_01_01_000002_create_zatca_invoice_logs_table.php
This creates two tables:
| Table | Purpose |
|---|---|
zatca_certificates |
Stores EGS certificate and private key after ZATCA onboarding (used for invoice signing) |
zatca_invoice_logs |
Logs every invoice submission request/response with invoice_serial_number for clearance & reporting audit trail |
You do not need these migrations if you only use Phase 1 (QR code generation).
Publish Views (Optional — to customize QR fallback)
php artisan vendor:publish --tag=zatca-views
Copies qr-code.blade.php to resources/views/vendor/zatca/ so you can customize the default SVG layout.
Note: This view auto-detects
simplesoftwareio/simple-qrcode,endroid/qr-code, and falls back to the built-in generator. Publish only if you need to customize the template.
Verify Installation
php artisan zatca:check
Configuration
Set these in your .env file:
# --- Phase Selection --- ZATCA_PHASE=both # phase_1, phase_2, both # --- Environment --- ZATCA_ENVIRONMENT=sandbox # sandbox | production
Env Variables Reference
| Variable | Required | Description | Where to get it |
|---|---|---|---|
ZATCA_PHASE |
Yes | Which phase to enable: phase_1, phase_2, or both |
You choose |
ZATCA_ENVIRONMENT |
Yes | sandbox for testing, production for live |
You choose |
ZATCA_EGS_UUID |
Phase 2 | Unique ID for your ERP/Government System | Generated by you (any UUID v4). Used to identify your system to ZATCA. |
ZATCA_VAT_NUMBER |
Yes | Your company VAT number (15 digits in Saudi Arabia) | Your company tax registration |
ZATCA_VAT_NAME |
Yes | Your company legal name as registered with ZATCA | Your company registration |
ZATCA_CRN_NUMBER |
Phase 2 | Commercial Registration Number | Your company commercial registry |
ZATCA_INDUSTRY |
Phase 2 | Business industry (e.g., Retail, Healthcare) | Your company profile |
ZATCA_CITY |
Phase 2 | City name (e.g., Riyadh, Jeddah) | Your business address |
ZATCA_CITY_SUBDIVISION |
Phase 2 | City district or suburb | Your business address |
ZATCA_STREET |
Phase 2 | Street name | Your business address |
ZATCA_BUILDING |
Phase 2 | Building number | Your business address |
ZATCA_PLOT_ID |
Phase 2 | Plot identification number | Your business address |
ZATCA_POSTAL_ZONE |
Phase 2 | Postal/ZIP code | Your business address |
ZATCA_BRANCH_NAME |
Phase 2 | Branch name (e.g., Main Branch) | Your business structure |
ZATCA_QUEUE_CONNECTION |
Optional | Queue driver for async sync (sync, redis, database) |
Your Laravel queue config |
ZATCA_QUEUE_NAME |
Optional | Queue name for ZATCA jobs | You choose |
ZATCA_QUEUE_TRIES |
Optional | Max retry attempts on failure | You choose |
ZATCA_QUEUE_TIMEOUT |
Optional | Job timeout in seconds | You choose |
ZATCA_RETRY_DELAY_MINUTES |
Optional | Delay between retries in minutes | You choose |
ZATCA_API_MIDDLEWARE |
Optional | Middleware group for API routes (default: api) |
You choose |
ZATCA_CERTIFICATE |
Phase 2 | Base64-encoded compliance certificate from ZATCA | ZATCA Developer Portal → after running zatca:onboard with OTP. The certificate is the binarySecurityToken returned by the compliance API. |
ZATCA_PRIVATE_KEY |
Phase 2 | Base64-encoded EC private key (secp256k1) | Generated by you via zatca:onboard or Zatca::phase2()->generateKeysAndCsr(). Store securely — this is your secret key for signing invoices. |
ZATCA_SECRET |
Phase 2 | Secret string returned by ZATCA during onboarding | ZATCA Developer Portal → returned alongside the certificate when you issue a compliance certificate with OTP. |
How the onboarding flow works
1. You run: php artisan zatca:onboard --otp=123456 --save
2. Package generates EC key pair (private_key + public_key)
3. Package creates a CSR (Certificate Signing Request)
4. Package sends CSR + OTP to ZATCA API
5. ZATCA returns:
- binarySecurityToken → save as ZATCA_CERTIFICATE
- secret → save as ZATCA_SECRET
6. Your private_key → save as ZATCA_PRIVATE_KEY
The OTP is obtained from the ZATCA Developer Portal (sandbox) or ZATCA production portal.
Full config reference
See config/zatca.php for all available options with documentation.
Phase 1 -- QR Code Generation (Basic Compliance)
Phase 1 requires no API calls. It generates a TLV-encoded Base64 QR string containing:
| Tag | Field | Example |
|---|---|---|
| 1 | Seller Name | شركة التقنية |
| 2 | VAT Number | 300000000000003 |
| 3 | Date/Time (ISO 8601) | 2024-01-01T12:00:00Z |
| 4 | Invoice Total (SAR) | 115.00 |
| 5 | VAT Total (SAR) | 15.00 |
Usage
use Aghfatehi\Zatca\Facades\Zatca; // Simple QR text generation $qrText = Zatca::phase1()->generateQrCodeText( sellerName: 'شركة التقنية', vatNumber: '300000000000003', invoiceDate: '2024-01-01T12:00:00Z', totalAmount: '115.00', taxAmount: '15.00', ); // Base64-encoded TLV string ready for embedding echo $qrText;
Using with Invoice DTO
use Aghfatehi\Zatca\DTO\InvoiceDTO; $invoice = InvoiceDTO::fromArray([ 'invoice_serial_number' => 'INV-001', 'issue_date' => '2024-01-01', 'issue_time' => '12:00:00', 'line_items' => [ [ 'id' => '1', 'name' => 'Product A', 'quantity' => 2, 'tax_exclusive_price' => 100.00, 'vat_percent' => 0.15, ], ], ]); $egsUnit = [ 'vat_name' => 'شركة التقنية', 'vat_number' => '300000000000003', ]; $qrText = Zatca::phase1()->generateQrCodeFromInvoice($invoice, $egsUnit);
Phase 2 -- FATOORA API Integration (Full Compliance)
Phase 2 requires completing the ZATCA onboarding process to obtain a compliance certificate, then signing and submitting invoices.
Step 1: Onboarding (One-time setup)
Generate EC key pair, CSR, and get compliance certificate from ZATCA:
php artisan zatca:onboard --otp=123456 --solution-name=ERP --save
Or programmatically:
// Generate keys & CSR $keys = Zatca::phase2()->generateKeysAndCsr($egsUnit, 'ERP'); // Issue compliance certificate with OTP from ZATCA portal $result = Zatca::phase2()->issueComplianceCertificate($keys['csr'], $otp); if ($result->success) { // Save these securely $certificate = $result->binarySecurityToken; $secret = $result->secret; $privateKey = $keys['private_key']; // Store in .env or database \Illuminate\Support\Facades\Env::set('ZATCA_CERTIFICATE', base64_encode($certificate)); \Illuminate\Support\Facades\Env::set('ZATCA_SECRET', $secret); \Illuminate\Support\Facades\Env::set('ZATCA_PRIVATE_KEY', base64_encode($privateKey)); }
Step 2: Sign & Submit Invoice
// Build invoice data $invoice = InvoiceDTO::fromArray([ 'invoice_serial_number' => 'EGS1-886431145-1', 'invoice_counter_number' => 2, 'issue_date' => '2024-01-01', 'issue_time' => '14:40:40', 'previous_invoice_hash' => '', 'line_items' => [ [ 'id' => '1', 'name' => 'Product A', 'quantity' => 10, 'tax_exclusive_price' => 100.00, 'vat_percent' => 0.15, ], ], ]); $egsUnit = [ 'uuid' => '6f4d20e0-6bfe-4a80-9389-7dabe6620f12', 'custom_id' => 'EGS1-886431145', 'model' => 'Desktop', 'vat_number' => '300000000000003', 'vat_name' => 'شركة التقنية', 'crn_number' => '454634645645654', 'location' => [ 'city' => 'Riyadh', 'city_subdivision' => 'West', 'street' => 'King Fahd Road', 'building' => '1234', 'plot_identification' => '0000', 'postal_zone' => '11564', ], 'branch_name' => 'Main Branch', 'branch_industry' => 'Retail', ]; // 1. Sign invoice (generates XML + hash + QR) $signed = Zatca::phase2()->signInvoice( invoice: $invoice, egsUnit: $egsUnit, certificate: $certificate, privateKey: $privateKey, ); // 2. Submit to ZATCA (auto-detects sandbox vs production) $result = Zatca::phase2()->submitInvoice( signedInvoiceXml: $signed['signed_xml'], invoiceHash: $signed['invoice_hash'], certificate: $certificate, secret: $secret, ); if ($result->success) { echo 'Invoice submitted successfully! Request ID: ' . $result->requestID; }
Using Queue for Async Sync
use Aghfatehi\Zatca\Jobs\SyncInvoiceToZatcaJob; SyncInvoiceToZatcaJob::dispatch( invoiceData: $invoice->toArray(), egsUnit: $egsUnit, certificate: $certificate, privateKey: $privateKey, secret: $secret, );
QR Code Display on PDF / View
Method 1: Blade View (Direct Rendering)
1. In the Controller — generate QR TLV:
<?php namespace App\Http\Controllers; use Aghfatehi\Zatca\Facades\Zatca; class InvoiceController extends Controller { public function show(Invoice $invoice) { $qrTlv = Zatca::phase1()->generateQrCodeFromInvoice( invoice: $invoice->toInvoiceDto(), egsUnit: [ 'vat_name' => config('zatca.egs.vat_name'), 'vat_number' => config('zatca.egs.vat_number'), ], ); return view('invoice.show', compact('invoice', 'qrTlv')); } }
2. In the Blade file — render the QR:
{{-- resources/views/invoice/show.blade.php --}} @extends('layouts.app') @section('content') <div class="invoice"> <h1>invoice No: {{ $invoice->number }}</h1> <table> @foreach ($invoice->items as $item) <tr> <td>{{ $item->name }}</td> <td>{{ $item->price }}</td> </tr> @endforeach </table> {{-- QR Code output — PNG (with endroid/qr-code) or SVG (fallback, not ZATCA-compatible) --}} <div class="qr-section" style="text-align: center; margin-top: 20px;"> <img src="data:image/png;base64,{{ base64_encode(Zatca::qr()->render($qrTlv, 200)) }}" alt="ZATCA QR Code" style="width: 200px; height: 200px;"> </div> </div> @endsection
How it works: render() auto-detects installed packages in this order:
simplesoftwareio/simple-qrcode→ SVG (ZATCA-compatible)endroid/qr-code→ PNG (ZATCA-compatible)- Built-in
SvgQrGenerator→ SVG (visual-only, not ZATCA-compatible)
Note: This is the same priority as the built-in Blade view. Install either
simplesoftwareio/simple-qrcodeorendroid/qr-codefor production use.
Method 2: Using the Built-in Blade View
The package ships with a qr-code.blade.php view under the zatca:: namespace. It auto-detects the available QR library:
| Priority | Library | Output | ZATCA Compatible | How to install |
|---|---|---|---|---|
| 1st | simplesoftwareio/simple-qrcode |
SVG | ✅ Yes | composer require simplesoftwareio/simple-qrcode |
| 2nd | endroid/qr-code |
SVG | ✅ Yes | composer require endroid/qr-code |
| 3rd | Built-in SvgQrGenerator (fallback) |
SVG | ❌ No | Not needed (but avoid for production) |
⚠️ Important: The built-in
SvgQrGeneratoris a visual-only fallback for development previews. It does not produce QR codes that the official ZATCA (Fatoora) app can scan. You must install one of the recommended packages (simplesoftwareio/simple-qrcodeorendroid/qr-code) for ZATCA-compliant QR codes.
View variables:
| Variable | Type | Default | Description |
|---|---|---|---|
$qrData |
string | required | TLV-encoded QR text from generateQrCodeText() |
$size |
int | 200 | Width/height of the QR code in pixels |
Example:
{{-- Pass TLV data from controller --}} @include('zatca::qr-code', ['qrData' => $qrTlv, 'size' => 200])
Full controller + view example:
use Zatca; class InvoiceController extends Controller { public function show(Invoice $invoice) { $qrTlv = Zatca::phase1()->generateQrCodeText( sellerName: config('zatca.egs.vat_name'), vatNumber: config('zatca.egs.vat_number'), invoiceDate: $invoice->created_at->format('Y-m-d\TH:i:s\Z'), totalAmount: (string)$invoice->total, taxAmount: (string)$invoice->tax, ); return view('invoice.show', compact('invoice', 'qrTlv')); } }
{{-- resources/views/invoice/show.blade.php --}} @extends('layouts.app') @section('content') <div class="invoice"> <h1>Invoice #{{ $invoice->number }}</h1> {{-- ... invoice details ... --}} <div class="qr-section" style="text-align: center; margin-top: 20px;"> @include('zatca::qr-code', ['qrData' => $qrTlv, 'size' => 200]) </div> </div> @endsection
Note: This view is always available (no
vendor:publishrequired). Runphp artisan vendor:publish --tag=zatca-viewsonly if you need to customize the blade template.
Phase 2 QR Code (9 Tags)
Phase 1 QR contains 5 tags (seller name, VAT, date, total, tax). When scanned with the ZATCA (Fatoora) app, it displays a notice: "This code is not compatible with Phase 2" — this is normal for Phase 1 QR.
For full Phase 2 compliance, the QR must contain 9 tags including the invoice hash and digital signature. Generate it after signing the invoice:
$qrData = Zatca::generatePhase2Qr( sellerName: config('zatca.egs.vat_name'), vatNumber: config('zatca.egs.vat_number'), invoiceDate: $invoice->created_at->format('Y-m-d\TH:i:s\Z'), totalAmount: '115.00', taxAmount: '15.00', invoiceHash: $signedInvoice['invoice_hash'], // from InvoiceSignerService digitalSignature: $signedInvoice['digital_signature'], // ECDSA signature publicKey: $signedInvoice['public_key'], // from certificate certificateSignature: $signedInvoice['certificate_signature'], // from ZATCA );
Then pass it to the same view:
@include('zatca::qr-code', ['qrData' => $qrData, 'size' => 200])
The 9 tags in a Phase 2 QR:
| Tag | Field | Source |
|---|---|---|
| 1 | Seller Name | Config |
| 2 | VAT Number | Config |
| 3 | Timestamp | Invoice date |
| 4 | Invoice Total | Invoice |
| 5 | Total VAT | Invoice |
| 6 | Invoice Hash | SHA-256 of signed XML |
| 7 | Digital Signature | ECDSA signature |
| 8 | Public Key | EC certificate |
| 9 | Certificate Signature | ZATCA certificate |
Method 3: Using the Model Trait
Add the trait to your invoice model:
use Aghfatehi\Zatca\Traits\HasZatcaQrCode; class Invoice extends Model { use HasZatcaQrCode; // Customize field names (optional) protected $zatcaSellerField = 'company_name'; protected $zatcaVatField = 'vat_number'; protected $zatcaDateField = 'invoice_date'; protected $zatcaTotalField = 'total_amount'; protected $zatcaTaxField = 'tax_amount'; }
Then in your view:
{{-- Automatically generates QR from model fields --}} {!! $invoice->getZatcaQrCode(200) !!}
Method 4: PDF Generation with barryvdh/laravel-dompdf
use Barryvdh\DomPDF\Facade\Pdf; $qrText = Zatca::phase1()->generateQrCodeText( sellerName: $invoice->company_name, vatNumber: $invoice->vat_number, invoiceDate: $invoice->invoice_date->format('Y-m-d\TH:i:s\Z'), totalAmount: (string)$invoice->total_amount, taxAmount: (string)$invoice->tax_amount, ); // Generate QR as data URI (auto-detects format) $qrDataUri = Zatca::qr()->renderAsDataUri($qrText, 150); $pdf = Pdf::loadView('invoice.pdf', compact('invoice', 'qrDataUri')); return $pdf->download('invoice.pdf');
In invoice/pdf.blade.php:
<html> <head> <style> .qr-code { position: fixed; bottom: 20px; right: 20px; width: 150px; } </style> </head> <body> <h1>{{ $invoice->invoice_serial_number }}</h1> <table> <tr><th>Item</th><th>Price</th><th>VAT</th></tr> @foreach($invoice->items as $item) <tr><td>{{ $item->name }}</td><td>{{ $item->price }}</td><td>{{ $item->vat }}</td></tr> @endforeach </table> <div class="qr-code"> <img src="{{ $qrDataUri }}" alt="ZATCA QR"> </div> </body> </html>
Method 5: PDF with mpdf (for ERP System or E-commerce)
use Mpdf\Mpdf; $mpdf = new Mpdf(['mode' => 'utf-8', 'format' => 'A4']); $qrText = Zatca::phase1()->generateQrCodeText(...); $qrDataUri = Zatca::qr()->renderAsDataUri($qrText, 150); $html = '<div style="position: absolute; bottom: 10mm; right: 10mm;"> <img src="' . $qrDataUri . '" width="150" height="150"/> </div>'; $mpdf->WriteHTML($html); $mpdf->Output('invoice.pdf', 'D');
Method 6: Advanced Output (Base64, Data URI, File)
// Base64-encoded image (SVG or PNG depending on installed packages) $base64 = Zatca::qr()->renderAsBase64($qrText, 200); // Data URI ready for <img> tag $dataUri = Zatca::qr()->renderAsDataUri($qrText, 200); // Save directly to file (SVG or PNG) Zatca::qr()->renderToFile($qrText, storage_path('app/public/qr/invoice.svg'), 200);
These methods work automatically — output format depends on which package is installed (see Method 1).
API Routes (Optional)
The package registers HTTP API endpoints. You do not need these for basic Phase 1 or Phase 2 usage — they are an alternative to calling the package methods directly from PHP code or Artisan commands.
These routes are separate from the QR rendering methods above. By default they use the api middleware group — change it with ZATCA_API_MIDDLEWARE in your .env.
| Method | Path | Description |
|---|---|---|
POST |
/zatca/onboard |
Onboard via API — requires otp and optional solution_name |
POST |
/zatca/invoice/sync |
Dispatch a sync job — requires invoice_serial_number |
GET |
/zatca/status |
Returns current phase, environment, and enabled status |
These routes are completely independent from the QR rendering methods above. You can use them with any HTTP client (Postman, cURL, your frontend app, etc.).
Testing with cURL
Test the routes directly with cURL:
# Check package status curl -X GET http://localhost:8000/zatca/status \ -H "Accept: application/json" # Onboard with OTP curl -X POST http://localhost:8000/zatca/onboard \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"otp": "123456", "solution_name": "ERP"}' # Dispatch invoice sync curl -X POST http://localhost:8000/zatca/invoice/sync \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"invoice_serial_number": "INV-001"}'
Offline Mode & Queue Sync (Optional)
The package natively supports offline invoice preparation with queue-based synchronization. You do not need this for basic Phase 1 or Phase 2 — it is only useful if you want to generate and sign invoices when offline, then sync them to ZATCA later.
Offline Flow
┌─────────────────────────────────────────────────────────┐
│ OFFLINE MODE │
│ │
│ ERP System ──► Generate QR (Phase 1) │
│ ──► Sign Invoice (Phase 2) │
│ ──► Save Signed XML Locally │
│ ──► Dispatch SyncInvoiceToZatcaJob │
│ │
│ When online ──► Queue Worker picks up job │
│ ──► Submits to ZATCA API │
│ ──► Fires InvoiceCleared/InvoiceReported │
│ ──► Logs result │
└─────────────────────────────────────────────────────────┘
Configuration
# Use database queue for persistence across restarts ZATCA_QUEUE_CONNECTION=database ZATCA_QUEUE_NAME=zatca # Retry settings ZATCA_QUEUE_TRIES=5 ZATCA_RETRY_DELAY_MINUTES=60
Run the Queue Worker
php artisan queue:work --queue=zatca --tries=3 --delay=3600
Sync Pending Invoices Manually
# Sync a specific invoice php artisan zatca:sync --invoice=INV-001 # Sync all pending invoices php artisan zatca:sync --all
Data Flow
Phase 1 Flow:
Invoice Data (5 tags) --> TLV Encoder --> Base64 --> QR Image
Phase 2 Flow:
1. Generate EC Key Pair (secp256k1)
2. Generate CSR
3. Submit CSR + OTP --> ZATCA API --> Compliance Certificate
4. Build UBL 2.1 XML Invoice
5. Hash Invoice (SHA-256)
6. Create Digital Signature (ECDSA)
7. Generate TLV QR (9 tags)
8. Embed XAdES Signature
9. Submit to ZATCA:
- Sandbox: POST /compliance/invoices
- Production: POST /invoices/clearance OR /reporting
10. Handle Response --> Fire Events --> Log
Events (Optional)
The package fires events that you can listen to in your application. Not needed for basic usage — only useful if you want to react to invoice submission results (e.g., update your database when an invoice is cleared).
| Event | Description | Payload |
|---|---|---|
InvoiceCleared |
Invoice successfully cleared (production) | invoiceData, ComplianceResultDTO |
InvoiceReported |
Invoice reported successfully (sandbox) | invoiceData, ComplianceResultDTO |
InvoiceComplianceChecked |
Compliance check completed | invoiceData, ComplianceResultDTO |
InvoiceFailed |
Invoice submission failed | invoiceData, errorMessage |
Example Listener
namespace App\Listeners; use Aghfatehi\Zatca\Events\InvoiceCleared; class UpdateInvoiceStatus { public function handle(InvoiceCleared $event): void { $serial = $event->invoiceData['invoice_serial_number']; // Update your invoice status in DB Invoice::where('serial_number', $serial) ->update(['zatca_status' => 'cleared']); } }
Register in EventServiceProvider:
protected $listen = [ \Aghfatehi\Zatca\Events\InvoiceCleared::class => [ \App\Listeners\UpdateInvoiceStatus::class, ], \Aghfatehi\Zatca\Events\InvoiceFailed::class => [ \App\Listeners\MarkInvoiceAsFailed::class, ], ];
Artisan Commands
| Command | Required | Description |
|---|---|---|
php artisan zatca:onboard |
Phase 2 | Interactive onboarding wizard (generates keys, CSR, gets certificate) |
php artisan zatca:check |
Phase 1 & 2 | Check package readiness (OpenSSL, config, etc.). Run after install. |
php artisan zatca:sync |
Optional | Sync invoices to ZATCA via queue (single or all pending) |
Testing
composer test
Or with PHPUnit directly:
vendor/bin/phpunit
Security & Logging (Optional)
Logging is enabled by default but not required for basic Phase 1 or Phase 2. It is useful for audit trails and debugging.
Logging Design
- Levels:
info,warning,erroronly - Non-blocking: Uses Laravel's async-safe log channel
- PII Masking: Automatically masks
otp,secret,password,private_key,csr - Truncation: Values exceeding 500 chars are truncated
- Audit trail: Optional
zatca_invoice_logsdatabase table for compliance tracking
Security Best Practices
- Never store private keys or secrets in code
- Use
.envvariables or a secrets manager for credentials - Restrict queue worker access to authorized personnel
- Enable PII masking in production (
ZATCA_LOG_MASK_PII=true) - Use HTTPS for all ZATCA API communication (enforced by cURL)
- Rotate OTP and secrets according to ZATCA guidelines
Integration Scenarios
Scenario 1: New Project — Both Phases
ZATCA_PHASE=both ZATCA_ENVIRONMENT=sandbox
Start with Phase 1 QR codes immediately, then add Phase 2 when ready.
Scenario 2: Phase 1 Only (Existing System)
ZATCA_PHASE=phase_1
Just generate QR codes. No API keys needed.
Scenario 3: Phase 2 Only (Already have Phase 1)
ZATCA_PHASE=phase_2
You already generate QR codes elsewhere (or via another package). This package handles the API integration.
Scenario 4: External API Integration
If your ERP exposes an API, you can use the package's internal services directly:
use Aghfatehi\Zatca\Services\Phase2Service; class YourController { public function sync(Request $request, Phase2Service $phase2) { $invoice = InvoiceDTO::fromArray($request->all()); $result = $phase2->signInvoice($invoice, ...); // ... } }
Support
- Issues: github.com/aghfatehi/laravel-zatca/issues
- Source: github.com/aghfatehi/laravel-zatca
- ZATCA Portal: sandbox.zatca.gov.sa