ZATCA Phase 2 Laravel Integration Guide
Master ZATCA Phase 2 e-invoicing in Laravel: clearance vs reporting, CSID onboarding, XAdES signing, and queue-based Fatoora API submission with production code.
Umar Farooq
System Architect & Full-Stack Engineer

ZATCA Phase 2 Laravel Integration: The Complete Guide
Every VAT-registered business in Saudi Arabia must clear its B2B invoices with ZATCA in real time, and a zatca phase 2 laravel integration fails in places the official documentation never warns you about. Phase 2, the Integration Phase, requires your Laravel application to generate UBL 2.1 XML, sign it with XAdES digital signatures, and submit it to the Fatoora APIs for clearance before the invoice is legally valid. I have wired this flow into Laravel applications serving Saudi businesses, and the gap between the docs and production is where most projects lose weeks.
Quick Answer: ZATCA Phase 2 integration means your Laravel app generates UBL 2.1 invoices, signs them with your CSID cryptographic stamp, and submits B2B invoices for real-time clearance (or B2C invoices for 24-hour reporting) through the Fatoora APIs. Build it as queue-based submission with signed-payload audit logs, and test the full flow in the simulation environment before touching production.
Clearance vs Reporting: Know Which Flow You Need
ZATCA Phase 2 has two distinct flows and your code must handle both. Standard (B2B) invoices go through clearance: your system submits the signed invoice and waits for ZATCA to return it cleared with a cryptographic stamp. The invoice is not valid until clearance succeeds. Simplified (B2C) invoices go through reporting: you issue the invoice to the customer first, then report it to ZATCA within 24 hours.
This distinction drives your architecture. Clearance is synchronous from the business perspective, so retry handling must be aggressive and loud. Reporting is asynchronous by nature, so a background retry loop with backoff is enough. Treating both flows identically will either block checkouts or silently drop reports.
Aspect | Clearance (B2B standard) | Reporting (B2C simplified) |
|---|---|---|
Timing | Real time, before the invoice is valid | Within 24 hours of issuance |
ZATCA response | Cleared invoice plus cryptographic stamp | Acknowledgement receipt |
On failure | Invoice cannot be issued, retry immediately | Invoice already issued, retry and reconcile |
Queue strategy | Priority queue with alerting on failure | Background queue with exponential backoff |
The Signing Flow: From CSID Onboarding to XAdES Signatures
Before a single invoice can be submitted, your business must complete zatca csid onboarding. The flow: generate a Certificate Signing Request from your server, fetch a one-time OTP from the Fatoora portal, submit the CSR with the OTP to the compliance endpoint, and receive your CSID certificate plus private key. Store both outside version control, in your secrets manager or encrypted environment storage. I have seen private keys committed to Git repositories more than once, and that mistake is painful to unwind.
Per invoice, the signing pipeline for ubl 2.1 xml signing runs: build the UBL 2.1 XML from your invoice model, canonicalize it, compute the invoice hash, apply the XAdES signature block using your CSID certificate, then generate the TLV QR payload from the signed data. Keep the full signed payload in an audit table. When ZATCA or an auditor asks what exactly was submitted six months later, that table is your answer.
Queue Every Submission: The Architecture That Survives Outages
Never submit to the fatoora api clearance endpoint inside the HTTP request cycle. ZATCA has maintenance windows, rate limits, and slow periods. A synchronous call turns their downtime into your checkout outage. Dispatch a queued job per invoice instead:
<?php
namespace App\Jobs;
use App\Models\Invoice;
use App\Services\ZatcaSigningService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
class SubmitZatcaInvoice implements ShouldQueue
{
use Queueable;
// ZATCA rate-limits during peak hours; back off instead of hammering.
public int $tries = 5;
public int $backoff = 120;
public function __construct(public Invoice $invoice) {}
public function handle(ZatcaSigningService $signer): void
{
// Idempotency guard: never submit the same invoice twice.
if ($this->invoice->zatca_status === 'cleared') {
return;
}
$signedXml = $signer->sign($this->invoice);
$response = $signer->submitForClearance($signedXml);
$this->invoice->update([
'zatca_status' => $response->cleared ? 'cleared' : 'failed',
'zatca_hash' => $response->invoiceHash,
'zatca_cleared_at' => $response->cleared ? now() : null,
]);
Log::info('ZATCA submission result', [
'invoice_id' => $this->invoice->id,
'cleared' => $response->cleared,
]);
}
}
The signing service itself stays small and testable. Each step is a separate method so failures are easy to isolate when ZATCA rejects a payload:
<?php
namespace App\Services;
use App\Models\Invoice;
class ZatcaSigningService
{
public function __construct(
protected string $certificatePath,
protected string $privateKeyPath,
) {}
public function sign(Invoice $invoice): string
{
// 1. Build UBL 2.1 XML from the invoice model.
$xml = $this->buildUblXml($invoice);
// 2. Canonicalize, hash, and apply the XAdES signature
// with the CSID certificate from onboarding.
$signed = $this->applyXadesSignature($xml);
// 3. Build the TLV QR payload from the signed data.
$qrPayload = $this->buildQrPayload($invoice, $signed);
// Persist everything: auditors will ask for the exact payload.
$invoice->zatcaPayload()->create([
'signed_xml' => $signed,
'qr_payload' => $qrPayload,
]);
return $signed;
}
protected function buildUblXml(Invoice $invoice): string { /* ... */ }
protected function applyXadesSignature(string $xml): string { /* ... */ }
protected function buildQrPayload(Invoice $invoice, string $signed): string { /* ... */ }
}
Sandbox, Simulation, Production: Closing the Gap
ZATCA provides three environments and most teams test in the wrong one. The sandbox accepts almost anything, which builds false confidence. The simulation environment mirrors production validation, so use it for your full end-to-end pass. E-invoicing saudi arabia projects most often break on three things: an expired CSID nobody rotated, server clock drift past the signature validity window, and duplicate invoice counters after a database restore. Monitor all three from day one.
Do I still need Phase 1 if I am building Phase 2 directly?
Yes, in practice. Phase 1 QR generation logic, the TLV payload format and the invoice data model, is reused inside Phase 2 signing. Build Phase 1 first even if your mandate wave only requires Phase 2; it gives you a tested foundation and a fallback display format.
How long does CSID onboarding take?
The technical steps take under an hour when your CSR is generated correctly and you have portal access with the OTP. The slow part is organizational: getting the authorized person to fetch the OTP and approve the device. Start that conversation early.
What happens if clearance fails at 2 AM?
Your queue retries with backoff, and after the final attempt the invoice lands in a dead-letter queue with an alert to your on-call channel. The invoice is not issued until clearance succeeds, so your staff sees a clear "pending clearance" state instead of a silent failure. I cover the full failure taxonomy in my guide to handling Fatoora API errors.
Can I test the full flow without production credentials?
Yes. The simulation environment issues test CSIDs and validates payloads against production rules. Never point test traffic at the production gateway; ZATCA treats those as real submissions.
Summary & Production Takeaways
A production-grade zatca phase 2 laravel integration comes down to four decisions: separate clearance from reporting, complete CSID onboarding before writing code, sign with XAdES and archive every payload, and submit through queued jobs with idempotency guards. Test in simulation and monitor certificate expiry. If you need this built, book a call, read about hiring a Laravel developer in Saudi Arabia, or explore my Laravel development services.

Umar Farooq
Author & ConsultantSpecializes in Laravel, Next.js, and AI products. 5+ years enterprise experience with 80+ delivered platforms and full source code ownership.
Related Engineering Insights

Mada Payment Gateway Laravel Comparison
Mada payment integration in Laravel compared: Moyasar, Tap and HyperPay webhook handling, Apple Pay support and sandbox quality, with production code.

How I Structure a Large Next.js Application
As Next.js applications grow beyond a few pages, messy folder structures create circular dependencies and bloated client bundles. Here is my scalable enterprise architecture.

Your Vibe-Coded App Works. Is It Actually Production Ready?
Getting an AI-generated app to run on localhost is easy. Making it survive concurrent traffic, SQL injections, and billing webhooks is where real engineering begins.