Integrating the TTN API into Your System: Developer Technical Guide

Noqta Team
By Noqta Team ·

Loading the Text to Speech Audio Player...

This guide is aimed at developers and technical architects responsible for integrating TTN electronic invoicing into an existing application. We cover the complete architecture, three integration strategies, and provide concrete Node.js/TypeScript code examples for each key step: TEIF XML generation, TUNTRUST signing, API submission, and response handling.

This tutorial is the seventh episode of our Electronic Invoicing in Tunisia series. It assumes you have already read Episode 5: TEIF Format and Technical Specifications and that your company is registered on the El Fatoora platform.


Overall System Architecture

Before writing a single line of code, it is essential to understand how your system interacts with TTN and DGI infrastructure.

┌─────────────────────────────────────────────────┐
│              YOUR SYSTEM                         │
│  ┌──────────┐   ┌──────────┐   ┌─────────────┐  │
│  │  ERP/CRM │──►│  TEIF    │──►│  TTN Module │  │
│  │  (data)  │   │Generator │   │  (signing   │  │
│  └──────────┘   └──────────┘   │  + submit)  │  │
│                                 └──────┬────────┘  │
└────────────────────────────────────────│───────────┘
                                         │ HTTPS/REST
                                         ▼
                          ┌──────────────────────────┐
                          │   TTN API (El Fatoora)    │
                          │  api.elfatoora.tn         │
                          │  ┌──────────────────────┐ │
                          │  │ TEIF Validation       │ │
                          │  │ Signature Verification│ │
                          │  │ TTN Timestamping      │ │
                          │  │ Archiving             │ │
                          │  └──────────┬───────────┘ │
                          └─────────────│──────────────┘
                                        │ Automatic transmission
                                        ▼
                          ┌──────────────────────────┐
                          │   DGI (Tax Authority)     │
                          │  VAT Declaration          │
                          │  Tax monitoring           │
                          └──────────────────────────┘

Key interaction points:

  • Your system generates the TEIF XML from your business data
  • Digital signing with the TUNTRUST certificate is performed client-side (your server)
  • The TTN API validates the format, the signature, and archives the invoice
  • TTN automatically transmits data to the DGI — you do not need to interact directly with the DGI for each invoice

Setup and Authentication

Technical Prerequisites

Before starting the integration, make sure you have:

  • Node.js v18+ (v20 recommended)
  • PKCS#12 certificate (.p12) issued by TUNTRUST, with its password
  • TTN API credentials (client_id and client_secret) obtained after registering in EDI mode
  • TTN sandbox access: https://api-sandbox.elfatoora.tn

Installing Dependencies

npm install xmlbuilder2 node-forge axios dotenv qrcode
npm install --save-dev @types/node typescript tsx
  • xmlbuilder2: XML document construction
  • node-forge: PKCS#12 certificate handling and XML signing
  • axios: HTTP client for the TTN API
  • qrcode: QR code generation

Environment Variables

# .env
TTN_API_BASE_URL=https://api-sandbox.elfatoora.tn
TTN_CLIENT_ID=your_ttn_client_id
TTN_CLIENT_SECRET=your_ttn_client_secret
 
# TUNTRUST Certificate
TUNTRUST_CERT_PATH=/path/to/your-certificate.p12
TUNTRUST_CERT_PASSWORD=your_certificate_password
 
# Supplier info
SUPPLIER_TAX_ID=12345678A000000
SUPPLIER_NAME=Your Company SARL

Option 1: Direct TTN API Integration

Direct integration is the most flexible but also the most complex approach to implement. You manage the complete invoice lifecycle: XML generation, signing, submission, and response handling.

Step 1: OAuth2 Authentication

The TTN API uses OAuth2 with the client_credentials flow:

// src/ttn/auth.ts
import axios from "axios";
 
interface TTNTokenResponse {
  access_token: string;
  token_type: string;
  expires_in: number;
}
 
