Search by

corecave / laravel-zatca

taekunger

Laravel package for ZATCA e-invoicing integration (Saudi Arabia Phase 2)

Package info

github.com/nadyshalaby/laravel-zatca

pkg:composer/corecave/laravel-zatca

Statistics

Installs: 7 338

Dependents: 0

Suggesters: 0

Stars: 7

Open Issues: 0

v2.1.0 2026-10-17 19:09 UTC

This package is auto-updated.

Last update: 2026-10-17 19:10:26 UTC


README

A comprehensive Laravel package for integrating with Saudi Arabia's ZATCA (Zakat, Tax and Customs Authority) e-invoicing system (FATOORA platform).

This package supports Phase 2 requirements including:

  • CSR generation and certificate management
  • Invoice XML generation (UBL 2.1 compliant)
  • Digital signing with ECDSA (secp256k1)
  • QR code generation (9 TLV tags)
  • Invoice reporting (B2C simplified invoices)
  • Invoice clearance (B2B standard invoices)
  • Credit and debit note handling
  • Sandbox, simulation, and production environments

Requirements

  • PHP 8.3+, which Laravel 13 needs
  • Laravel 13.x
  • OpenSSL extension
  • GMP extension (recommended for better performance)

Installation

The package is on Packagist:

composer require corecave/laravel-zatca:^2.1

Publish the configuration file:

php artisan vendor:publish --tag=zatca-config

The package ships no migrations. Each workshop keeps its own certificates, invoice counter and hash chain in its own schema, so the host app keeps the two table migrations with its workshop migrations (database/migrations/tenant in Syanat) and runs them per workshop:

php artisan tenants:migrate

An app whose one seller keeps its device centrally, while a tenant's connection is the default, keeps the two tables with its central migrations instead and names its central connection in ZATCA_DB_CONNECTION: the certificates, the chain and the chain's lock all use it.

Configuration

Add these environment variables to your .env file:

# Environment: sandbox, simulation, or production
ZATCA_ENVIRONMENT=simulation

# Seller Information
ZATCA_SELLER_NAME="Your Company Name"
ZATCA_SELLER_NAME_AR="اسم شركتك"
ZATCA_VAT_NUMBER=300000000000003
ZATCA_REGISTRATION_NUMBER=1234567890

# Seller Address
ZATCA_SELLER_STREET="Main Street"
ZATCA_SELLER_BUILDING="1234"
ZATCA_SELLER_CITY="Riyadh"
ZATCA_SELLER_DISTRICT="Al Olaya"
ZATCA_SELLER_POSTAL_CODE="12345"

# CSR Configuration
ZATCA_CSR_ORGANIZATION="Your Company Name"
ZATCA_CSR_ORGANIZATION_UNIT="Main Branch"
ZATCA_CSR_COMMON_NAME="Your Company Name"
ZATCA_INVOICE_TYPES=1100

# Database connection for the certificates and the chain (optional; empty is the default connection)
ZATCA_DB_CONNECTION=

# Debug (optional)
ZATCA_DEBUG_ENABLED=true
ZATCA_DEBUG_PATH=zatca/debug

Environments

The package supports three environments:

Environment Endpoint Certificate Issuer Purpose
sandbox developer-portal CN=eInvoicing (mock) Basic development testing
simulation simulation CN=TSZEINVOICE-SubCA-1, DC=extgazt, DC=gov, DC=local Real testing with ZATCA
production core CN=PEZEINVOICESCA2-CA, DC=extgazt, DC=gov, DC=local Live production

Important:

  • The sandbox environment returns mock certificates that will NOT pass ZATCA's official validators
  • Use simulation for testing with the ZATCA validator at https://sandbox.zatca.gov.sa/Compliance
  • Use production only when ready to go live

Onboarding Process

Step 1: Generate CSR

php artisan zatca:generate-csr --save

Step 2: Get Compliance CSID

  1. Log in to the FATOORA portal
  2. Generate an OTP for your EGS unit
  3. Run the compliance command:
php artisan zatca:compliance --otp=123456

Step 3: Get Production CSID

After passing compliance checks:

php artisan zatca:production-csid

Usage

Creating a Simplified Invoice (B2C)

use Corecave\Zatca\Facades\Zatca;
use Corecave\Zatca\Invoice\InvoiceBuilder;
use Corecave\Zatca\Enums\VatCategory;

