npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@khaledhajsalem/zatca-node

v1.0.4

Published

A comprehensive Node.js package for ZATCA (Saudi Arabia e-invoicing) invoice processing, signing, and submission

Downloads

149

Readme

ZATCA Node.js Package

npm version License ZATCA Phase 2

A Node.js/TypeScript package for ZATCA (Saudi Arabia) Phase 2 e-invoicing. Handles XML generation (UBL 2.1), digital signing, QR codes, and API submission — all without database dependencies.

Table of Contents


Requirements

  • Node.js 18.0+
  • OpenSSL (for CSR generation)

Installation

npm install @khaledhajsalem/zatca-node

Key Concepts

Before using this package, understand these ZATCA-specific terms:

| Term | What it means | |------|--------------| | Standard Invoice | B2B/B2G invoice. Must be cleared by ZATCA before you can send it to the buyer. Type name: 0100000. | | Simplified Invoice | B2C invoice (e.g., retail receipt). Must be reported to ZATCA within 24 hours. Type name: 0200000. | | Clearance | ZATCA validates and approves a Standard invoice in real-time. You get back a "cleared" XML. | | Reporting | You send a Simplified invoice to ZATCA for record-keeping. No real-time approval needed. | | PIH (Previous Invoice Hash) | SHA-256 hash of the previous invoice. Creates a tamper-proof chain. First invoice uses 'MA==' (base64 encoded '0'). | | ICV (Invoice Counter Value) | Sequential counter starting at 1. Must increment for every invoice. | | CSR | Certificate Signing Request — you generate this and send it to ZATCA to get your signing certificate. | | OTP | One-Time Password — ZATCA gives you this when you register your device on the Fatoora portal. |

Invoice Type Codes

| Code | Type | Method | |------|------|--------| | 388 | Tax Invoice | .taxInvoice() | | 381 | Credit Note | .creditNote() | | 383 | Debit Note | .debitNote() | | 386 | Prepayment Invoice | .prepaymentInvoice() |

Party Identification Schemes

Both seller and buyer support these identification types:

| Scheme ID | Description | |-----------|-------------| | CRN | Commercial Registration Number | | VAT | VAT Number | | TIN | Tax Identification Number | | NAT | National ID | | IQA | Iqama Number | | GCC | GCC ID | | PAS | Passport ID | | MOM | MOMRAH License | | MLS | MHRSD License | | SAG | MISA License | | 700 | 700 Number | | OTH | Other ID |


How It Works (Lifecycle)

Here is the complete flow from setup to invoice submission:

┌─────────────────────────────────────────────────────────────────┐
│  ONE-TIME SETUP                                                 │
│                                                                 │
│  1. Generate CSR + Private Key  (CertificateBuilder)            │
│  2. Submit CSR to ZATCA with OTP → get Compliance Certificate   │
│  3. Run compliance tests with the compliance certificate        │
│  4. Request Production Certificate → get Production Certificate │
│                                                                 │
│  Save: certificate.pem, private.pem, secret key                 │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  FOR EVERY INVOICE                                              │
│                                                                 │
│  1. Create InvoiceData (number, date, type, PIH, ICV)           │
│  2. Create SellerData  (company name, VAT, address)             │
│  3. Create BuyerData   (customer name, VAT, address)            │
│  4. Create InvoiceLineData items (name, qty, price, tax%)       │
│  5. Call calculateTotals() on lines, then on invoice            │
│  6. Submit via zatcaManager.processInvoice()                    │
│     → Standard invoice? ZATCA clears it (real-time)             │
│     → Simplified invoice? ZATCA reports it                      │
│  7. Save the invoice_hash as PIH for the next invoice           │
└─────────────────────────────────────────────────────────────────┘

Step 1: Generate CSR & Private Key

Do this once when setting up your system with ZATCA.

import { CertificateBuilder } from '@khaledhajsalem/zatca-node';

const builder = new CertificateBuilder();
builder.setOrganizationIdentifier('300000000000003') // 15 digits, starts & ends with 3
    .setSerialNumber('MySolution', 'Model1', 'SN001')
    .setCommonName('Your Company Name')
    .setCountryName('SA')
    .setOrganizationName('Your Company Name')
    .setOrganizationalUnitName('IT Department')
    .setAddress('123 Main Street, Riyadh, Saudi Arabia')
    .setInvoiceType(1100) // 4 digits: [Standard][Simplified][future][future] — 1100 = both standard & simplified
    .setProduction(false) // false = sandbox/simulation, true = production
    .setBusinessCategory('Legal Entity');