let cachedToken: { token: string; expiresAt: number } | null = null;
 
export async function getTTNAccessToken(): Promise<string> {
  // Reuse the token if still valid (with a 60s margin)
  if (cachedToken && Date.now() < cachedToken.expiresAt - 60000) {
    return cachedToken.token;
  }
 
  const response = await axios.post<TTNTokenResponse>(
    `${process.env.TTN_API_BASE_URL}/api/v1/auth/token`,
    new URLSearchParams({
      grant_type: "client_credentials",
      client_id: process.env.TTN_CLIENT_ID!,
      client_secret: process.env.TTN_CLIENT_SECRET!,
      scope: "invoices:write invoices:read",
    }),
    { headers: { "Content-Type": "application/x-www-form-urlencoded" } }
  );
 
  cachedToken = {
    token: response.data.access_token,
    expiresAt: Date.now() + response.data.expires_in * 1000,
  };
 
  return cachedToken.token;
}

Step 2: TEIF XML Generation

// src/ttn/teif-generator.ts
import { create } from "xmlbuilder2";
 
export interface InvoiceLine {
  id: number;
  description: string;
  quantity: number;
  unitCode: string;
  unitPrice: number; // excl. VAT
  vatRate: number;   // e.g. 19 for 19%
}
 
export interface InvoiceData {
  invoiceNumber: string;
  issueDate: string;       // YYYY-MM-DD
  dueDate: string;
  supplierTaxId: string;
  supplierName: string;
  supplierAddress: string;
  supplierCity: string;
  supplierPostalCode: string;
  customerTaxId: string;
  customerName: string;
  customerAddress: string;
  customerCity: string;
  customerPostalCode: string;
  lines: InvoiceLine[];
  notes?: string;
}
 
function formatAmount(amount: number): string {
  return amount.toFixed(3);
}
 