// Build the invoice
$invoice = InvoiceBuilder::simplified()
    ->setInvoiceNumber('INV-2024-001')
    ->setIssueDate(now())
    ->addLineItem([
        'name' => 'Product Name',
        'quantity' => 2,
        'unit_price' => 100.00,
        'vat_category' => VatCategory::STANDARD,
    ])
    ->addLineItem([
        'name' => 'Service',
        'quantity' => 1,
        'unit_price' => 250.00,
        'vat_category' => VatCategory::STANDARD,
    ])
    ->build();

// Report to ZATCA
$result = Zatca::report($invoice);

if ($result->isSuccess()) {
    $qrCode = $result->getQrCode();
    $signedXml = $result->getSignedXml();

    // Store or display the QR code
}

Creating a Standard Invoice (B2B)

For standard B2B invoices, buyer information is required and must include complete address details for Saudi Arabian buyers.

use Corecave\Zatca\Facades\Zatca;
use Corecave\Zatca\Invoice\InvoiceBuilder;

$invoice = InvoiceBuilder::standard()
    ->setInvoiceNumber('INV-2024-002')
    ->setIssueDate(now())
    ->setBuyer([
        'name' => 'Buyer Company LLC',
        'vat_number' => '300000000000003',        // Used for TIN scheme if no registration_number
        'registration_number' => '1234567890',   // CRN - Commercial Registration Number
        'registration_scheme' => 'CRN',          // Optional: CRN, MOM, MLS, SAG, 700, OTH
        'address' => [
            'street' => 'King Fahd Road',
            'building' => '1234',                // Required for SA buyers (KSA-18)
            'additional_number' => '5678',       // Optional (KSA-23)
            'city' => 'Riyadh',
            'district' => 'Al Olaya',            // Required for SA buyers (KSA-4)
            'postal_code' => '12345',
            'country' => 'SA',
        ],
    ])
    ->addLineItem([
        'name' => 'Consulting Services',
        'quantity' => 10,
        'unit_price' => 1000.00,
        'vat_category' => VatCategory::STANDARD,
    ])
    ->build();

// Clear with ZATCA (must be done BEFORE issuing to customer)
$result = Zatca::clear($invoice);

if ($result->isSuccess()) {
    // ZATCA adds cryptographic stamp and QR code
    $clearedXml = $result->getClearedXml();
    $qrCode = $result->getQrCode();
}

Buyer Identification Schemes

For the registration_scheme field, ZATCA accepts:

Scheme ID Description
CRN Commercial Registration Number
MOM Momra License
MLS MLSD License
SAG Sagia License
NAT National ID (10 digits)
GCC GCC ID
IQA Iqama Number
TIN Tax Identification Number (VAT)
700 700 Number
OTH Other ID

Required Buyer Address Fields for Saudi Arabia (BR-KSA-63)

When the buyer's country is SA, these fields are mandatory:

Field XML Element Description
street cbc:StreetName Street name (BT-50)
building cbc:BuildingNumber Building number - 4 digits (KSA-18)
postal_code cbc:PostalZone Postal code - 5 digits (BT-53)
city cbc:CityName City name (BT-52)
district cbc:CitySubdivisionName District name (KSA-4)
country cbc:IdentificationCode Country code (BT-55)

Creating Credit/Debit Notes

use Corecave\Zatca\Invoice\InvoiceBuilder;

// Credit note for a standard invoice
$creditNote = InvoiceBuilder::creditNote(simplified: false)
    ->setInvoiceNumber('CN-2024-001')
    ->setOriginalInvoice('INV-2024-002') // Reference original invoice
    ->setReason('Returned goods')
    ->setBuyer([/* buyer details */])
    ->addLineItem([
        'name' => 'Returned Product',
        'quantity' => 1,
        'unit_price' => 100.00,
        'vat_category' => VatCategory::STANDARD,
    ])
    ->build();

// Process (automatically clears for standard, reports for simplified)
$result = Zatca::process($creditNote);

Auto-Processing Invoices

The process() method automatically determines whether to report or clear:

// Automatically uses report() for simplified invoices
// and clear() for standard invoices
$result = Zatca::process($invoice);

if ($result->wasReported()) {
    // B2C invoice was reported
}

if ($result->wasCleared()) {
    // B2B invoice was cleared
}

VAT Categories

use Corecave\Zatca\Enums\VatCategory;
use Corecave\Zatca\Enums\VatExemptionReason;

