All Posts
November 20, 20244 min read

Integrating JoFotara: Jordan's National E-Invoicing System

A practical guide to integrating with Jordan's JoFotara e-invoicing system — XML generation, digital signing, QR codes, and the gotchas nobody warns you about.

NestJSJordanE-InvoicingIntegration

What is JoFotara?

JoFotara (جو فوترة) is Jordan's national electronic invoicing system, mandated by the Income and Sales Tax Department (ISTD). If you're building any invoicing or POS software for the Jordanian market, you must integrate with it.

The system requires businesses to submit invoices electronically in a specific XML format, digitally signed, with a QR code on every printed invoice. Sounds straightforward — but the documentation is sparse and the edge cases are many.

The Integration Architecture

I built this as a standalone NestJS module that can be dropped into any project:

@Module({
  providers: [
    JoFotaraService,
    XmlBuilderService,
    SigningService,
    QrCodeService,
  ],
  exports: [JoFotaraService],
})
export class JoFotaraModule {}

The main service exposes a clean interface:

@Injectable()
export class JoFotaraService {
  async submitInvoice(invoice: Invoice): Promise<SubmissionResult> {
    // 1. Build XML document
    const xml = await this.xmlBuilder.build(invoice);

    // 2. Sign with X.509 certificate
    const signed = await this.signing.sign(xml);

    // 3. Submit to ISTD API
    const result = await this.submit(signed);

    // 4. Generate QR code for the printed invoice
    const qr = await this.qrCode.generate(result.uuid, invoice);

    return { ...result, qrCode: qr };
  }
}

XML Generation: The Hard Part

JoFotara uses UBL 2.1 (Universal Business Language) XML format. The structure is deeply nested and every field has specific requirements:

<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
  <cbc:ID>INV-2024-001</cbc:ID>
  <cbc:IssueDate>2024-11-20</cbc:IssueDate>
  <cbc:InvoiceTypeCode>388</cbc:InvoiceTypeCode>
  <cbc:DocumentCurrencyCode>JOD</cbc:DocumentCurrencyCode>

  <cac:AccountingSupplierParty>
    <!-- Seller TIN, name, address -->
  </cac:AccountingSupplierParty>

  <cac:TaxTotal>
    <cbc:TaxAmount currencyID="JOD">1.60</cbc:TaxAmount>
    <cac:TaxSubtotal>
      <cbc:TaxableAmount currencyID="JOD">10.00</cbc:TaxableAmount>
      <cbc:TaxAmount currencyID="JOD">1.60</cbc:TaxAmount>
      <cac:TaxCategory>
        <cbc:Percent>16</cbc:Percent>
      </cac:TaxCategory>
    </cac:TaxSubtotal>
  </cac:TaxTotal>
</Invoice>

Gotcha 1: Decimal Precision

JoFotara is very strict about decimal precision. All monetary amounts must have exactly 2 decimal places. Tax percentages must match exactly. I learned this after getting cryptic validation errors:

function formatAmount(amount: number): string {
  return amount.toFixed(2);
}

function calculateTax(subtotal: number, rate: number): {
  taxable: string;
  tax: string;
  total: string;
} {
  const taxAmount = subtotal * (rate / 100);
  return {
    taxable: formatAmount(subtotal),
    tax: formatAmount(taxAmount),
    total: formatAmount(subtotal + taxAmount),
  };
}

Gotcha 2: Invoice Type Codes

Different transaction types use different codes, and using the wrong one silently succeeds but causes problems later during audits:

  • 388
    — Standard invoice
  • 381
    — Credit note (refund)
  • 383
    — Debit note (adjustment)

Digital Signing

Every invoice must be signed with an X.509 certificate issued by the ISTD. The signing process uses XML-DSig:

@Injectable()
export class SigningService {
  private certificate: string;
  private privateKey: string;

  constructor() {
    this.certificate = fs.readFileSync(
      process.env.JOFOTARA_CERT_PATH!,
      'utf-8',
    );
    this.privateKey = fs.readFileSync(
      process.env.JOFOTARA_KEY_PATH!,
      'utf-8',
    );
  }

  async sign(xml: string): Promise<string> {
    const sig = new SignedXml();
    sig.addReference({
      xpath: '//*[local-name()="Invoice"]',
      digestAlgorithm: 'http://www.w3.org/2001/04/xmlenc#sha256',
      transforms: ['http://www.w3.org/2001/10/xml-exc-c14n#'],
    });
    sig.signingKey = this.privateKey;
    sig.computeSignature(xml);
    return sig.getSignedXml();
  }
}

Gotcha 3: Certificate Renewal

Certificates expire. Build monitoring for this from day one. We got burned when a certificate expired on a Friday evening and the POS couldn't submit invoices all weekend.

QR Code Generation

Every printed invoice must include a QR code containing a Base64-encoded TLV (Tag-Length-Value) structure:

@Injectable()
export class QrCodeService {
  async generate(uuid: string, invoice: Invoice): Promise<string> {
    const tlv = Buffer.concat([
      this.encodeTLV(1, invoice.sellerName),
      this.encodeTLV(2, invoice.sellerTin),
      this.encodeTLV(3, invoice.issueDate),
      this.encodeTLV(4, invoice.totalWithTax),
      this.encodeTLV(5, invoice.taxAmount),
    ]);

    const base64 = tlv.toString('base64');
    // Generate QR code image from base64 string
    return QRCode.toDataURL(base64);
  }

  private encodeTLV(tag: number, value: string): Buffer {
    const valueBuffer = Buffer.from(value, 'utf-8');
    return Buffer.concat([
      Buffer.from([tag]),
      Buffer.from([valueBuffer.length]),
      valueBuffer,
    ]);
  }
}

Retry Logic

The ISTD API is not the most reliable. Timeouts and 5xx errors happen. Build retry logic with exponential backoff:

async function submitWithRetry(
  xml: string,
  maxRetries = 3,
): Promise<ApiResponse> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await submitToApi(xml);
    } catch (err) {
      if (attempt === maxRetries - 1) throw err;
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise((r) => setTimeout(r, delay));
    }
  }
  throw new Error('Unreachable');
}

Key Takeaways

  1. Test against the sandbox religiously — The sandbox and production behave slightly differently. Test both.
  2. Store every request and response — When something goes wrong (and it will), having the raw XML helps enormously.
  3. Monitor certificate expiry — Set up alerts 30 days, 7 days, and 1 day before expiry.
  4. Handle Arabic text carefully — Seller names and addresses may contain Arabic. Ensure your XML encoding handles UTF-8 correctly.
  5. Build idempotency — If a submission times out, you don't know if it succeeded. Use invoice UUIDs to prevent duplicates.

This integration is now handling all invoicing for the MIC platform and Shghaf perfume store. If you're building for the Jordanian market and need help with JoFotara, feel free to reach out.