export function generateTEIF(invoice: InvoiceData): string {
  // Calculate totals
  const lineCalculations = invoice.lines.map((line) => {
    const lineTotal = line.quantity * line.unitPrice;
    const vatAmount = (lineTotal * line.vatRate) / 100;
    return { ...line, lineTotal, vatAmount };
  });
 
  const subtotalHT = lineCalculations.reduce((s, l) => s + l.lineTotal, 0);
  const totalVAT = lineCalculations.reduce((s, l) => s + l.vatAmount, 0);
  const totalTTC = subtotalHT + totalVAT;
 
  // Build the XML document
  const doc = create({ version: "1.0", encoding: "UTF-8" })
    .ele("Invoice", {
      xmlns: "urn:tn:gov:dgi:teif:1.8",
      "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
      "xsi:schemaLocation":
        "urn:tn:gov:dgi:teif:1.8 TEIF_v1.8.7.xsd",
    });
 
  // Header
  const header = doc.ele("Header");
  header.ele("InvoiceID").txt(invoice.invoiceNumber);
  header.ele("IssueDate").txt(invoice.issueDate);
  header.ele("IssueTime").txt(new Date().toTimeString().split(" ")[0]);
  header.ele("InvoiceTypeCode").txt("380");
  header.ele("DocumentCurrencyCode").txt("TND");
  header.ele("TaxCurrencyCode").txt("TND");
  header.ele("DueDate").txt(invoice.dueDate);
  if (invoice.notes) {
    header.ele("Note").txt(invoice.notes);
  }
 
  // Parties
  const parties = doc.ele("Parties");
 
  const supplier = parties.ele("Supplier");
  supplier.ele("PartyIdentification").ele("ID", { schemeID: "TN_MF" })
    .txt(invoice.supplierTaxId);
  supplier.ele("PartyName").ele("Name").txt(invoice.supplierName);
  const supplierAddr = supplier.ele("PostalAddress");
  supplierAddr.ele("StreetName").txt(invoice.supplierAddress);
  supplierAddr.ele("CityName").txt(invoice.supplierCity);
  supplierAddr.ele("PostalZone").txt(invoice.supplierPostalCode);
  supplierAddr.ele("Country").ele("IdentificationCode").txt("TN");
 
  const customer = parties.ele("Customer");
  customer.ele("PartyIdentification").ele("ID", { schemeID: "TN_MF" })
    .txt(invoice.customerTaxId);
  customer.ele("PartyName").ele("Name").txt(invoice.customerName);
  const customerAddr = customer.ele("PostalAddress");
  customerAddr.ele("StreetName").txt(invoice.customerAddress);
  customerAddr.ele("CityName").txt(invoice.customerCity);
  customerAddr.ele("PostalZone").txt(invoice.customerPostalCode);
  customerAddr.ele("Country").ele("IdentificationCode").txt("TN");
 
  // Invoice lines
  const lines = doc.ele("InvoiceLines");
  for (const line of lineCalculations) {
    const invoiceLine = lines.ele("InvoiceLine");
    invoiceLine.ele("ID").txt(String(line.id));
    invoiceLine.ele("InvoicedQuantity", { unitCode: line.unitCode })
      .txt(String(line.quantity));
    invoiceLine.ele("LineExtensionAmount", { currencyID: "TND" })
      .txt(formatAmount(line.lineTotal));
 
    const item = invoiceLine.ele("Item");
    item.ele("Name").txt(line.description);
    const taxCat = item.ele("ClassifiedTaxCategory");
    taxCat.ele("ID").txt("S");
    taxCat.ele("Percent").txt(String(line.vatRate));
    taxCat.ele("TaxScheme").ele("ID").txt("TVA");
 
    const price = invoiceLine.ele("Price");
    price.ele("PriceAmount", { currencyID: "TND" })
      .txt(formatAmount(line.unitPrice));
    price.ele("BaseQuantity", { unitCode: line.unitCode }).txt("1");
  }
 
  // VAT totals
  const taxTotal = doc.ele("TaxTotal");
  taxTotal.ele("TaxAmount", { currencyID: "TND" }).txt(formatAmount(totalVAT));
  const taxSubtotal = taxTotal.ele("TaxSubtotal");
  taxSubtotal.ele("TaxableAmount", { currencyID: "TND" })
    .txt(formatAmount(subtotalHT));
  taxSubtotal.ele("TaxAmount", { currencyID: "TND" })
    .txt(formatAmount(totalVAT));
  const taxCategory = taxSubtotal.ele("TaxCategory");
  taxCategory.ele("ID").txt("S");
  taxCategory.ele("Percent").txt("19");
  taxCategory.ele("TaxScheme").ele("ID").txt("TVA");
 
  // Monetary totals
  const monetary = doc.ele("LegalMonetaryTotal");
  monetary.ele("LineExtensionAmount", { currencyID: "TND" })
    .txt(formatAmount(subtotalHT));
  monetary.ele("TaxExclusiveAmount", { currencyID: "TND" })
    .txt(formatAmount(subtotalHT));
  monetary.ele("TaxInclusiveAmount", { currencyID: "TND" })
    .txt(formatAmount(totalTTC));
  monetary.ele("PayableAmount", { currencyID: "TND" })
    .txt(formatAmount(totalTTC));
 
  return doc.end({ prettyPrint: true });
}

Step 3: Signing with the TUNTRUST Certificate

// src/ttn/signer.ts
import * as forge from "node-forge";
import * as fs from "fs";
import { create } from "xmlbuilder2";
 
interface SigningResult {
  signedXml: string;
  signatureValue: string;
  certificateBase64: string;
}
 