// Standard rate (15%)
->addLineItem([
    'name' => 'Product',
    'quantity' => 1,
    'unit_price' => 100.00,
    'vat_category' => VatCategory::STANDARD,
])

// Zero-rated
->addLineItem([
    'name' => 'Export Product',
    'quantity' => 1,
    'unit_price' => 100.00,
    'vat_category' => VatCategory::ZERO_RATED,
    'vat_exemption_reason' => VatExemptionReason::EXPORT,
])

// Exempt
->addLineItem([
    'name' => 'Financial Service',
    'quantity' => 1,
    'unit_price' => 100.00,
    'vat_category' => VatCategory::EXEMPT,
    'vat_exemption_reason' => VatExemptionReason::FINANCIAL_SERVICES,
])

Working with Certificates

use Corecave\Zatca\Facades\Zatca;

// Check if production certificate exists
if (Zatca::certificate()->hasActiveProductionCertificate()) {
    // Ready to issue invoices
}

// Get certificate details
$cert = Zatca::certificate()->getActive('production');
$expiresAt = $cert->getExpiresAt();
$isExpiringSoon = $cert->isExpiringSoon(30);

// Get certificates expiring soon
$expiring = Zatca::certificate()->getExpiringSoon(30);

// Get certificate issuer (for debugging)
$issuer = $cert->getFormattedIssuer();
// Should be: CN=TSZEINVOICE-SubCA-1, DC=extgazt, DC=gov, DC=local (simulation)
// NOT: CN=eInvoicing (mock/sandbox)

Manual XML Generation

use Corecave\Zatca\Facades\Zatca;

// Generate XML without submitting
$xml = Zatca::generateXml($invoice);

// Validate XML
$isValid = Zatca::validateXml($xml);

// Sign XML
$signedXml = Zatca::signXml($xml);

Hash Chain Management

use Corecave\Zatca\Hash\HashChainManager;

$hashManager = app(HashChainManager::class);

// Get next ICV
$icv = $hashManager->getNextIcv();

// Get previous invoice hash
$pih = $hashManager->getPreviousInvoiceHash();

// Verify chain integrity
$result = $hashManager->verifyChain();
if (!$result['valid']) {
    // Chain has issues
}

// Get statistics
$stats = $hashManager->getStatistics();

Artisan Commands

# Generate CSR for onboarding
php artisan zatca:generate-csr --save

# Run compliance process
php artisan zatca:compliance --otp=123456

# Request production CSID
php artisan zatca:production-csid

# Renew expiring certificate
php artisan zatca:renew-csid --otp=123456

Events

The package dispatches events you can listen to:

  • InvoiceReported - When a simplified invoice is successfully reported
  • InvoiceCleared - When a standard invoice is successfully cleared
  • InvoiceRejected - When an invoice is rejected by ZATCA
  • CertificateExpiring - When a certificate is about to expire

Debugging

Enable debug mode to save XML files and hashes for inspection:

ZATCA_DEBUG_ENABLED=true
ZATCA_DEBUG_PATH=zatca/debug

Debug files are saved to storage/app/{ZATCA_DEBUG_PATH}/:

  • {invoice}_unsigned.xml - XML before signing
  • {invoice}_signed.xml - XML after signing
  • {invoice}_hash.txt - Invoice hash
  • {invoice}_qr.txt - QR code data

Error Handling

use Corecave\Zatca\Exceptions\ApiException;
use Corecave\Zatca\Exceptions\ValidationException;
use Corecave\Zatca\Exceptions\CertificateException;

try {
    $result = Zatca::report($invoice);
} catch (ValidationException $e) {
    // Invoice validation failed
    $errors = $e->getErrors();
} catch (ApiException $e) {
    // ZATCA API error
    $zatcaErrors = $e->getZatcaErrors();
    $zatcaWarnings = $e->getZatcaWarnings();
} catch (CertificateException $e) {
    // Certificate issue: renew the CSID with `php artisan zatca:renew-csid`, then retry
    $certificateError = $e->getMessage();
}

Common ZATCA Validation Errors

Error Code Message Solution
X509IssuerName wrong X509IssuerName Use simulation or production environment to get real certificates
X509SerialNumber wrong X509SerialNumber Certificate not from ZATCA - regenerate with real OTP
BR-KSA-F-13 Invalid Seller/Buyer ID Provide valid registration_number with correct registration_scheme
BR-KSA-63 Missing address fields Include all required fields: street, building, postal_code, city, district
BR-CL-KSA-14 QR Code exceeds 1000 chars Reduce invoice data or check QR generation