builder.generateAndSave('storage/certificate.csr', 'storage/private.pem');

What happens next:

  1. Log in to the ZATCA Fatoora Portal
  2. Register your device — ZATCA gives you an OTP
  3. Use the OTP to request a compliance certificate (see Step 2)

Step 2: Get Your Certificate from ZATCA

import { ZatcaAPIService } from '@khaledhajsalem/zatca-node';
import * as fs from 'fs';

// Initialize API service
const apiService = new ZatcaAPIService('sandbox'); // 'sandbox', 'simulation', or 'production'

// ── Step 2a: Request Compliance Certificate ──
const csr = fs.readFileSync('storage/certificate.csr', 'utf8');
const otp = '123456'; // OTP from ZATCA Fatoora Portal

const complianceResult = await apiService.requestComplianceCertificate(csr, otp);

// Save the compliance certificate
fs.writeFileSync('storage/certificate.pem', complianceResult.getCertificate());
const complianceSecret    = complianceResult.getSecret();     // Save this — it's your API secret
const complianceRequestId = complianceResult.getRequestId();  // Need this for production certificate

// ── Step 2b: Run Compliance Tests ──
// Submit test invoices using the compliance certificate (see Step 3)
// Once all tests pass...

// ── Step 2c: Request Production Certificate ──
const complianceCert = fs.readFileSync('storage/certificate.pem', 'utf8');

const productionResult = await apiService.requestProductionCertificate(
    complianceCert,          // Compliance certificate
    complianceSecret,        // Compliance secret
    complianceRequestId      // Request ID from step 2a
);

// Save the production certificate — use this for all real invoices
fs.writeFileSync('storage/certificate.pem', productionResult.getCertificate());
const productionSecret = productionResult.getSecret(); // Save this as your new API secret

Step 3: Create & Submit an Invoice

This is the main workflow you'll use for every invoice.

Simplified Tax Invoice (B2C)

import { ZatcaManager, InvoiceData, SellerData, BuyerData, InvoiceLineData } from '@khaledhajsalem/zatca-node';

// ── 1. Initialize ZatcaManager ──
const zatcaManager = new ZatcaManager({
    environment:      'sandbox',                              // 'sandbox', 'simulation', or 'production'
    certificate_path: __dirname + '/storage/certificate.pem', // Path to your certificate file
    private_key_path: __dirname + '/storage/private.pem',     // Path to your private key file
    secret:           'CkYsEXfV8c1gFHAtFWoZv73pGMvh/Qyo4LzKM2h/8Hg=', // API secret from ZATCA
});

// ── 2. Create Invoice Data ──
const invoiceData = new InvoiceData();
invoiceData
    .setInvoiceNumber('INV-001')           // Your invoice number
    .simplified()                          // B2C invoice (reporting) — or .standard() for B2B (clearance)
    .taxInvoice()                          // Tax Invoice (388) — or .creditNote(), .debitNote(), .prepaymentInvoice()
    .setIssueDate('2025-01-15')            // Issue date in Y-m-d format
    .setIssueTime('10:30:00')              // Issue time in H:i:s format
    .setDueDate('2025-02-15')              // Due date in Y-m-d format
    .setCurrencyCode('SAR')                // Currency code (ISO 4217)
    .setDocumentCurrencyCode('SAR')        // Document currency (usually same as above)
    .setTaxCurrencyCode('SAR')             // Tax currency (usually same as above)
    .setInvoiceCounter('1')                // ICV: sequential counter, must increment per invoice
    .setPreviousInvoiceHash('MA==');        // PIH: 'MA==' for first invoice, then use hash from previous invoice

// ── 3. Create Seller Data ──
const seller = new SellerData();
seller.setRegistrationName('Your Company Name')  // Company legal name
    .setVatNumber('399999999900003')               // VAT number (15 digits)
    .setPartyIdentification('1010203020')          // Identification value (e.g., CRN number)
    .setPartyIdentificationId('CRN')               // Identification type — see "Party Identification Schemes" above
    .setStreetName('Main Street')                  // Street name
    .setBuildingNumber('1234')                     // Building number
    .setCityName('Riyadh')                         // City
    .setPostalZone('12345')                        // Postal/ZIP code
    .setCountryCode('SA')                          // 2-letter country code
    .setPlotIdentification('PLOT-001')             // Plot identification (optional)
    .setCitySubdivisionName('District 1');         // District name (optional)

invoiceData.setSeller(seller);