export function signTEIF(xmlContent: string): SigningResult {
  // Load the PKCS#12 certificate
  const p12Buffer = fs.readFileSync(process.env.TUNTRUST_CERT_PATH!);
  const p12Der = forge.util.createBuffer(p12Buffer.toString("binary"));
  const p12Asn1 = forge.asn1.fromDer(p12Der);
  const p12 = forge.pkcs12.pkcs12FromAsn1(
    p12Asn1,
    process.env.TUNTRUST_CERT_PASSWORD!
  );
 
  // Extract the private key and certificate
  const bags = p12.getBags({
    bagType: forge.pki.oids.pkcs8ShroudedKeyBag,
  });
  const keyBag = bags[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0];
  if (!keyBag?.key) throw new Error("Private key not found in PKCS#12");
  const privateKey = keyBag.key;
 
  const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
  const certBag = certBags[forge.pki.oids.certBag]?.[0];
  if (!certBag?.cert) throw new Error("Certificate not found in PKCS#12");
  const certificate = certBag.cert;
 
  // Canonicalize the XML (simplified C14N for this example)
  const xmlBuffer = Buffer.from(xmlContent, "utf8");
 
  // Compute the SHA-256 digest of the document
  const md = forge.md.sha256.create();
  md.update(xmlBuffer.toString("binary"));
  const digestValue = Buffer.from(md.digest().bytes(), "binary").toString(
    "base64"
  );
 
  // Sign the digest with the RSA private key
  const signatureMd = forge.md.sha256.create();
  signatureMd.update(xmlBuffer.toString("binary"));
  const signatureBytes = privateKey.sign(signatureMd);
  const signatureValue = Buffer.from(signatureBytes, "binary").toString(
    "base64"
  );
 
  // Encode the certificate in Base64
  const certDer = forge.asn1.toDer(forge.pki.certificateToAsn1(certificate));
  const certificateBase64 = Buffer.from(certDer.bytes(), "binary").toString(
    "base64"
  );
 
  // Build the XML Signature section and inject it into the document
  const signatureXml = `
  <Signature Id="TEIF-SIG-001" xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
      <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
        <DigestValue>${digestValue}</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>${signatureValue}</SignatureValue>
    <KeyInfo>
      <X509Data>
        <X509Certificate>${certificateBase64}</X509Certificate>
      </X509Data>
    </KeyInfo>
  </Signature>`;
 
  // Insert the signature before the closing </Invoice> tag
  const signedXml = xmlContent.replace(
    "</Invoice>",
    `${signatureXml}\n</Invoice>`
  );
 
  return { signedXml, signatureValue, certificateBase64 };
}

Step 4: QR Code Generation

// src/ttn/qrcode-generator.ts
import QRCode from "qrcode";
import crypto from "crypto";
 
export async function generateInvoiceQRCode(params: {
  invoiceId: string;
  supplierTaxId: string;
  issueDate: string;
  totalAmount: number;
  signatureValue: string;
}): Promise<string> {
  // Generate a short hash for the sig parameter (first 8 characters of SHA256 hash)
  const sigHash = crypto
    .createHash("sha256")
    .update(params.signatureValue)
    .digest("hex")
    .substring(0, 8);
 
  // Generate the TTN invoice identifier (hash of invoice ID + tax number)
  const iid = crypto
    .createHash("sha256")
    .update(`${params.invoiceId}:${params.supplierTaxId}`)
    .digest("hex")
    .substring(0, 12);
 
  // Format the date for the QR code (YYYYMMDDHHMMSS)
  const dt = params.issueDate.replace(/-/g, "") + "000000";
 
  const verifyUrl = new URL("https://verify.elfatoora.tn/v");
  verifyUrl.searchParams.set("iid", iid);
  verifyUrl.searchParams.set("sid", params.supplierTaxId);
  verifyUrl.searchParams.set("dt", dt);
  verifyUrl.searchParams.set("amt", params.totalAmount.toFixed(3));
  verifyUrl.searchParams.set("sig", sigHash);
 
  // Generate the QR code in Base64 (for embedding in XML or PDF)
  const qrCodeDataUrl = await QRCode.toDataURL(verifyUrl.toString(), {
    errorCorrectionLevel: "M",
    type: "image/png",
    width: 200,
    margin: 1,
  });
 
  // Return only the Base64 part (without the data:image/png;base64, prefix)
  return qrCodeDataUrl.replace("data:image/png;base64,", "");
}

