Intégration API TTN / El Fatoora avec votre ERP : Guide complet Odoo, SAP, WooCommerce et systèmes personnalisés
La facturation électronique en Tunisie n'est plus optionnelle. Avec l'extension aux prestataires de services (LdF 2026) et les sanctions pouvant atteindre 50 000 DT par exercice, l'intégration de votre système de gestion avec l'API TTN / El Fatoora est devenue une priorité opérationnelle.
Ce guide couvre les différents scénarios d'intégration — que vous utilisiez Odoo, SAP, un ERP personnalisé ou une plateforme e-commerce — avec des architectures détaillées, des exemples de code et les pièges à éviter.
Vue d'ensemble de l'API TTN
Architecture générale
L'intégration avec TTN suit un flux en 5 étapes :
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Votre ERP │────▶│ Middleware │────▶│ Signature │
│ (facturation)│ │ (validation │ │ TUNTRUST │
│ │ │ + TEIF) │ │ (ANCE) │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
┌──────────────┐ ┌───────▼───────┐
│ Réponse │◀────│ API TTN │
│ (statut, │ │ El Fatoora │
│ PDF, QR) │ │ │
└──────────────┘ └───────────────┘
Endpoints principaux
| Endpoint | Méthode | Description |
|---|---|---|
/api/v1/invoice/submit | POST | Soumettre une facture TEIF signée |
/api/v1/invoice/{id}/status | GET | Vérifier le statut d'une facture |
/api/v1/invoice/{id}/pdf | GET | Récupérer le PDF officiel (avec QR) |
/api/v1/company/register | POST | Inscrire une entreprise |
/api/v1/invoice/bulk | POST | Soumission en lot |
Authentification
L'API TTN utilise une authentification par certificat client (mutual TLS) :
// Exemple d'authentification avec certificat TUNTRUST
import https from 'https';
import fs from 'fs';
const agent = new https.Agent({
cert: fs.readFileSync('/path/to/tuntrust-cert.pem'),
key: fs.readFileSync('/path/to/private-key.pem'),
ca: fs.readFileSync('/path/to/ance-ca.pem'),
rejectUnauthorized: true
});
const response = await fetch('https://api.elfatoora.digital/api/v1/invoice/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/xml' },
body: teifXml,
agent
});Format TEIF (rappel)
Chaque facture doit être au format TEIF — un XML structuré avec des champs obligatoires :
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<InvoiceHeader>
<InvoiceNumber>FAC-2026-001234</InvoiceNumber>
<IssueDate>2026-03-31</IssueDate>
<InvoiceTypeCode>380</InvoiceTypeCode>
<CurrencyCode>TND</CurrencyCode>
</InvoiceHeader>
<SellerParty>
<TaxID>1234567A/M/000</TaxID>
<Name>Votre Entreprise SARL</Name>
<!-- ... -->
</SellerParty>
<BuyerParty>
<TaxID>7654321B/M/000</TaxID>
<Name>Client SA</Name>
</BuyerParty>
<InvoiceLines>
<Line>
<Description>Service de développement</Description>
<Quantity>1</Quantity>
<UnitPrice>5000.000</UnitPrice>
<TaxRate>19</TaxRate>
<LineTotal>5950.000</LineTotal>
</Line>
</InvoiceLines>
<TaxTotal>950.000</TaxTotal>
<InvoiceTotal>5950.000</InvoiceTotal>
<DigitalSignature><!-- Signature TUNTRUST --></DigitalSignature>
<QRCode><!-- Données QR code --></QRCode>
</Invoice>Scénario 1 : Intégration Odoo
Odoo est l'ERP le plus déployé en Tunisie pour les PME. Voici l'architecture recommandée :
Architecture
┌─────────────────────────────────────────────────────────┐
│ Odoo (15/16/17) │
│ ┌────────────┐ ┌────────────┐ ┌──────────────────┐ │
│ │ Module │ │ Facturation │ │ Module El Fatoora│ │
│ │ Comptabilité│─▶│ PDF standard│─▶│ (personnalisé) │ │
│ └────────────┘ └────────────┘ └────────┬─────────┘ │
│ │ │
└────────────────────────────────────────────┼────────────┘
│
┌─────────▼──────────┐
│ Middleware Noqta │
│ - Validation TEIF │
│ - Signature │
│ - Transmission TTN │
└─────────┬──────────┘
│
┌─────────▼──────────┐
│ API TTN El Fatoora │
└────────────────────┘
Module Odoo personnalisé
# odoo_elfatoora/models/account_move.py
from odoo import models, fields, api
import requests
import xmltodict
class AccountMove(models.Model):
_inherit = 'account.move'
elfatoora_status = fields.Selection([
('draft', 'Brouillon'),
('submitted', 'Soumise'),
('accepted', 'Acceptée'),
('rejected', 'Rejetée'),
], string='Statut El Fatoora', default='draft')
elfatoora_id = fields.Char('ID El Fatoora')
elfatoora_qr = fields.Binary('QR Code')
def action_submit_elfatoora(self):
"""Soumettre la facture à El Fatoora via le middleware"""
self.ensure_one()
# Générer le XML TEIF
teif_data = self._generate_teif_xml()
# Envoyer au middleware
response = requests.post(
'https://middleware.noqta.tn/api/v1/invoice/submit',
json={
'invoice_data': teif_data,
'company_tax_id': self.company_id.vat,
'odoo_invoice_id': self.id,
},
headers={'Authorization': f'Bearer {self.env["ir.config_parameter"].get_param("elfatoora.api_key")}'}
)
if response.status_code == 200:
result = response.json()
self.write({
'elfatoora_status': 'submitted',
'elfatoora_id': result['invoice_id'],
})
else:
raise UserError(f"Erreur El Fatoora: {response.text}")
def _generate_teif_xml(self):
"""Convertir la facture Odoo en format TEIF"""
lines = []
for line in self.invoice_line_ids:
lines.append({
'Description': line.name,
'Quantity': str(line.quantity),
'UnitPrice': f'{line.price_unit:.3f}',
'TaxRate': str(int(line.tax_ids[0].amount)) if line.tax_ids else '0',
'LineTotal': f'{line.price_total:.3f}',
})
return {
'InvoiceNumber': self.name,
'IssueDate': str(self.invoice_date),
'SellerTaxID': self.company_id.vat,
'BuyerTaxID': self.partner_id.vat or '',
'Lines': lines,
'TaxTotal': f'{self.amount_tax:.3f}',
'InvoiceTotal': f'{self.amount_total:.3f}',
}Pièges courants avec Odoo
- Format des montants : TTN exige 3 décimales pour les dinars (5000.000, pas 5000.00)
- Identifiant fiscal : Doit être au format exact
XXXXXXXXA/M/NNN - Séquences : Les numéros de facture Odoo doivent être continus (pas de trous)
- Notes de crédit : Doivent référencer la facture originale via
elfatoora_id - Multi-devises : TEIF n'accepte que TND. Les factures en EUR/USD doivent être converties
Scénario 2 : SAP, Sage et autres ERP
Architecture middleware
Pour les ERP propriétaires (SAP, Sage, Microsoft Dynamics), un middleware dédié est recommandé :
┌───────────┐ ┌───────────┐ ┌───────────┐
│ SAP B1 │ │ Sage X3 │ │ Dynamics │
│ │ │ │ │ 365 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
└───────────────┼───────────────┘
│
┌─────────▼──────────┐
│ API Middleware │
│ Noqta El Fatoora │
│ ─────────────────│
│ • Mapping ERP→TEIF│
│ • Validation │
│ • Signature │
│ • File d'attente │
│ • Retry & logs │
└─────────┬──────────┘
│
┌─────────▼──────────┐
│ API TTN El Fatoora│
└────────────────────┘
SAP : Integration via RFC/BAPI
// Middleware: SAP → TEIF mapping
function mapSAPInvoiceToTEIF(sapInvoice) {
return {
InvoiceHeader: {
InvoiceNumber: sapInvoice.BELNR,
IssueDate: formatSAPDate(sapInvoice.BLDAT),
CurrencyCode: 'TND',
},
SellerParty: {
TaxID: sapInvoice.STCD1, // Matricule fiscal
Name: sapInvoice.BUTXT,
},
BuyerParty: {
TaxID: sapInvoice.KUNNR_STCD1,
Name: sapInvoice.NAME1,
},
Lines: sapInvoice.ITEMS.map(item => ({
Description: item.ARKTX,
Quantity: item.MENGE,
UnitPrice: item.NETPR,
TaxRate: getTVARate(item.MWSKZ),
LineTotal: item.NETWR,
})),
TaxTotal: sapInvoice.MWSBK,
InvoiceTotal: sapInvoice.WRBTR,
};
}Sage : Export CSV → Middleware
# Middleware Python pour Sage
import csv
from datetime import datetime
def process_sage_export(csv_path: str) -> list[dict]:
"""Convertir l'export CSV Sage en données TEIF"""
invoices = {}
with open(csv_path) as f:
reader = csv.DictReader(f, delimiter=';')
for row in reader:
inv_num = row['NumFacture']
if inv_num not in invoices:
invoices[inv_num] = {
'InvoiceNumber': inv_num,
'IssueDate': datetime.strptime(row['Date'], '%d/%m/%Y').isoformat()[:10],
'SellerTaxID': row['MatriculeFiscal'],
'BuyerTaxID': row['MatriculeFiscalClient'],
'Lines': [],
'TaxTotal': 0,
'InvoiceTotal': 0,
}
invoices[inv_num]['Lines'].append({
'Description': row['Designation'],
'Quantity': row['Quantite'],
'UnitPrice': f"{float(row['PrixUnitaire']):.3f}",
'TaxRate': row['TauxTVA'],
'LineTotal': f"{float(row['MontantTTC']):.3f}",
})
return list(invoices.values())Scénario 3 : Système personnalisé (API directe)
Pour les systèmes développés sur mesure, voici un client API complet en TypeScript :
// ttn-client.ts — Client API TTN El Fatoora
import { Agent } from 'https';
import { readFileSync } from 'fs';
import { XMLBuilder, XMLParser } from 'fast-xml-parser';
interface TTNConfig {
baseUrl: string; // https://api.elfatoora.digital
certPath: string; // Certificat TUNTRUST
keyPath: string; // Clé privée
caPath: string; // CA ANCE
companyTaxId: string;
}
interface InvoiceLine {
description: string;
quantity: number;
unitPrice: number;
taxRate: number;
}
interface InvoiceData {
number: string;
date: string;
buyerTaxId: string;
buyerName: string;
lines: InvoiceLine[];
}
class TTNClient {
private agent: Agent;
private config: TTNConfig;
private xmlBuilder: XMLBuilder;
private xmlParser: XMLParser;
constructor(config: TTNConfig) {
this.config = config;
this.agent = new Agent({
cert: readFileSync(config.certPath),
key: readFileSync(config.keyPath),
ca: readFileSync(config.caPath),
});
this.xmlBuilder = new XMLBuilder({ ignoreAttributes: false });
this.xmlParser = new XMLParser({ ignoreAttributes: false });
}
async submitInvoice(invoice: InvoiceData): Promise<{
id: string;
status: string;
qrCode: string;
}> {
const teifXml = this.buildTEIF(invoice);
const response = await fetch(`${this.config.baseUrl}/api/v1/invoice/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/xml' },
body: teifXml,
// @ts-ignore
agent: this.agent,
});
if (!response.ok) {
const error = await response.text();
throw new Error(`TTN API error (${response.status}): ${error}`);
}
return response.json();
}
async getStatus(invoiceId: string): Promise<string> {
const response = await fetch(
`${this.config.baseUrl}/api/v1/invoice/${invoiceId}/status`,
{ agent: this.agent as any }
);
const data = await response.json();
return data.status;
}
async getPDF(invoiceId: string): Promise<Buffer> {
const response = await fetch(
`${this.config.baseUrl}/api/v1/invoice/${invoiceId}/pdf`,
{ agent: this.agent as any }
);
return Buffer.from(await response.arrayBuffer());
}
private buildTEIF(invoice: InvoiceData): string {
const taxTotal = invoice.lines.reduce(
(sum, l) => sum + l.quantity * l.unitPrice * l.taxRate / 100, 0
);
const invoiceTotal = invoice.lines.reduce(
(sum, l) => sum + l.quantity * l.unitPrice * (1 + l.taxRate / 100), 0
);
return this.xmlBuilder.build({
Invoice: {
InvoiceHeader: {
InvoiceNumber: invoice.number,
IssueDate: invoice.date,
InvoiceTypeCode: '380',
CurrencyCode: 'TND',
},
SellerParty: {
TaxID: this.config.companyTaxId,
},
BuyerParty: {
TaxID: invoice.buyerTaxId,
Name: invoice.buyerName,
},
InvoiceLines: {
Line: invoice.lines.map(l => ({
Description: l.description,
Quantity: l.quantity.toString(),
UnitPrice: l.unitPrice.toFixed(3),
TaxRate: l.taxRate.toString(),
LineTotal: (l.quantity * l.unitPrice * (1 + l.taxRate / 100)).toFixed(3),
})),
},
TaxTotal: taxTotal.toFixed(3),
InvoiceTotal: invoiceTotal.toFixed(3),
},
});
}
}
// Utilisation
const ttn = new TTNClient({
baseUrl: 'https://api.elfatoora.digital',
certPath: './certs/tuntrust.pem',
keyPath: './certs/private.pem',
caPath: './certs/ance-ca.pem',
companyTaxId: '1234567A/M/000',
});
const result = await ttn.submitInvoice({
number: 'FAC-2026-001234',
date: '2026-03-31',
buyerTaxId: '7654321B/M/000',
buyerName: 'Client SA',
lines: [
{ description: 'Développement web', quantity: 1, unitPrice: 5000, taxRate: 19 },
{ description: 'Hébergement annuel', quantity: 1, unitPrice: 1200, taxRate: 19 },
],
});
console.log(`Facture soumise: ${result.id}, QR: ${result.qrCode}`);Scénario 4 : E-commerce (WooCommerce, PrestaShop)
Architecture e-commerce
┌───────────────────────────────────────────┐
│ Boutique en ligne │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ WooCommerce │ │ PrestaShop / │ │
│ │ / Custom │ │ Magento │ │
│ └──────┬──────┘ └──────┬───────────┘ │
│ │ │ │
│ ┌──────▼──────────────────▼──────────┐ │
│ │ Plugin/Module El Fatoora │ │
│ │ - Webhook sur commande validée │ │
│ │ - Extraction données facture │ │
│ │ - Envoi au middleware │ │
│ └────────────────┬───────────────────┘ │
└───────────────────┼──────────────────────┘
│
┌─────────▼──────────┐
│ Middleware Noqta │
│ El Fatoora │
└─────────┬──────────┘
│
┌─────────▼──────────┐
│ API TTN │
└────────────────────┘
Plugin WooCommerce
<?php
/**
* Plugin Name: Noqta El Fatoora pour WooCommerce
* Description: Intégration facturation électronique TTN
*/
add_action('woocommerce_order_status_completed', 'noqta_submit_elfatoora');
function noqta_submit_elfatoora($order_id) {
$order = wc_get_order($order_id);
// Vérifier que c'est un client tunisien (B2B)
$tax_id = get_post_meta($order_id, '_billing_tax_id', true);
if (empty($tax_id)) return;
$lines = [];
foreach ($order->get_items() as $item) {
$lines[] = [
'description' => $item->get_name(),
'quantity' => $item->get_quantity(),
'unit_price' => $item->get_total() / $item->get_quantity(),
'tax_rate' => 19, // TVA tunisienne standard
];
}
$payload = [
'invoice_number' => 'WC-' . $order->get_order_number(),
'date' => $order->get_date_completed()->format('Y-m-d'),
'buyer_tax_id' => $tax_id,
'buyer_name' => $order->get_billing_company() ?: $order->get_billing_first_name(),
'lines' => $lines,
];
$response = wp_remote_post('https://middleware.noqta.tn/api/v1/invoice/submit', [
'body' => json_encode($payload),
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . get_option('noqta_elfatoora_api_key'),
],
]);
if (!is_wp_error($response)) {
$body = json_decode(wp_remote_retrieve_body($response), true);
update_post_meta($order_id, '_elfatoora_id', $body['invoice_id']);
update_post_meta($order_id, '_elfatoora_status', 'submitted');
}
}Pièges courants (tous scénarios)
1. Format des montants
TTN exige exactement 3 décimales pour tous les montants en TND. 5000.00 sera rejeté, 5000.000 sera accepté.
2. Matricule fiscal
Le format exact est XXXXXXXXA/M/NNN. Un espace ou un tiret en trop causera un rejet.
3. Certificat TUNTRUST
- Le certificat expire après 2 ans — planifiez le renouvellement
- En environnement de test (sandbox), utilisez le certificat de test fourni par TTN
- Ne stockez jamais la clé privée dans le code source
4. Gestion des erreurs
L'API TTN peut retourner des erreurs temporaires. Implémentez un système de retry avec backoff exponentiel :
async function submitWithRetry(invoice: InvoiceData, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await ttn.submitInvoice(invoice);
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.pow(2, attempt) * 1000;
console.log(`Tentative ${attempt} échouée, retry dans ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
}
}
}5. Continuité des numéros
TTN vérifie la séquentialité des numéros de facture. Un trou dans la séquence déclenche une alerte.
6. Notes de crédit
Les avoirs doivent référencer la facture originale par son ID El Fatoora (pas votre numéro interne).
Noqta : Votre partenaire intégration TTN
Nous avons intégré l'API TTN dans plus de 20 systèmes différents. Notre offre :
Middleware El Fatoora as-a-Service
- API REST unifiée pour tous vos ERP
- Validation TEIF automatique
- Signature électronique intégrée
- File d'attente et retry automatique
- Dashboard de suivi en temps réel
- Support multi-entreprises
Modules ERP clé en main
- Module Odoo El Fatoora (Community & Enterprise)
- Connecteur SAP Business One
- Plugin WooCommerce / PrestaShop
- API pour systèmes personnalisés
Accompagnement
- Audit de votre système actuel
- Conception de l'architecture d'intégration
- Développement et déploiement
- Formation des équipes
- Support post-déploiement
📩 Contactez-nous pour un devis gratuit d'intégration TTN.
📞 Appelez-nous : +216 XX XXX XXX — Consultation gratuite de 30 minutes.
FAQ
Combien de temps prend l'intégration TTN ?
Pour un ERP standard (Odoo, SAP) avec notre middleware : 2 à 4 semaines. Pour un système personnalisé : 4 à 8 semaines selon la complexité.
Quel est le coût du certificat TUNTRUST ?
Le certificat de signature électronique coûte entre 200 et 500 DT selon le prestataire accrédité (TUNTRUST, ANCE). Validité 2 ans.
Puis-je tester avant de passer en production ?
Oui. TTN fournit un environnement sandbox. Nous configurons votre intégration en sandbox d'abord, puis basculons en production après validation.
El Fatoora accepte-t-elle les factures en devises étrangères ?
Le format TEIF est en TND. Les factures en EUR/USD doivent inclure le montant converti en TND au taux du jour.
Quelle est la différence entre le mode Web et le mode EDI ?
- Mode Web : Saisie manuelle sur elfatoora.digital — adapté aux faibles volumes
- Mode EDI (API) : Intégration automatisée — indispensable dès 50+ factures/mois
Articles connexes :
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.