laravel-zatca maintained by delwarhossaindev
🧾 Laravel ZATCA
Saudi Arabia's Fatoora e-invoicing, Phase 1 & Phase 2, for Laravel
Onboard a device, sign the invoice and report or clear it, in a few lines of Laravel.
✨ Features • 📦 Installation • 🔐 Onboarding • 🧾 Invoices • ⚙️ Artisan • 🚀 Production
Zatca::onboard($device); // 🔐 one time per device
$document = Zatca::issueAndSubmit($device, $invoice); // 🧾 sign + report / clear
$document->status; // ✅ REPORTED / CLEARED
✨ Features
🔐 Onboarding
- secp256k1 key and CSR with the ZATCA extensions
- Compliance CSID with the Fatoora OTP
- All 6 compliance checks run automatically
- Production CSID issued
- 🔄 Certificate renewal without breaking the chain
🧾 Invoicing
- Standard (B2B) cleared, Simplified (B2C) reported
- Invoice, credit note and debit note
- VAT S · Z · E · O with exemption reasons
- UBL 2.1 XML, XAdES signature, Phase 2 QR
- ZATCA-stamped XML stored for cleared invoices
🏪 Multi-device
- Every branch or POS is its own EGS unit
- Separate certificate and ICV / PIH chain per device
- Row locking, so two invoices never share an ICV
🛡️ Built for real use
- Keys and secrets encrypted with
APP_KEY - 📴 Offline mode: sign now, send later
- ⏰ Scheduled retry for the 24 hour rule
- 🔗
HasZatcaDocumentstrait for your own models
🗺️ How it works
flowchart LR
subgraph ONCE["🔐 Once per device"]
A[🏢 Device info] --> B[🔑 CSR]
B --> C[📨 Compliance CSID<br/><sub>OTP</sub>]
C --> D[✅ 6 checks]
D --> E[🎫 Production CSID]
end
subgraph EVERY["🧾 Every invoice"]
F[📝 InvoiceData] --> G[✍️ Sign + QR]
G --> H{Type?}
H -->|B2C| I[📤 Report<br/><sub>within 24h</sub>]
H -->|B2B| J[🛂 Clear<br/><sub>before sharing</sub>]
end
E ==> F
[!TIP] 📘 বাংলা গাইড: docs/ZATCA-Guide-Bangla.pdf — কোন ফাইল কী কাজ করে, কীভাবে ব্যবহার করবেন, আর সাধারণ error-এর সমাধান।
📦 Installation
[!NOTE] Until the package is on Packagist, install it from GitHub. Supports Laravel 10, 11, 12 and 13.
1️⃣ Add the repository to your Laravel app's composer.json:
"repositories": [
{ "type": "vcs", "url": "https://github.com/delwarhossaindev/zatca_package" }
]
2️⃣ Install it and create the tables:
composer require delwarhossaindev/laravel-zatca:^1.0
php artisan migrate
This creates two tables:
| Table | Purpose |
|---|---|
🏪 zatca_devices |
Seller, EGS unit, keys, certificates, invoice chain |
📄 zatca_documents |
Signed XML, hash, QR, ZATCA status and response |
3️⃣ Configure .env:
ZATCA_ENV=sandbox # sandbox | simulation | production
ZATCA_TIMEZONE=Asia/Riyadh
Optional, to publish the config:
php artisan vendor:publish --tag=zatca-config
[!CAUTION] Private keys and secrets are encrypted with
APP_KEY. If you lose it, every device must be onboarded again. Back it up.
🔐 Onboarding
use delwarhossaindev\Zatca\Facades\Zatca;
use delwarhossaindev\Zatca\Models\ZatcaDevice;
$device = ZatcaDevice::create([
'environment' => 'sandbox',
'seller_name' => 'Al Noor Trading Co',
'vat_number' => '399999999900003', // 15 digits, starts and ends with 3
'crn' => '1010010000',
'street' => 'Prince Sultan',
'building_number' => '2322', // 4 digits
'district' => 'Al-Murabba',
'city' => 'Riyadh',
'postal_code' => '23333', // 5 digits
'egs_name' => 'POS-01',
'egs_serial' => '1-MyInventory|2-1.0|3-'.Str::uuid(),
'branch_name' => 'Riyadh Branch',
'industry' => 'Supply activities',
'invoice_types' => '1100', // standard + simplified
]);
Zatca::onboard($device); // 🧪 sandbox: OTP 123345 is used automatically
Zatca::onboard($device, '482913'); // 🏭 simulation / production: OTP from fatoora.zatca.gov.sa
| Value | Device issues |
|---|---|
1100 |
🏢 Standard + 🛒 Simplified |
1000 |
🏢 Standard (B2B) only |
0100 |
🛒 Simplified (B2C) only |
🧾 Issuing invoices
🛒 Simplified (B2C), reported
use delwarhossaindev\Zatca\Data\{InvoiceData, InvoiceLine, Buyer, Address};
$document = Zatca::issueAndSubmit($device, new InvoiceData(
number: 'INV-1001',
type: InvoiceData::SIMPLIFIED,
lines: [
new InvoiceLine('Rice 5kg', quantity: 2, unitPrice: 45.50),
new InvoiceLine('Medicine', 1, 30, InvoiceLine::ZERO_RATED, 0, 'VATEX-SA-35', 'Medicines and medical equipment'),
],
), $order); // 🔗 $order = your own model (optional)
$document->status; // ✅ REPORTED
$document->qr_code; // 📱 print this on the receipt
🏢 Standard (B2B), cleared
$document = Zatca::issueAndSubmit($device, new InvoiceData(
number: 'INV-1002',
type: InvoiceData::STANDARD,
lines: [new InvoiceLine('Laptop', 1, 2500)],
buyer: new Buyer(
name: 'ACME Trading LLC',
vatNumber: '399999999800003',
address: new Address('Salah Al-Din', '1111', 'Al-Murooj', 'Riyadh', '12222'),
),
paymentMeans: '30', // 💳 credit
));
$document->status; // ✅ CLEARED
$document->finalXml(); // 📄 ZATCA-stamped XML for the buyer
↩️ Credit and debit notes
Zatca::issueAndSubmit($device, new InvoiceData(
number: 'CRN-1',
type: InvoiceData::SIMPLIFIED,
lines: [new InvoiceLine('Rice 5kg', 1, 45.50)],
documentType: InvoiceData::CREDIT_NOTE, // or DEBIT_NOTE
originalInvoiceNumber: 'INV-1001',
noteReason: 'Returned items',
));
📴 Offline: sign now, send later
$document = Zatca::issue($device, $invoiceData, $order); // ⏳ pending
// ... internet is back ...
Zatca::submit($document); // ✅ REPORTED
[!TIP] Schedule the retry so every B2C invoice reaches ZATCA within 24 hours:
Schedule::command('zatca:submit-pending')->hourly();
| Code | Constant | Rate | Exemption reason |
|---|---|---|---|
S |
InvoiceLine::STANDARD |
15% | ➖ |
Z |
InvoiceLine::ZERO_RATED |
0% | ✔️ required |
E |
InvoiceLine::EXEMPT |
0% | ✔️ required |
O |
InvoiceLine::OUT_OF_SCOPE |
0% | ✔️ required |
| Code | Meaning |
|---|---|
10 |
💵 Cash (default) |
30 |
🧾 Credit |
42 |
🏦 Bank account |
48 |
💳 Bank card |
🔗 Linking your own models
use delwarhossaindev\Zatca\Concerns\HasZatcaDocuments;
class Order extends Model
{
use HasZatcaDocuments;
}
$order->zatcaDocument->status; // ✅ REPORTED
$order->zatcaDocument->qr_code; // 📱 QR payload
$order->zatcaDocuments; // 📚 invoice + its credit / debit notes
📄 The ZatcaDocument model
| Field / method | Meaning |
|---|---|
🏷️ status |
pending · REPORTED · CLEARED · NOT_REPORTED · NOT_CLEARED · ERROR |
✅ isAccepted() |
true once REPORTED or CLEARED |
⚠️ errors() / warnings() |
ZATCA's validation messages |
📱 qr_code |
Base64 TLV QR, render it with any QR library |
📄 signed_xml / finalXml() |
Signed XML, or ZATCA's stamped copy |
🔗 icv · invoice_hash · previous_invoice_hash |
The invoice chain |
[!IMPORTANT] An accepted document can never be sent again or edited. Fix mistakes with a credit or debit note.
⚙️ Artisan commands
| Command | What it does |
|---|---|
🔐 zatca:onboard {device} |
Full onboarding (--otp= outside the sandbox) |
🔄 zatca:onboard {device} --renew |
Renew the Production CSID |
📤 zatca:submit-pending |
Send documents that have not reached ZATCA yet |
📊 zatca:status |
Devices, certificate expiry, last ICV, pending count |
📮 zatca:payload {document} |
invoiceHash, uuid and invoice of a document for the Postman collection |
🔑 zatca:payload --device={id} |
CSR and CSID credentials of a device for the Postman collection |
If ZATCA answers with a server error (5xx) or a rate limit (429), the document stays
pendingandzatca:submit-pendingsends it again. The Postman collection (ZATCA-API.postman_collection.json) is in the GitHub repository, not in the Composer package.
🌍 Environments
| Environment | OTP | Counts as real tax? | |
|---|---|---|---|
| 🧪 | sandbox |
always 123345 |
❌ |
| 🎭 | simulation |
Fatoora portal | ❌ |
| 🏭 | production |
Fatoora portal | ✅ |
🚀 Going to production
- 🎭 Test once in
simulationwith your real company details - 🏭 Create a new device with
environment = production - 🔑 Run
php artisan zatca:onboard {id} --otp=...with a real OTP - 🧾 Send one small real invoice and check it is ✅ accepted
- 💾 Back up
.env(APP_KEY) and the database - ⏰ Schedule
zatca:submit-pending - 📅 Watch
zatca:statusand renew before the certificate expires (an expired device is onboarded again withzatca:onboard; the ICV / PIH chain continues)
🧪 Testing
composer install
vendor/bin/phpunit --testsuite Unit # 💻 offline
vendor/bin/phpunit --testsuite Sandbox # 🌐 against the real ZATCA sandbox
The sandbox suite runs the full flow against the ZATCA developer portal:
- onboarding with all 6 compliance checks
- a simplified invoice with a zero-rated line: REPORTED
- a standard invoice: CLEARED
- a credit note sent with
zatca:submit-pending: REPORTED - certificate renewal, after which the invoice chain still holds
Made with ❤️ for Saudi businesses by delwarhossaindev
⭐ If this package helps you, give it a star!