Step 5: TTN API Submission and Response Handling

// src/ttn/client.ts
import axios, { AxiosError } from "axios";
import { getTTNAccessToken } from "./auth";
 
export interface TTNSubmitResponse {
  invoiceId: string;         // Internal TTN ID
  status: "SUBMITTED" | "VALID" | "INVALID";
  validationErrors?: Array<{
    code: string;
    field: string;
    message: string;
  }>;
  timestamp: string;
  acknowledgementUrl?: string;
}
 
export class TTNApiError extends Error {
  constructor(
    public readonly code: string,
    public readonly details: string,
    public readonly httpStatus: number
  ) {
    super(`TTN API Error [${code}]: ${details}`);
  }
}
 
export async function submitInvoice(
  signedXml: string
): Promise<TTNSubmitResponse> {
  const token = await getTTNAccessToken();
 
  try {
    const response = await axios.post<TTNSubmitResponse>(
      `${process.env.TTN_API_BASE_URL}/api/v1/invoices`,
      signedXml,
      {
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/xml",
          Accept: "application/json",
        },
        timeout: 30000, // 30 seconds
      }
    );
 
    return response.data;
  } catch (error) {
    if (error instanceof AxiosError && error.response) {
      const { status, data } = error.response;
 
      // TEIF validation errors (400)
      if (status === 400) {
        throw new TTNApiError(
          data.code || "VALIDATION_ERROR",
          JSON.stringify(data.errors || data.message),
          status
        );
      }
 
      // Authentication error (401)
      if (status === 401) {
        // Invalidate the token cache and retry
        throw new TTNApiError("AUTH_EXPIRED", "Token expired, please retry", 401);
      }
 
      // Rate limit exceeded (429)
      if (status === 429) {
        const retryAfter = error.response.headers["retry-after"] || "60";
        throw new TTNApiError(
          "RATE_LIMIT",
          `API quota exceeded. Retry in ${retryAfter}s`,
          429
        );
      }
 
      throw new TTNApiError(
        data.code || "API_ERROR",
        data.message || "Unknown API error",
        status
      );
    }
 
    throw error;
  }
}
 
export async function getInvoiceStatus(
  ttnInvoiceId: string
): Promise<{ status: string; updatedAt: string }> {
  const token = await getTTNAccessToken();
 
  const response = await axios.get(
    `${process.env.TTN_API_BASE_URL}/api/v1/invoices/${ttnInvoiceId}`,
    { headers: { Authorization: `Bearer ${token}` } }
  );
 
  return response.data;
}

Orchestration: the Complete Service

// src/ttn/invoice-service.ts
import { generateTEIF, InvoiceData } from "./teif-generator";
import { signTEIF } from "./signer";
import { generateInvoiceQRCode } from "./qrcode-generator";
import { submitInvoice, TTNApiError } from "./client";
 
export interface InvoiceSubmissionResult {
  ttnInvoiceId: string;
  status: string;
  submittedAt: string;
  signatureValue: string;
  qrCodeBase64: string;
}
 
