Au 1er octobre 2026, chaque banque tunisienne soumise au décret n° 2026-148 n'aura plus qu'une seule porte pour recevoir les demandes de crédits et financements micro sur l'honneur : sa plateforme électronique. C'est ce que décide la circulaire aux banques n° 2026-08 de la Banque Centrale de Tunisie, datée du 1er septembre 2026, un texte court de sept articles et trois annexes qui est en réalité un cahier des charges technique complet.
La phrase qui résume toute la circulaire se trouve à la fin de l'article 3 : aucune demande présentée par un moyen autre que la plateforme électronique dédiée n'est prise en compte. Une demande déposée sur papier au guichet n'est plus une demande incomplète : juridiquement, elle n'existe pas. La plateforme n'est donc pas un formulaire d'inscription, c'est le registre officiel à partir duquel se calcule le délai de décision et s'établit l'ordre des priorités.
Ce guide s'adresse à l'équipe technique qui doit livrer cette plateforme avant le 1er octobre. Le volet client — qui a droit, quel plafond s'applique, quels documents joindre — est traité dans Conditions et documents du crédit sur l'honneur, et le contenu de la circulaire dans Circulaire BCT 2026-08.
Ce que vous allez construire
Un service TypeScript unique couvrant les quatre obligations que génère la circulaire :
- Un guichet de dépôt qui horodate électroniquement chaque demande et émet un accusé automatique laissant une trace écrite.
- Un moteur de classement par antériorité fondé sur la date et l'heure de l'horodatage, et non sur la saisie d'un agent, avec le délai de dix jours calculé à partir de ce même instant.
- Un pont avec la centrale d'informations de la BCT : consultation obligatoire avant décaissement, et déclaration instantanée au décaissement selon les codes de l'annexe 1.
- Un générateur d'états mensuels aux modèles des annexes 2 et 3, transmis via le système d'échange de données dans les quinze jours suivant la fin du mois.
Prérequis
- Node.js 20 ou plus et TypeScript 5.
- Une base de données transactionnelle (les extraits ci-dessous visent PostgreSQL).
- Un accès au service d'horodatage de la banque, et un accès aux canaux de la centrale d'informations et du système d'échange de données retenus par votre établissement.
- Une connaissance du décret n° 2026-148 : les trois catégories et leurs plafonds, le remboursement en deux ans au maximum, et le différé qui ne dépasse pas six mois.
Note sur le périmètre. La circulaire ne nomme ni norme technique d'horodatage ni format de fichier pour le système d'échange de données. Ce que nous proposons ici (RFC 3161 pour l'horodatage, une génération des états à partir d'une source unique) est un choix d'ingénierie conforme au texte, pas une prescription de la circulaire. Retenez toujours les formats validés par la BCT pour votre canal.
D'où viennent ces obligations
Avant d'écrire une ligne, il est utile de rattacher chaque composant à l'article qui l'engendre. Ce tableau est la carte du projet :
| Article | Obligation | Traduction en code |
|---|---|---|
| Article 3 | Dépôt exclusivement via la plateforme, horodatage électronique, accusé automatique | Guichet de dépôt unique + table d'horodatages + notification tracée |
| Article 3 | Classement par antériorité et calcul du délai depuis l'horodatage | Index sur l'instant d'horodatage + minuteur de délai |
| Article 4 | Consultation des engagements avant décaissement | Contrôle bloquant dans le chemin de décaissement |
| Article 4 | Déclaration instantanée au décaissement aux codes de l'annexe 1 | Motif outbox avec reprise sur échec |
| Article 5 | Deux états mensuels via le système d'échange de données sous 15 jours | Générateur des annexes 2 et 3 + ordonnanceur |
| Article 7 | Entrée en vigueur au 1er octobre 2026 | Date de livraison |
Notez que l'article 2 maintient ces crédits sous les politiques internes de la banque et sous les règles de gouvernance, de contrôle interne et de classification des engagements. La plateforme ne crée donc pas un circuit parallèle au système bancaire : elle y introduit une nouvelle catégorie.
Étape 1 : modéliser la demande — l'horodatage est la preuve
La première erreur de conception, et la plus coûteuse, consiste à traiter l'instant d'horodatage comme un simple champ de date. L'article 3 en fait ce qui atteste la date et l'heure du dépôt, ce qui permet de classer les demandes par ordre d'antériorité, et ce à partir de quoi court le délai. C'est une preuve, pas une métadonnée.
Preuve signifie deux choses : le jeton binaire est conservé tel que renvoyé par le service, et il n'est jamais dérivé de l'horloge du serveur.
// src/domain/application.ts
export type ApplicantClass = 'individual' | 'small_project' | 'sme_or_community_company';
export interface TimestampToken {
/** رمز الختم كما ورد من خدمة ختم التوقيت، محفوظًا كما هو */
readonly token: Buffer;
/** التوقيت المستخرج من الرمز — لا من ساعة الخادم */
readonly genTime: Date;
readonly authority: string;
readonly serial: string;
}
export interface LoanApplication {
readonly id: string;
readonly applicantId: string;
readonly applicantClass: ApplicantClass;
/** المبلغ بالمليم، لتفادي حساب الفاصلة العائمة */
readonly amountMillimes: number;
readonly governorateCode: string;
readonly stamp: TimestampToken;
readonly receiptRef: string;
status: 'submitted' | 'under_review' | 'approved' | 'rejected' | 'disbursed';
}Les plafonds s'imposent au niveau du modèle, pas de l'interface, parce que l'interface se contourne :
const CEILING_MILLIMES: Record<ApplicantClass, number> = {
individual: 5_000_000, // خمسة آلاف دينار
small_project: 10_000_000, // عشرة آلاف دينار
sme_or_community_company: 25_000_000 // خمسة وعشرون ألف دينار
};
export function assertWithinCeiling(app: LoanApplication): void {
const ceiling = CEILING_MILLIMES[app.applicantClass];
if (app.amountMillimes > ceiling) {
throw new DomainError('CEILING_EXCEEDED', {
requested: app.amountMillimes,
ceiling,
applicantClass: app.applicantClass
});
}
}Étape 2 : une seule porte, et l'accusé fait partie de la transaction
L'article 3 impose que la plateforme délivre l'accusé dès le dépôt et de manière automatique. Le mot « automatique » signifie que l'accusé n'est ni une tâche différée dans une file, ni un message envoyé plus tard par un agent.
Concrètement : si le dépôt réussit sans que l'accusé parte, la banque ne peut plus prouver qu'elle a informé le client. Si l'accusé part et que la transaction échoue, vous avez remis au client la preuve d'une demande inexistante. La solution consiste à écrire l'accusé dans la transaction elle-même et à l'expédier juste après la validation, en conservant la trace écrite.
// src/api/submit.ts
export async function submitApplication(input: SubmitInput): Promise<Receipt> {
assertWithinCeiling(toApplication(input));
// 1) الختم أولًا: التوقيت هو ما سيُرتّب المطلب ويحتسب منه الأجل
const stamp = await timestampService.stamp(canonicalDigest(input));
return db.transaction(async (tx) => {
const application = await tx.applications.insert({ ...input, stamp });
const receipt = await tx.receipts.insert({
applicationId: application.id,
reference: buildReceiptReference(application, stamp),
depositedAt: stamp.genTime,
channel: input.contactChannel,
body: renderReceiptText(application, stamp)
});
// إشعار مؤجّل داخل نفس المعاملة: لا يُرسل إلا إذا التزمت
await tx.outbox.insert({ topic: 'receipt.deliver', payload: { receiptId: receipt.id } });
return receipt;
});
}Le texte de l'accusé doit porter la date et l'heure du dépôt attestées par l'horodatage. Ajoutez-y une référence que le client peut citer : un numéro interne qu'il ne voit nulle part ne lui sert à rien.
Quant à la « porte unique », elle s'impose par l'architecture et non par l'intention : tout autre chemin d'entrée — import de fichier, saisie en agence, API interne — doit passer par la même fonction ou être fermé. Une demande entrée par une autre porte n'est pas prise en compte par le texte, et sa présence dans votre base crée une obligation sans fondement.
Étape 3 : classement par antériorité et délai de dix jours
L'antériorité se calcule à partir de genTime seul. En cas d'égalité d'instants — cas prévisible dans les premières secondes d'ouverture de la plateforme — le départage doit être déterministe et explicable à un contrôleur, jamais aléatoire.
export function byPriority(a: LoanApplication, b: LoanApplication): number {
const t = a.stamp.genTime.getTime() - b.stamp.genTime.getTime();
if (t !== 0) return t;
// فاصل حتمي عند التساوي: الرقم التسلسلي للختم من نفس السلطة
return a.stamp.serial.localeCompare(b.stamp.serial);
}Le délai de décision — dix jours ouvrables bancaires selon l'article 6 du décret n° 2026-148 — court depuis l'horodatage. C'est un délai en jours ouvrés et non en jours calendaires, ce qui suppose un calendrier des jours fériés bancaires :
export function decisionDeadline(stampedAt: Date, calendar: BankingCalendar): Date {
let cursor = new Date(stampedAt);
let remaining = 10;
while (remaining > 0) {
cursor = addDays(cursor, 1);
if (calendar.isBankingDay(cursor)) remaining -= 1;
}
return cursor;
}Ne figez pas le calendrier des fériés dans le code. Les fêtes religieuses se déplacent chaque année en Tunisie, et des journées chômées peuvent être ajoutées par décision. Faites de
BankingCalendarune table mise à jour, et enregistrez avec chaque demande la version du calendrier ayant servi au calcul, pour que le résultat reste reproductible un an plus tard.
Étape 4 : consulter la centrale d'informations avant le décaissement
L'article 4 ne fait pas de ce contrôle une simple bonne pratique : avant le décaissement, la banque doit consulter la situation des engagements du demandeur auprès de la centrale d'informations de la BCT afin de vérifier qu'il ne bénéficie pas déjà d'un crédit ou financement de la même catégorie non intégralement remboursé.
Trois précisions de rédaction se traduisent directement en code :
- Le contrôle est rattaché au moment du décaissement, pas à celui de l'accord. L'accord peut précéder le décaissement de plusieurs jours, et le client peut emprunter ailleurs entre-temps.
- La condition porte sur « la même catégorie » : le contrôle se compare aux codes de l'annexe 1, pas à l'endettement global du client.
- « Non intégralement remboursé » signifie qu'un reliquat, si petit soit-il, bloque le nouveau financement.
const HONOUR_LOAN_CODES = ['260', '261', '185', '3400'] as const;
export async function assertNoOutstandingSameCategory(
applicantId: string,
category: (typeof HONOUR_LOAN_CODES)[number]
): Promise<void> {
const exposures = await centraleClient.getExposures(applicantId);
const blocking = exposures.filter(
(e) => e.kfcred === category && e.outstandingMillimes > 0
);
if (blocking.length > 0) {
throw new DomainError('OUTSTANDING_SAME_CATEGORY', { category, blocking });
}
}Faites de ce contrôle un verrou dans le chemin de décaissement lui-même, pas un écran que l'agent consulte. Et conservez la réponse complète avec son horodatage : lors d'un contrôle, la question ne sera pas « avez-vous consulté ? » mais « que disait la réponse au moment du décaissement ? ».
Étape 5 : la déclaration instantanée au décaissement
L'article 4 impose de déclarer à la centrale d'informations de manière instantanée, au décaissement du crédit ou du financement, selon les codes de l'annexe 1 :
| Code KFCRED | Libellé |
|---|---|
| 260 | Crédit court terme sur l'honneur (décret n° 2026-148) |
| 261 | Financement court terme sur l'honneur (décret n° 2026-148) |
| 185 | Impayés en principal sur crédit court terme d'honneur |
| 3400 | Crédit sur l'honneur aux particuliers (décret n° 2026-148) |
La distinction entre 260 et 261 est celle entre crédit et financement, c'est-à-dire entre la forme conventionnelle et la forme de finance islamique à laquelle la circulaire renvoie explicitement dans ses visas. Le code 185 n'est pas une catégorie à l'octroi, mais un état ultérieur : un impayé en principal. Qui fige sa table de codes au décaissement en oubliant que 185 apparaît plus tard dans le cycle de vie le découvrira au premier incident.
« Instantané » ne veut pas dire « dans la nuit ». Mais cela ne veut pas dire non plus un appel synchrone qui ferait échouer le décaissement si le canal tombe. Le motif correct est l'outbox : le message de déclaration est écrit dans la transaction du décaissement, et un travailleur indépendant l'expédie immédiatement avec une reprise progressive.
export async function disburse(applicationId: string): Promise<void> {
const app = await repo.load(applicationId);
await assertNoOutstandingSameCategory(app.applicantId, categoryOf(app));
await db.transaction(async (tx) => {
await tx.applications.update(app.id, { status: 'disbursed', disbursedAt: new Date() });
await tx.ledger.recordDisbursement(app);
await tx.outbox.insert({
topic: 'centrale.declare',
payload: { applicationId: app.id, kfcred: categoryOf(app) },
availableAt: new Date() // فورًا
});
});
}L'intérêt pratique de l'outbox est de rendre la question « chaque crédit décaissé a-t-il été déclaré ? » répondable par une seule requête : tout décaissement sans message livré devient un écart visible, et non une erreur silencieuse.
Étape 6 : annexe 2 — l'état mensuel ventilé par gouvernorat
L'annexe 2 demande un état du volume des crédits et financements accordés sur les ressources du compte de la ligne de financement sur l'honneur, ventilé par gouvernorat, en montants recouvrés et non recouvrés. Chaque gouvernorat occupe une ligne portant le nombre et le montant pour chaque catégorie de bénéficiaires — particuliers, petit projet, petite ou moyenne entreprise, société communautaire — puis le total des crédits accordés, la part du gouvernorat dans les montants accordés en pourcentage, puis les montants recouvrés et non recouvrés et leur ratio.
L'annexe définit elle-même les deux catégories en chiffres : le petit projet est celui dont l'investissement cumulé ne dépasse pas cent cinquante mille dinars, fonds de roulement compris ; la petite ou moyenne entreprise est celle dont les investissements se situent entre cent cinquante mille dinars et quinze millions de dinars. Classez le bénéficiaire une fois, à l'octroi, et conservez la catégorie avec l'opération. Une entreprise qui grandit entre deux mois ne doit pas réécrire l'état d'un mois passé.
L'unité de l'annexe est le millier de dinars, alors que votre système tient le millime. C'est là que la plupart des implémentations échouent : chaque ligne est arrondie séparément, et la somme des lignes ne retombe plus sur la ligne de total.
const MILLIMES_PER_THOUSAND_DINARS = 1_000_000;
function toThousandDinars(millimes: number): number {
return Math.round((millimes / MILLIMES_PER_THOUSAND_DINARS) * 1000) / 1000;
}
export function buildAnnex2(rows: DisbursementRow[], period: Period): Annex2 {
const byGovernorate = new Map<string, Annex2Row>();
for (const row of rows) {
const g = byGovernorate.get(row.governorateCode) ?? emptyRow(row.governorateCode);
const bucket = g.classes[row.beneficiaryClass];
bucket.count += 1;
bucket.millimes += row.principalMillimes;
g.collectedMillimes += row.collectedMillimes;
byGovernorate.set(row.governorateCode, g);
}
const totalMillimes = sum([...byGovernorate.values()].map(totalOf));
return {
period,
rows: [...byGovernorate.values()].map((g) => ({
...g,
sharePercent: totalMillimes === 0 ? 0 : round2((totalOf(g) / totalMillimes) * 100),
uncollectedMillimes: totalOf(g) - g.collectedMillimes
})),
total: { millimes: totalMillimes, thousandDinars: toThousandDinars(totalMillimes) }
};
}La règle : additionnez en millimes, arrondissez une seule fois à l'affichage, et calculez les pourcentages sur les montants d'origine, jamais sur les arrondis. Émettez les vingt-quatre gouvernorats, y compris ceux sans aucun octroi : une ligne absente se lit comme une donnée manquante, une ligne à zéro se lit comme une information.
Étape 7 : annexe 3 — l'état du compte de la ligne de financement
L'annexe 3 comporte deux parties. La première rassemble les données du compte : date de tenue de l'assemblée générale ordinaire des actionnaires, résultat comptable net approuvé, volume des crédits affectés à la ligne de financement — que l'annexe qualifie explicitement de 8 pour cent du résultat comptable — puis date d'ouverture du compte et date de mise à disposition des fonds.
Ces données ne changent pas d'un mois sur l'autre, mais elles se déclarent chaque mois. Lisez-les depuis une source de référence unique rattachée à la décision de l'assemblée générale, et non depuis une saisie manuelle répétée douze fois par an. Une valeur ressaisie chaque mois finira par différer d'elle-même.
La seconde partie est l'état des opérations enregistrées sur le compte à la fin du mois : solde d'ouverture (1), moins les tirages au titre du décaissement des crédits et financements (2), d'où le solde de fin de mois (3) = (1) − (2).
Attention à une divergence apparente entre l'article et l'annexe. L'article 5 demande un inventaire de toutes les opérations enregistrées au crédit comme au débit, tandis que le modèle de l'annexe 3 n'affiche que le solde d'ouverture, les tirages et le solde de clôture. Ne supprimez pas le côté crédit de votre modèle de données au motif que le tableau ne lui réserve pas de colonne : gardez l'inventaire complet à la source, et faites du modèle une vue dérivée. Le jour où le détail est demandé — et le texte de l'article le prévoit — vous pourrez le produire sans reconstitution historique.
Cette règle est générale : déclarez ce que demande le modèle, conservez ce que demande le texte.
Étape 8 : transmettre via le système d'échange de données sous quinze jours
Les deux états sont adressés à la BCT via le système d'échange de données, dans un délai maximal de quinze jours à compter de la fin du mois déclaré. Ce délai est court quand on sait que votre arrêté comptable mensuel n'est parfois pas terminé avant le dixième jour.
Rendez la génération rejouable et déterministe : le même mois et les mêmes données produisent le même fichier, octet pour octet. Lancez-la tôt pour la relecture, et transmettez après l'arrêté.
// src/jobs/monthly-declaration.ts
export async function runMonthlyDeclaration(period: Period): Promise<void> {
const deadline = addDays(endOfMonth(period), 15);
const annex2 = buildAnnex2(await repo.disbursementsFor(period), period);
const annex3 = buildAnnex3(await repo.creditLineAccountFor(period), period);
const bundle = serializeForDataExchange({ annex2, annex3, period });
const digest = sha256(bundle);
await repo.declarations.upsert({
period,
digest,
deadline,
generatedAt: new Date(),
status: 'ready'
});
}Stockez l'empreinte de chaque transmission. En cas de correction ultérieure, l'écart entre deux empreintes explique au contrôleur ce qui a changé et pourquoi — bien moins cher que de reconstituer le mois de mémoire.
Tester votre implémentation
Cinq tests couvrent les endroits où ce type de système casse réellement :
describe('امتثال المنشور عدد 8 لسنة 2026', () => {
it('يرتّب حسب توقيت الختم لا حسب توقيت الإدراج', async () => {
const late = await submit({ ref: 'A', stampedAt: '2026-10-01T08:00:02Z' });
const early = await submit({ ref: 'B', stampedAt: '2026-10-01T08:00:01Z' });
expect([late, early].sort(byPriority)[0].receiptRef).toBe(early.receiptRef);
});
it('يرفض الإيداع إذا فشل ختم التوقيت', async () => {
timestampService.failNext();
await expect(submit(validInput)).rejects.toThrow('TIMESTAMP_UNAVAILABLE');
expect(await repo.count()).toBe(0); // لا مطلب بلا ختم
});
it('يحجب الصرف عند وجود قرض من نفس الصنف غير مخلّص', async () => {
centraleClient.setExposures('CIN123', [{ kfcred: '260', outstandingMillimes: 1 }]);
await expect(disburse(appOfCategory('260'))).rejects.toThrow('OUTSTANDING_SAME_CATEGORY');
});
it('يحتسب الأجل بأيام العمل المصرفية لا بالأيام التقويمية', () => {
const stamped = new Date('2026-10-01T09:00:00Z'); // خميس
expect(decisionDeadline(stamped, calendarWithWeekends())).toEqual(new Date('2026-10-15T09:00:00Z'));
});
it('يساوي مجموع أسطر الملحق 2 سطرَ الإجمالي بعد التقريب', () => {
const annex2 = buildAnnex2(fixtureRows, period);
const sumOfRows = annex2.rows.reduce((acc, r) => acc + totalOf(r), 0);
expect(toThousandDinars(sumOfRows)).toBe(annex2.total.thousandDinars);
});
});Le deuxième test est le plus important. Une plateforme qui accepte la demande puis tente de l'horodater ensuite produit des demandes sans fondement, et c'est le seul cas qu'aucune correction rétroactive ne rattrape : on n'horodate pas le passé.
Erreurs fréquentes et traitement
L'accusé arrive alors que la demande n'existe pas. Cause : notification envoyée hors transaction. Remède : l'outbox dans la même transaction, comme à l'étape 2.
Divergence entre l'ordre de la plateforme et celui de l'agence. Cause : usage du created_at de la base au lieu de genTime. Supprimez tout tri qui ne passe pas par byPriority.
Le total des gouvernorats ne retombe pas sur le total général. Arrondi ligne à ligne. Additionnez en millimes et arrondissez une seule fois.
Déclaration centrale manquante pour un crédit décaissé. Message bloqué dans l'outbox sans surveillance. Alertez sur l'âge de chaque message au-delà d'un seuil, pas seulement sur un taux d'échec.
Consultation faite à l'accord et non au décaissement. Si plus d'une journée sépare l'accord du décaissement, refaites le contrôle au décaissement : le texte le rattache explicitement au décaissement.
Prochaines étapes
- Reprenez les conditions, plafonds et documents dans Conditions du crédit sur l'honneur avant de figer les règles de validation de l'interface.
- Utilisez le générateur de demande de crédit sur l'honneur comme référence des champs et de la formulation usuelle lors de la conception du formulaire, et le montant en toutes lettres pour produire le montant en lettres dans l'accusé et les pièces contractuelles.
- Suivez ce que change la circulaire 2026-08 et le démarrage des octrois via les plateformes bancaires.
Conclusion
La circulaire n° 2026-08 ne demande pas aux banques un formulaire d'inscription, mais quatre choses mesurables : un horodatage qui atteste l'instant, un accusé qui atteste l'information, un contrôle qui précède le décaissement, et deux états qui arrivent sous quinze jours. Construire ces quatre éléments autour d'une source unique de vérité — l'opération en millimes, la catégorie figée à l'octroi, le jeton conservé tel quel — permet de livrer avant le 1er octobre et de répondre aux questions du contrôle par une requête plutôt que par une enquête.
Si vous construisez cette plateforme ou la couche déclarative au-dessus de votre système existant et souhaitez une lecture technique indépendante des écarts de conformité avant l'échéance, demandez un diagnostic : nous confrontons le modèle, les parcours et les états au texte de la circulaire et à ses annexes.