// ── 4. Create Buyer Data ──
const buyer = new BuyerData();
buyer.setRegistrationName('Customer Company')
    .setVatNumber('300000000000003')               // VAT number (optional for simplified invoices)
    .setPartyIdentification('1010203030')
    .setPartyIdentificationId('CRN')
    .setStreetName('Customer Street')
    .setBuildingNumber('4567')
    .setCityName('Jeddah')
    .setPostalZone('54321')
    .setCountryCode('SA');

invoiceData.setBuyer(buyer);

// ── 5. Add Line Items ──
const line1 = new InvoiceLineData();
line1.setId(1)                                    // Line number (sequential)
    .setItemName('Product 1')                      // Item name
    .setDescription('High-quality product')        // Description (optional)
    .setQuantity(2)                                // Quantity
    .setUnitPrice(100.00)                          // Unit price (tax-exclusive)
    .setTaxPercent(15.0)                           // VAT percentage
    .calculateTotals();                            // Auto-calculates: lineExtension, taxAmount, taxExclusive, taxInclusive

const line2 = new InvoiceLineData();
line2.setId(2)
    .setItemName('Product 2')
    .setQuantity(1)
    .setUnitPrice(50.00)
    .setTaxPercent(15.0)
    .calculateTotals();

invoiceData.addLine(line1);
invoiceData.addLine(line2);

// ── 6. Calculate Invoice Totals ──
invoiceData.calculateTotals();                    // Sums all line items into invoice-level totals

// ── 7. Submit to ZATCA ──
const result = await zatcaManager.processInvoice(invoiceData);

// ── 8. Use the Result ──
console.log(result.uuid);                               // Invoice UUID (generated automatically)
console.log(result.invoice_hash);                       // Invoice hash — SAVE THIS as PIH for your next invoice
console.log(result.qr_code);                            // Base64-encoded QR code
console.log(result.xml);                                // Signed XML string
console.log(result.is_clearance_required);              // true for standard, false for simplified

// API response from ZATCA:
console.log(result.response['validationResults']);       // { status: 'PASS' | 'WARNING' | 'ERROR', ... }
console.log(result.response['reportingStatus']);         // For simplified: 'REPORTED'
console.log(result.response['clearanceStatus']);         // For standard: 'CLEARED'

Standard Tax Invoice (B2B)

The only difference from simplified is the invoice type — everything else is the same:

const invoiceData = new InvoiceData();
invoiceData
    .setInvoiceNumber('INV-002')
    .standard()                                    // ← This is the only change (B2B, requires clearance)
    .taxInvoice()
    .setIssueDate('2025-01-15')
    .setIssueTime('10:30:00')
    .setCurrencyCode('SAR')
    .setDocumentCurrencyCode('SAR')
    .setTaxCurrencyCode('SAR')
    .setInvoiceCounter('2')                        // ICV: second invoice
    .setPreviousInvoiceHash(previousInvoiceHash);  // PIH: hash from INV-001

// ... seller, buyer, lines same as above ...

const result = await zatcaManager.processInvoice(invoiceData);

// For standard invoices, ZATCA returns a cleared XML:
if (result.response['clearanceStatus'] === 'CLEARED') {
    const clearedXml = result.xml; // Use this XML (not your original)
}

Invoice Types

Summary

| Type | Code | Name | Clearance? | Method | |------|------|------|-----------|--------| | Standard Tax Invoice | 388 | 0100000 | Yes (real-time) | .standard().taxInvoice() | | Simplified Tax Invoice | 388 | 0200000 | No (report within 24h) | .simplified().taxInvoice() | | Standard Credit Note | 381 | 0100000 | Yes | .standard().creditNote() | | Simplified Credit Note | 381 | 0200000 | No | .simplified().creditNote() | | Standard Debit Note | 383 | 0100000 | Yes | .standard().debitNote() | | Simplified Debit Note | 383 | 0200000 | No | .simplified().debitNote() | | Prepayment Invoice | 386 | — | Depends on standard/simplified | .prepaymentInvoice() |


Credit & Debit Notes

Credit and debit notes must reference the original invoice using addBillingReference(). Payment means are optional but recommended.