export async function createAndSubmitInvoice(
  invoiceData: InvoiceData
): Promise<InvoiceSubmissionResult> {
  console.log(`[TTN] Generating invoice ${invoiceData.invoiceNumber}...`);
 
  // 1. Generate the TEIF XML
  const xmlContent = generateTEIF(invoiceData);
 
  // 2. Sign the document
  console.log("[TTN] Signing the XML document...");
  const { signedXml, signatureValue } = signTEIF(xmlContent);
 
  // 3. Calculate the total amount including tax for the QR code
  const totalTTC = invoiceData.lines.reduce((sum, line) => {
    const lineTotal = line.quantity * line.unitPrice;
    return sum + lineTotal + (lineTotal * line.vatRate) / 100;
  }, 0);
 
  // 4. Generate the QR code
  console.log("[TTN] Generating QR code...");
  const qrCodeBase64 = await generateInvoiceQRCode({
    invoiceId: invoiceData.invoiceNumber,
    supplierTaxId: invoiceData.supplierTaxId,
    issueDate: invoiceData.issueDate,
    totalAmount: totalTTC,
    signatureValue,
  });
 
  // 5. Submit to the TTN API with automatic retry
  let lastError: Error | null = null;
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      console.log(`[TTN] Submitting to API (attempt ${attempt}/3)...`);
      const response = await submitInvoice(signedXml);
 
      console.log(
        `[TTN] Invoice submitted successfully. TTN ID: ${response.invoiceId}`
      );
 
      return {
        ttnInvoiceId: response.invoiceId,
        status: response.status,
        submittedAt: response.timestamp,
        signatureValue,
        qrCodeBase64,
      };
    } catch (error) {
      if (error instanceof TTNApiError) {
        // Non-recoverable errors — do not retry
        if (error.httpStatus === 400 || error.httpStatus === 401) {
          throw error;
        }
        // 429 or 5xx errors — wait and retry
        lastError = error;
        const delay = attempt * 2000; // 2s, 4s, 6s
        console.warn(
          `[TTN] Error ${error.code}, retrying in ${delay}ms...`
        );
        await new Promise((resolve) => setTimeout(resolve, delay));
      } else {
        throw error;
      }
    }
  }
 
  throw lastError || new Error("Submission failed after 3 attempts");
}

Option 2: NGSign Middleware API

If you do not want to manage signing cryptography directly, TTN offers an intermediary service called NGSign API. This middleware handles the digital signature and significantly simplifies the integration.

Advantages:

  • No local management of the PKCS#12 certificate
  • Automatic updates when signing standards change
  • Managed SaaS service by TTN

NGSign flow:

Your system
    │ (unsigned TEIF XML)
    ▼
NGSign API (TTN)
    │ (signing + timestamping)
    ▼
El Fatoora API (TTN)
    │ (validation + archiving)
    ▼
Response to your system
// Submission via NGSign (without local signature management)
async function submitViaNGSign(xmlContent: string): Promise<string> {
  const token = await getTTNAccessToken();
 
  const response = await axios.post(
    `${process.env.TTN_API_BASE_URL}/api/v1/ngsign/submit`,
    {
      xmlContent: Buffer.from(xmlContent).toString("base64"),
      certificateAlias: process.env.TUNTRUST_CERT_ALIAS, // Alias pre-configured in NGSign
      signingMode: "SERVER", // TTN signs with the certificate stored on their end
    },
    {
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
    }
  );
 
  return response.data.invoiceId;
}

Note: NGSign requires depositing your TUNTRUST certificate with TTN, which raises security questions (the private key leaves your control). Evaluate this trade-off according to your security policy.


Option 3: Fully Custom Development

For large enterprises with specific requirements (multi-entity, complex ERP integration, very high volumes), a custom architecture may be necessary.

Typical architecture:

ERP / Source system
    │ (invoice creation events)
    ▼
Message Queue (Redis / RabbitMQ)
    │
    ▼