Changelog

v2.1.0 (2026-10-17)

Added

  • zatca.connection (ZATCA_DB_CONNECTION): the connection ZatcaCertificate and ZatcaInvoice use and HashChainManager takes the chain's lock on, through the new HashChainManager::connection(); the compliance samples read their place in the chain there too. Empty, as before, everything stays on the default connection.

v2.0.0 (2026-10-17)

Changed

Every method now takes at most three parameters, so the long parameter lists moved into named value objects. The XML, hashes, QR codes and signatures the package produces are unchanged. Callers change as follows:

  • Zatca::generateQrCode($invoice, $signatureValue, $publicKey, $certificateSignature) is now Zatca::generateQrCode($invoice, new QrSignature($signatureValue, $publicKey, $certificateSignature)), and QrGenerator::generate() takes the same two arguments.
  • new Invoice(...) takes one InvoiceData and new LineItem(...) one LineItemData: set their public properties by name. InvoiceBuilder and LineItem::fromArray() work as before.
  • new Certificate($type, $certificate, $privateKey, $secret, $requestId, $issuedAt, $expiresAt, $active, $metadata) is now new Certificate(new CertificateOrigin($type, $requestId, $metadata), new CertificateCredentials($certificate, $privateKey, $secret), new CertificateLifetime($issuedAt, $expiresAt, $active)). Certificate::fromApiResponse() and Certificate::fromArray() work as before.
  • CertificateManager::import($type, $certificatePath, $privateKeyPath, $secret, $requestId) is now import(new CertificateOrigin($type, $requestId), new CertificateFiles($certificatePath, $privateKeyPath), $secret).
  • CsrGenerator::generateWithSerial($companyName, $version, $uuid, $config) is now generateWithSerial(new EgsSerialNumber($companyName, $version, $uuid), $config).
  • HashChainManager::recordInvoice(...) takes one InvoiceRecord: set its public properties by name. updateInvoiceStatus($uuid, $status, $zatcaResponse, $signedXml, $qrCode) is now updateInvoiceStatus($uuid, $status, new StatusUpdate($zatcaResponse, $signedXml, $qrCode)).
  • DebugDumper::dumpAll($invoiceId, $unsignedXml, $signedXml, $qrCode, $hash) is now dumpAll($invoiceId, new InvoiceDocuments($unsignedXml, $signedXml, $qrCode), $hash).
  • new ReportResult($invoice, $signedXml, $qrCode, $response) is now new ReportResult(new Submission($invoice, $response), $signedXml, $qrCode), and ClearanceResult likewise.
  • ZatcaException has no constructor of its own: build one with context through ZatcaException::withContext($message, $context). ApiException's constructor no longer takes the ZATCA errors and warnings; ApiException::fromResponse() reads them from the response.
  • UblGenerator writes the parties, totals and lines through UblPartyWriter, UblTotalsWriter and UblLineWriter; its public methods are unchanged.

Added

  • InvoiceBuilder::fromConfig() builds an InvoiceBuilder from the zatca.seller and zatca.invoice config, which the service provider and the builder's own static constructors used to read separately.

v1.1.0 (2024-12-13)

Fixed

  • X509IssuerName format: Now correctly uses RFC 4514 format with , (comma + space) separators matching ZATCA SDK
  • X509SerialNumber: Properly extracted from certificate
  • SignedProperties hash: XML whitespace now matches Python SDK's exact format for correct hash computation
  • Buyer PartyIdentification: Now always includes <cac:PartyIdentification> element with proper schemeID
  • Buyer postal address: Now includes all required SA address fields (BuildingNumber, District, etc.)

Changed

  • Certificate::getFormattedIssuer() no longer reverses DN order
  • XmlSigner::formatIssuerDN() preserves , separators
  • XmlSigner::computeSignedPropertiesHashSdkStyle() uses exact SDK whitespace
  • XmlSigner::buildSignatureXml() matches SDK indentation structure
  • UblGenerator::addCustomerParty() always adds buyer ID, uses VAT as TIN fallback

Added

  • Support for flat buyer data with all SA-required address fields
  • Automatic schemeID detection (TIN for VAT numbers, NAT for national IDs, CRN for registration numbers)

v1.0.0 (2024-12-12)

  • Initial release with full Phase 2 support

License

MIT License. See LICENSE for more information.

Resources