// ── Credit Note (returns/refunds) ──
const creditNote = new InvoiceData();
creditNote
    .setInvoiceNumber('CN-001')
    .simplified()                                  // or .standard()
    .creditNote()                                  // Type code 381
    .setIssueDate('2025-01-20')
    .setIssueTime('14:00:00')
    .setCurrencyCode('SAR')
    .setDocumentCurrencyCode('SAR')
    .setTaxCurrencyCode('SAR')
    .setInvoiceCounter('3')
    .setPreviousInvoiceHash(previousHash)

    // REQUIRED: Reference to the original invoice
    .addBillingReference({
        id:   'INV-001',                            // Original invoice number
        uuid: '63decc4e-cc4d-4e3b-878c-b772560bb5f1', // Original invoice UUID
    })

    // OPTIONAL: Payment means (reason for the note)
    .addPaymentMeans({
        code:             '10',                      // Payment method code
        instruction_note: 'Returns',                 // Reason: Returns, Correction, Cancellation, etc.
    });

// ... set seller, buyer, lines, calculateTotals(), then submit
const result = await zatcaManager.processInvoice(creditNote);
// ── Debit Note (additional charges) ──
const debitNote = new InvoiceData();
debitNote
    .setInvoiceNumber('DN-001')
    .standard()                                    // or .simplified()
    .debitNote()                                   // Type code 383
    .setIssueDate('2025-01-20')
    .setIssueTime('14:00:00')
    .setCurrencyCode('SAR')
    .setDocumentCurrencyCode('SAR')
    .setTaxCurrencyCode('SAR')
    .setInvoiceCounter('4')
    .setPreviousInvoiceHash(previousHash)
    .addBillingReference({
        id:   'INV-001',
        uuid: '63decc4e-cc4d-4e3b-878c-b772560bb5f1',
    })
    .addPaymentMeans({
        code:             '10',
        instruction_note: 'Addition',
    });

// ... set seller, buyer, lines, calculateTotals(), then submit
const result = await zatcaManager.processInvoice(debitNote);

Data Reference

InvoiceData — All Setters

| Method | Type | Required | Description | |--------|------|----------|-------------| | setInvoiceNumber(num) | string | Yes | Your invoice number | | standard() | — | Yes* | Set as Standard (B2B). One of standard/simplified required | | simplified() | — | Yes | Set as Simplified (B2C) | | taxInvoice() | — | Yes* | Tax Invoice (388). *One of tax/credit/debit/prepayment required | | creditNote() | — | — | Credit Note (381) | | debitNote() | — | — | Debit Note (383) | | prepaymentInvoice() | — | — | Prepayment (386) | | setIssueDate(date) | string | Yes | Format: Y-m-d | | setIssueTime(time) | string | Yes | Format: H:i:s | | setDueDate(date) | string | No | Format: Y-m-d | | setCurrencyCode(code) | string | Yes | ISO 4217 (e.g., SAR) | | setDocumentCurrencyCode(code) | string | Yes | Usually same as currency code | | setTaxCurrencyCode(code) | string | Yes | Usually same as currency code | | setInvoiceCounter(icv) | string | Yes | Sequential counter starting at 1 | | setPreviousInvoiceHash(pih) | string | Yes | 'MA==' for first invoice | | setSeller(seller) | SellerData | Yes | Seller information | | setBuyer(buyer) | BuyerData | Yes | Buyer information | | addLine(line) | InvoiceLineData | Yes | At least one line required | | calculateTotals() | — | Yes | Call after adding all lines | | addBillingReference(ref) | object | For CN/DN | Keys: id, uuid | | addPaymentMeans(pm) | object | No | Keys: code, instruction_note | | addAllowance(allowance) | object | No | Document-level discount | | addCharge(charge) | object | No | Document-level charge |

SellerData — All Setters

| Method | Type | Required | Description | |--------|------|----------|-------------| | setRegistrationName(name) | string | Yes | Company legal name | | setVatNumber(vat) | string | Yes | 15-digit VAT number | | setPartyIdentification(value) | string | Yes | ID value (e.g., CRN number) | | setPartyIdentificationId(scheme) | string | Yes | Scheme: CRN, VAT, TIN, etc. | | setStreetName(street) | string | Yes | Street name | | setBuildingNumber(num) | string | Yes | Building number | | setCityName(city) | string | Yes | City name | | setPostalZone(zip) | string | Yes | Postal/ZIP code | | setCountryCode(code) | string | Yes | 2-letter code (e.g., SA) | | setPlotIdentification(plot) | string | No | Plot identification | | setCitySubdivisionName(district) | string | No | District/subdivision name |

BuyerData — All Setters

Same methods as SellerData. For simplified invoices, setVatNumber() is optional.

InvoiceLineData — All Setters