TTN Worker Pool (Node.js / microservice)
    ├── TEIF Generator
    ├── Signing Module (HSM or PKCS#12)
    ├── TTN API Client
    └── Error Handler / Retry Manager
    │
    ▼
Database (PostgreSQL)
    ├── Invoice statuses
    ├── TTN acknowledgements
    └── Audit logs

This architecture enables processing thousands of invoices per day with maximum reliability, complete traceability, and automatic error recovery.


Testing with the TTN Sandbox

Before switching to production, you must validate your integration on the TTN test environment.

Sandbox Configuration

// src/config.ts
export const TTN_CONFIG = {
  baseUrl:
    process.env.NODE_ENV === "production"
      ? "https://api.elfatoora.tn"
      : "https://api-sandbox.elfatoora.tn",
  timeoutMs: 30000,
  maxRetries: 3,
};

Mandatory Test Scenarios

TTN requires validating at minimum the following scenarios in sandbox before production activation:

// src/tests/ttn-sandbox.test.ts
import { createAndSubmitInvoice } from "../ttn/invoice-service";
 
const baseInvoiceData = {
  supplierTaxId: process.env.SUPPLIER_TAX_ID!,
  supplierName: process.env.SUPPLIER_NAME!,
  supplierAddress: "Test Address",
  supplierCity: "Tunis",
  supplierPostalCode: "1000",
};
 
// Test 1: Standard invoice with 19% VAT
async function testStandardInvoice() {
  const result = await createAndSubmitInvoice({
    ...baseInvoiceData,
    invoiceNumber: `TEST-${Date.now()}`,
    issueDate: new Date().toISOString().split("T")[0],
    dueDate: new Date(Date.now() + 30 * 86400000).toISOString().split("T")[0],
    customerTaxId: "99999999A000TEST",
    customerName: "Sandbox Test Customer",
    customerAddress: "Test Customer Address",
    customerCity: "Sfax",
    customerPostalCode: "3000",
    lines: [
      {
        id: 1,
        description: "Test Service",
        quantity: 1,
        unitCode: "C62",
        unitPrice: 100.0,
        vatRate: 19,
      },
    ],
  });
 
  console.assert(result.status === "VALID", "Test invoice must be VALID");
  console.log("Test 1 passed:", result.ttnInvoiceId);
}
 
// Test 2: Credit note (avoir)
async function testCreditNote() {
  // Similar but with InvoiceTypeCode = "381"
  // ...
}
 
// Test 3: Multi-line invoice with mixed VAT rates
async function testMixedVATInvoice() {
  // Lines with 19% and 7% VAT
  // ...
}
 
// Run all tests
(async () => {
  await testStandardInvoice();
  await testCreditNote();
  await testMixedVATInvoice();
  console.log("All sandbox tests passed!");
})();

Production Deployment Checklist

Before going live, validate every point on this list:

Security:

  • The TUNTRUST certificate is stored in a secure vault (AWS Secrets Manager, HashiCorp Vault, or equivalent)
  • The certificate password is never in plaintext in source code or logs
  • TTN API credentials are in environment variables, outside the Git repository
  • Access to the invoicing service is protected by internal authentication

Technical:

  • All sandbox test scenarios passed with VALID status
  • Error handling covers codes 400, 401, 429, 500, 503
  • Retry mechanism with exponential backoff is implemented
  • Logs include the TTN ID for each submitted invoice
  • The database retains the status and acknowledgement for each invoice

Legal and compliance:

  • Production TTN access has been activated by TTN after test validation
  • The DGI adherence declaration has been filed
  • The archiving strategy (minimum 5 years) is in place

Operational:

  • Alerts are configured for submission errors
  • A monitoring dashboard for invoice tracking is available to the accounting department
  • The remediation procedure in case of error is documented

Series Navigation

  • Previous episode: Integration into your accounting software (coming soon)
  • Next episode: Archiving and post-invoicing legal obligations (coming soon)

To revisit the technical fundamentals, see Episode 5: TEIF Format and Technical Specifications.


Does your company want to integrate TTN electronic invoicing without mobilizing your internal development resources? Noqta.tn offers a turnkey solution — from analyzing your existing system to production deployment, including training your teams. We have supported dozens of Tunisian companies through this transition.

Contact our technical team for a free diagnostic of your system and a project estimate.


Want to read more tutorials? Check out our latest tutorial on Build Your Own Code Interpreter with Dynamic Tool Generation.

Discuss Your Project with Us

We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.

Let's find the best solutions for your needs.

Related Articles