| Method | Type | Required | Description | |--------|------|----------|-------------| | setId(id) | number | Yes | Line number (sequential: 1, 2, 3...) | | setItemName(name) | string | Yes | Item name | | setDescription(desc) | string | No | Item description | | setQuantity(qty) | number | Yes | Quantity | | setUnitPrice(price) | number | Yes | Unit price (tax-exclusive) | | setTaxPercent(pct) | number | Yes | VAT percentage (e.g., 15.0) | | setUnitCode(code) | string | No | Unit code (default: EA) | | setItemCode(code) | string | No | Item code | | calculateTotals() | — | Yes | Auto-calculates all amounts from qty × price × tax% | | setAllowanceAmount(amt) | number | No | Line-level discount (set before calculateTotals) | | setChargeAmount(amt) | number | No | Line-level charge (set before calculateTotals) |

Tip: Call calculateTotals() on each line item, then call calculateTotals() on the invoice. This auto-fills lineExtensionAmount, taxAmount, taxExclusiveAmount, taxInclusiveAmount, and all invoice-level totals.

Previous Invoice Hash (PIH) Chain

Every invoice references the hash of the previous invoice to create a tamper-proof chain:

// Invoice 1 (first invoice — no previous)
invoice1.setPreviousInvoiceHash('MA==');           // base64('0')
const result1 = await zatcaManager.processInvoice(invoice1);
const hash1 = result1.invoice_hash;                // Save this!

// Invoice 2 (references invoice 1)
invoice2.setPreviousInvoiceHash(hash1);
const result2 = await zatcaManager.processInvoice(invoice2);
const hash2 = result2.invoice_hash;                // Save this!

// Invoice 3 (references invoice 2)
invoice3.setPreviousInvoiceHash(hash2);
// ... and so on

Error Handling

import { ZatcaException, CertificateBuilderException, ZatcaApiException } from '@khaledhajsalem/zatca-node';

try {
    const result = await zatcaManager.processInvoice(invoiceData);
} catch (error) {
    if (error instanceof CertificateBuilderException) {
        // Certificate generation errors
        console.error('Certificate error:', error.message);
        console.error('Details:', error.getContext());
    } else if (error instanceof ZatcaApiException) {
        // ZATCA API errors (network, validation, auth)
        console.error('API error:', error.message);
        console.error('Details:', error.getContext());
    } else if (error instanceof ZatcaException) {
        // General package errors (missing config, file not found, etc.)
        console.error('Error:', error.message);
        console.error('Details:', error.getContext());
    }
}

All exceptions extend ZatcaException and provide a getContext() method with structured error details.


Package Structure

zatca-node/
├── src/
│   ├── data/                          # Data transfer objects
│   │   ├── InvoiceData.ts             # Invoice header, totals, references
│   │   ├── SellerData.ts              # Seller name, VAT, address
│   │   ├── BuyerData.ts               # Buyer name, VAT, address
│   │   └── InvoiceLineData.ts         # Line item: name, qty, price, tax
│   ├── exceptions/                    # Exception classes
│   │   ├── ZatcaException.ts          # Base exception (all others extend this)
│   │   ├── CertificateBuilderException.ts
│   │   ├── ZatcaApiException.ts
│   │   └── ZatcaStorageException.ts
│   ├── services/                      # External services
│   │   ├── ZatcaAPIService.ts         # ZATCA API client (clearance, reporting, compliance)
│   │   └── Storage.ts                 # File storage helper
│   ├── support/                       # Internal support classes
│   │   ├── Certificate.ts             # Certificate loading & hashing
│   │   ├── CertificateBuilder.ts      # CSR & private key generation
│   │   ├── InvoiceExtension.ts        # UBL XML extension handling
│   │   ├── InvoiceSignatureBuilder.ts # XMLDsig signature builder
│   │   ├── InvoiceSigner.ts           # Signs XML, generates QR & hash
│   │   ├── QRCodeGenerator.ts         # TLV-encoded QR code generation
│   │   └── qrcode-tags/              # Individual QR code tag classes
│   ├── ZatcaInvoice.ts                # UBL 2.1 XML generator
│   ├── ZatcaManager.ts                # Main orchestrator (the class you use)
│   └── index.ts                       # Package exports
├── examples/
│   ├── basic-usage.ts                 # Complete working example
│   ├── certificate-generation.ts      # CSR generation example
│   └── invoice-types.ts               # Standard, simplified, credit, debit, prepayment
├── tests/
│   └── ZatcaInvoice.test.ts
├── package.json
├── tsconfig.json
└── README.md

Testing

npm test

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests for new functionality
  5. Ensure all tests pass
  6. Submit a pull request

Acknowledgments

This package is a Node.js/TypeScript port of zatca-php.

License

This package is open-sourced software licensed under the MIT license.

Support