API eFacture Connect

Intégrez la facture électronique tunisienne dans votre ERP en quelques minutes. Envoyez du JSON, recevez un XML TEIF v1.8.8 signé (XAdES) et un PDF avec QR code, prêts à être déposés chez TunisieTradeNet.

Base URLToutes les requêtes utilisent https://efacturetn.com/api/v1

Premier appel en 30 secondes

Copiez la commande à droite dans votre terminal pour tester l'API en mode sandbox. Remplacez ck_test_xxx par la clé de test d'un de vos clients, affichée dans le portail développeur à sa création (Comptes et clients). Votre clé développeur sk_dev_ n'émet pas de facture.

Vous recevrez en retour un XML réellement signé (environnement sandbox) et un PDF, exactement comme en production. Aucun crédit n'est consommé en mode test.

Authentification

L'API utilise des clés API passées dans le header X-Api-Key. Chaque requête doit inclure une clé valide.

PréfixeModeUsage
sk_dev_ Développeur Votre clé, et la seule. Elle sert à gérer vos clients : les créer, les raccorder, lire leurs compteurs. Elle n'émet aucune facture, et elle ne porte aucun environnement. Les comptes ouverts avant septembre 2026 gardent leur clé sk_test_, toujours valide.
ck_test_ Test Clé d'un de vos clients. Signature réelle sur l'environnement sandbox, dépôt TTN de test, aucun crédit consommé.
ck_live_ Production Clé du même client, en production. Signature XAdES réelle, dépôt TTN effectif, 1 crédit déduit par signature.
Ne partagez jamais votre cléLes clés API donnent un accès complet à votre compte. Stockez-les comme des secrets (variables d'environnement, gestionnaire de secrets).

Comptes et clients

Un compte développeur peut gérer plusieurs clients (une intégration par entreprise finale). Vous créez et configurez vos clients depuis votre espace développeur, section « Mes clients ».

NotionDescription
Clés par client Chaque client possède sa propre paire de clés ck_test_ / ck_live_ (clé de client), distinctes de votre clé de développeur sk_dev_ qui sert à la gestion. Une requête d'émission est toujours rattachée au client dont vous utilisez la clé. La clé développeur ne peut pas émettre de factures, que vous ayez des clients rattachés ou non : /einvoices/prepare, /einvoices/process et /einvoices/{id}/resubmit-ttn répondent 403 DEVELOPER_KEY_NOT_ALLOWED. Utilisez la clé du client concerné. Pour émettre au nom de votre propre société, créez-vous un client (POST /api/v1/clients) et utilisez sa clé : votre matricule y est accepté même s'il est aussi celui de votre compte.
Portefeuille partagé Les crédits sont achetés au niveau du compte développeur et partagés entre tous vos clients.
Plafond par client Vous pouvez fixer un plafond de crédits par client. Le client se bloque à son plafond sans affecter les autres ni le reste du portefeuille. Vous suivez la consommation par client.
Canal de dépôt Chaque client dépose à TTN par SOAP (synchrone) ou SFTP (asynchrone), selon la configuration de son compte TTN. En SFTP, la facture passe par le statut submitted_ttn, et vous lisez la validation par GET /einvoices/{id}.
Méthode de signature Configurée par client : cachet serveur (automatique), clé USB (signature locale) ou signature à distance. Sans incidence sur le format des requêtes.
INSUFFICIENT_CREDITSCette erreur (402) est renvoyée lorsque le portefeuille du compte développeur est vide OU lorsque le plafond de crédits du client concerné est atteint.
Le vendeur, c'est votre cléChaque clé appartient à un client dont le matricule fiscal a été enregistré et validé au format complet exigé par TTN. C'est sous ce matricule que la facture est signée. seller.tax_id est donc facultatif : si vous l'omettez, nous posons celui du compte. Si vous l'envoyez, nous comparons uniquement les 8 premiers caractères, ceux qui identifient le contribuable. 1234567M et 1234567MAM000 sont donc équivalents, et la forme que vous envoyez n'a pas d'importance : c'est la forme complète du compte qui part dans le document. En cas de contribuable réellement différent, la requête est refusée en SELLER_MISMATCH avant toute signature, donc sans crédit consommé.
Renvoyer la même facture ne la signe pas deux fois Si votre connecteur n'a pas reçu notre réponse à temps et rejoue l'appel, vous ne payez plus deux fois. Nous comparons le contenu de la facture, pas seulement son numéro, et nous répondons selon ce que ce numéro porte déjà :
  • Même contenu, facture déjà produite : nous vous rendons le document existant, à l'identique, avec "replayed": true. Aucun crédit n'est consommé et rien n'est redéposé chez TTN.
  • Contenu différent sur un numéro déjà accepté par TTN : refus en INVOICE_NUMBER_ALREADY_ISSUED. Un numéro déposé ne peut pas porter deux documents.
  • Contenu différent après un refus de TTN : la signature repart normalement, et consomme un crédit. Le document précédent est nul, le numéro vous appartient toujours.
  • Même contenu, signature en attente (DigiGo, clé USB, E-Houwiya) : nous vous rendons le lien déjà ouvert, avec "resumed": true et la même signing_url. Vous avez perdu le lien ? Renvoyez simplement la même facture à POST /einvoices/prepare, ou lisez GET /einvoices/{signing_id}, qui porte signing_url tant que la signature attend. Aucune seconde transaction n'est ouverte.
  • Contenu différent pendant qu'une signature attend : refus en INVOICE_IN_PROGRESS. error.details porte le signing_id, la signing_url de la signature ouverte pour la terminer, et can_prepare_again_at : sans signature d'ici là, la tentative est abandonnée à cette heure (30 minutes après son ouverture) et le numéro se prépare de nouveau. Pour ne pas attendre, annulez-la : POST /einvoices/{signing_id}/cancel.
Deux précisions utiles. Les champs de transport (return_url, un identifiant de corrélation) ne comptent pas dans la comparaison : les changer ne fait pas de votre facture un document différent. Et l'ordre des lignes compte, lui, parce qu'il change le document déposé. Le sandbox suit exactement la même règle que la production, et les deux environnements sont séparés : une facture d'essai ne bloque jamais un numéro réel.

Test vs Production

Passer un client en production ne nécessite aucun changement de code : dans son ERP, remplacez simplement sa clé ck_test_xxx par sa clé ck_live_xxx. C'est le préfixe qui décide de l'environnement, il n'y a aucun réglage à changer ailleurs.

Vos clients sont indépendants les uns des autres : l'un peut être en production pendant qu'un autre reste en test. Il n'existe aucun interrupteur global, et votre clé développeur ne change jamais.

Sandbox = environnement réel de testAucune simulation : en mode test, sauf si le banc d'essai a été ouvert pour ce client, la signature et le dépôt s'exécutent réellement sur les environnements de TEST du service de signature et de TTN. La signing_url de /einvoices/prepare pointe vers la vraie page de signature (sandbox), où le signataire signe avec un certificat de test. Le compte de signature sandbox du client doit donc être configuré.
CritèreTestProduction
SignatureXAdES réelle (environnement sandbox)XAdES réelle (production)
Dépôt TTNPlateforme TTN de testPlateforme TTN de production
Crédits consommésAucun1 par signature
Cachet/CertificatRequis (sandbox)Requis (production)

Banc d'essai : intégrer avant d'avoir les accès TTN

Le banc d'essai n'est pas le mode test.Le mode test (clé ck_test_) signe et dépose réellement, sur les environnements de test du service de signature et de TTN : il faut pour cela un compte El Fatoora et un certificat. Le banc d'essai, lui, simule la signature et TTN. Il sert à une seule chose : vous permettre d'écrire et de mettre au point votre intégration pendant que votre client attend ses accès TTN et son certificat, au lieu d'attendre avec lui. Rien n'est signé, rien n'est déposé, et aucun document produit n'a de valeur légale.

Obtenir un compte El Fatoora et un certificat prend souvent plusieurs semaines. Sans banc d'essai, aucun appel n'aboutit avant leur arrivée : vous ne pouvez ni tester vos écrans, ni lire nos réponses, ni préparer la gestion des erreurs. Avec le banc d'essai, vous livrez une intégration déjà rodée, et le premier appel réel réussit du premier coup.

CritèreBanc d'essaiMode test (ck_test_)Production (ck_live_)
À quoi il sertÉcrire et mettre au point votre intégrationRecetter avec la vraie chaîne, sans valeur légaleÉmettre les factures du client
Compte El Fatoora et certificatNon requisRequis (environnement de test)Requis (production)
SignatureSimuléeRéelle (environnement de test)Réelle
Dépôt TTNSimulé, aucun envoiPlateforme TTN de testPlateforme TTN de production
Référence TTNSIMULATION-AAAAMMJJ-XXXXXXXXRéférence TTN de testRéférence TTN officielle
Crédits consommésAucunAucun1 par signature
Valeur légaleAucuneAucuneOui

L'obtenir

Le banc d'essai s'ouvre sur demande, client par client : écrivez au support en indiquant le client concerné. Il s'applique uniquement aux appels faits avec la clé ck_test_ de ce client. Une clé ck_live_ n'est jamais simulée, quel que soit le réglage : une facture de production ne peut pas sortir d'une simulation.

Ce qui change dans votre code : rien

Mêmes adresses, mêmes corps de requête, mêmes champs de réponse, mêmes codes d'erreur : POST /einvoices/process, POST /einvoices/prepare, GET /einvoices/{signing_id}, POST /einvoices/{signing_id}/resubmit-ttn et POST /einvoices/{signing_id}/cancel. Le jour où les accès de votre client arrivent, nous fermons le banc d'essai : sa clé ck_test_ repart vers le vrai mode test, puis vous passez à sa clé ck_live_. Aucune ligne de votre code ne change.

Pour savoir qu'une réponse vient du banc d'essai, deux marques s'ajoutent, qu'un code existant ignore sans rien casser :

  • le champ "simulation": true dans data ;
  • l'en-tête HTTP X-eFactureTN-Simulation: 1.

Ne faites dépendre aucune logique métier de ces marques : elles servent à afficher un bandeau « simulation » dans votre interface, pas à suivre un autre chemin. Sinon, votre code de production n'aurait jamais été éprouvé.

Ce que vous recevez

  • Le même TEIF que la production : il sort de notre générateur réel. Une erreur dans votre corps de requête se découvre donc au banc d'essai, pas en production.
  • xml_base64 : le TEIF avec un bloc de signature Id="SigFrs" construit comme le vrai, mais dont la valeur est le mot SIMULATION. Aucune vérification cryptographique ne l'acceptera.
  • pdf_base64 : le PDF de la production, barré sur chaque page de la mention « SIMULATION - SANS VALEUR LÉGALE ».
  • Le cachet arrive plus tard, comme chez TTN. Le dépôt ne rend que la référence. Environ 20 secondes après, GET /einvoices/{signing_id} rend ttn.reference_confirmee: true, ttn.qr_png_base64 (QR marqué SIMULATION) et xml_validettn_base64 (le document avec RefTtnVal puis la signature SigTTN, dans l'ordre de TTN). Si votre code lit le QR dans la réponse du dépôt, il ne le trouvera pas au banc d'essai, et il ne le trouverait pas non plus en production : c'est voulu.

La page de signature (DigiGo, clé USB, E-Houwiya)

POST /einvoices/prepare rend une signing_url vers une page de signature simulée. Redirigez-y votre utilisateur comme vous le ferez en production :

  • le code PIN est toujours 0000, et aucun SMS n'est envoyé ;
  • un autre code affiche « Code PIN incorrect » ; au bout de 3 codes erronés, la signature est bloquée ;
  • le bouton « Refuser de signer » simule le refus du signataire : le statut passe à rejected avec le code SIGNATURE_REFUSED ;
  • après la signature ou le refus, l'utilisateur revient sur votre return_url, avec efacture_invoice et signing_id, comme en production. Vérifiez ensuite le statut par GET /einvoices/{signing_id} : la redirection ramène, elle n'annonce rien ;
  • le lien sert une seule fois et expire au bout de 30 minutes.

Éprouver les erreurs

Les erreurs de votre requête sont refusées comme en production, avec les mêmes codes : un corps invalide rend 422 VALIDATION_FAILED, un matricule mal formé est refusé comme TTN le refuserait. Utilisez aussi POST /einvoices/validate, qui est le même au banc d'essai et en production.

Les pannes que vous ne savez pas provoquer se demandent par l'en-tête X-Simulation-Scenario, sur /process, /prepare ou /resubmit-ttn. Il ne vaut que pour l'appel qui le porte : un renvoi sans l'en-tête suit le parcours normal, ce qui vous permet de dérouler le cas « TTN était indisponible, je renvoie ».

curl -X POST https://efacturetn.com/api/v1/einvoices/process \
  -H "X-Api-Key: ck_test_xxxxxxxxxxxx" \
  -H "X-Simulation-Scenario: ttn_indisponible" \
  -H "Content-Type: application/json" \
  -d @facture.json
ValeurCe qui se passeRéponse
ttn_indisponibleTTN ne répond pas. Le document n'est pas en cause.signed_ttn_rejected, next_action: resubmit : renvoyez-le par /resubmit-ttn.
ttn_compte_desactiveTTN refuse : le compte El Fatoora du client est désactivé (SERV01).signed_ttn_rejected, next_action: resubmit.
ttn_rejet_contrl05TTN refuse : le matricule d'authentification n'est pas celui de l'émetteur (CONTRL05). Le document est en cause.signed_ttn_rejected, next_action: resign : corrigez puis émettez à nouveau.
ttn_rejet_contrl02TTN refuse : bloc de transformation de la signature non conforme (CONTRL02).signed_ttn_rejected, next_action: resign.
signature_echoueeLa signature échoue : rien n'est signé ni déposé.503 SIGNING_FAILED sur /process. Sur la page de signature, le statut passe à rejected.

Une valeur inconnue est refusée en 422 INVALID_SIMULATION_SCENARIO, avec la liste des valeurs acceptées : la taire vous ferait croire que vous éprouvez un cas alors que vous suivez le parcours normal. Sur une clé qui n'est pas au banc d'essai, l'en-tête est ignoré.

Limites

  • Aucune valeur légale. Ne transmettez jamais un PDF ou un XML du banc d'essai à un client final : ce n'est pas une facture.
  • 300 factures par période de 24 heures et par client, au-delà : 429 SANDBOX_DAILY_LIMIT.
  • Envoyer deux fois le même numéro crée deux factures au banc d'essai, alors qu'en production le second envoi vous rend la première (voir Renvoyer la même facture). Éprouvez ce cas en mode test ou en production.
  • Les documents du banc d'essai ne se lisent qu'avec la clé ck_test_ du client, ou avec votre clé développeur.

Modes de signature

Quatre modes de signature selon votre cas d'usage : Entreprise ID (cachet serveur SEAL), DigiGo (PIN + OTP), clé USB (signature locale) et E-Houwiya (Mobile ID). Vous pouvez basculer à la volée via le champ method (seal, digigo, usb ou mobileid) dans la requête.

Entreprise ID (SEAL)DigiGoClé USBE-Houwiya
Synchrone ?Oui (~5s)Non (signature manuelle)Non (signature locale)Non (smartphone)
InteractionAucunePIN + OTP utilisateurClé USB sur le poste du signataireValidation sur le smartphone du signataire
Valeur methodsealdigigousbmobileid
Endpoint (POST)/einvoices/process/einvoices/prepare/einvoices/prepare/einvoices/prepare
Cas d'usageERP back-office, volumes élevésSignature personnelle, petits volumesVous détenez déjà votre clé de certificationAucun matériel : le smartphone suffit
RésultatDirect dans la réponsePolling GET /einvoices/{id}Polling GET /einvoices/{id}Polling GET /einvoices/{id}

E-Houwiya (Mobile ID) : le signataire valide depuis son smartphone avec son identité numérique mobile. L'option s'active chez le service de signature pour chaque compte : demandez-la nous avant le premier essai, sans quoi la page de signature ne la propose pas.

Gestion des erreurs

L'API utilise les codes HTTP standards et retourne un payload JSON normalisé. Toutes les erreurs ont la même forme :

CodeHTTPDescription
AUTH_INVALID_KEY401Clé API invalide, inactive ou non autorisée
VALIDATION_FAILED422Champs requis manquants : voir error.details
INVALID_JSON400Corps de requête non JSON valide
INSUFFICIENT_CREDITS402Portefeuille épuisé ou plafond du client atteint (live uniquement)
SEAL_NOT_CONFIGURED412Cachet électronique SEAL non provisionné, ou passphrase du cachet manquante, pour ce client sur l'environnement de la clé (sandbox ou production). Renseignez seal_passphrase_test / seal_passphrase_prod via l'API ou le portail.
METHOD_REQUIRES_PREPARE422method vaut digigo, usb ou mobileid (ou c'est le mode du client sans cachet prêt) sur POST /einvoices/process, qui ne signe que par cachet serveur. Envoyez la facture à POST /einvoices/prepare : elle rend une signing_url.
SIGNER_NOT_CONFIGURED412Le compte de signature de ce client n'est pas encore créé dans l'environnement de la clé (DigiGo, clé USB, E-Houwiya). Créez-le depuis le portail développeur, ou par PATCH /api/v1/clients/{id} avec "provision": "sandbox" ou "production"
SIGNER_EMAIL_MISSING412Signature à distance (digigo, mobileid) sans signer_email renseigné sur le client. Renseignez-le (PATCH /api/v1/clients/{id} ou portail) avec l'adresse du certificat du signataire
DEVELOPER_KEY_NOT_ALLOWED403Clé développeur (sk_) utilisée sur /einvoices/prepare, /process, /resubmit-ttn ou /cancel : l'émission passe obligatoirement par la clé du client concerné (ck_), y compris pour votre propre société
SELLER_MISMATCH422Le seller.tax_id envoyé désigne un autre contribuable que le titulaire de la clé. Refusé avant signature, donc sans consommer de crédit : une facture est signée sous le matricule du compte, et TTN refuserait un document dont l'émetteur ne correspond pas au signataire. Émettez avec la clé du client concerné, ou corrigez le matricule
INVOICE_NUMBER_ALREADY_ISSUED409Ce numéro de facture porte déjà un document accepté par TTN, et le contenu que vous envoyez est différent. Un numéro déposé chez TTN ne peut pas porter deux documents : émettez un avoir, ou utilisez un nouveau numéro. Si vous vouliez seulement redéposer le document existant, appelez POST /einvoices/{id}/resubmit-ttn, qui ne coûte aucun crédit
ALREADY_SIGNED / CANCEL_NOT_SUPPORTED / SIGNATURE_ALREADY_CLOSED409Réponses de POST /einvoices/{signing_id}/cancel : voir Annuler une signature
CANCEL_UNCONFIRMED502Annulation non confirmée par le service de signature : rien n'est annulé ni débité, réessayez
INVOICE_IN_PROGRESS409Une autre requête signe ce numéro en ce moment, ou une signature à la main est ouverte et pas encore terminée. Le signing_id concerné est rendu dans error.details : interrogez-le avec GET /einvoices/{id} plutôt que de réessayer, sinon vous paieriez deux fois la même facture. Pour une signature à la main, error.details.signing_url donne le lien ouvert, pour la terminer. Renvoyer la même facture à /einvoices/prepare ne produit pas cette erreur : le lien vous est rendu ("resumed": true)
PERMISSION_DENIED403Le signing_id consulté appartient à un autre compte. Seuls le client propriétaire et son développeur parent peuvent le consulter
WRONG_ENVIRONMENT403Chaque clé ne lit que son propre environnement. Le document vous appartient, mais il a été émis dans l'autre environnement : une clé ck_test_ ne consulte que des documents de test, une clé ck_live_ que des documents de production. Le message indique quelle clé l'ouvre. Votre clé développeur sk_dev_, elle, voit les deux
NOT_SIGNED409Renvoi TTN demandé sur une facture sans document signé : rien à renvoyer
AWAITING_TTN409Renvoi TTN demandé alors que TTN a déjà accepté le dépôt : la confirmation est relevée toutes les cinq minutes, renvoyer ferait un doublon
SIGNATURE_EXPIRED-Porté par une facture rejected : la signature n'a pas été terminée dans les 7 jours. Aucun crédit débité. Arrêtez d'interroger ce signing_id et préparez à nouveau la facture.
ALREADY_VALIDATED409Renvoi TTN demandé sur une facture déjà confirmée par TTN : le document est définitif
DEPOSIT_DELEGATED409Le dépôt de ce client est assuré par le service de signature : il ne se rejoue pas par l'API
RESIGN_REQUIRED409Renvoi TTN demandé alors que TTN a refusé le document lui-même (next_action = resign) : corrigez la facture et émettez-la à nouveau
DOCUMENTS_PURGED409Renvoi TTN demandé sur une facture dont les documents ont été effacés au terme des 90 jours de conservation : émettez-la à nouveau
TTN_DEPOSIT_FAILED502TTN a refusé ou n'a pas répondu au dépôt : le message de TTN est dans error.message, le document signé est conservé et se renvoie avec POST /einvoices/{id}/resubmit-ttn
TTN_DEPOSIT_FAILED-Présent dans ttn.error de l'objet einvoice : la facture est signée mais le dépôt TTN a échoué (ex. identifiants TTN invalides). Corrigez la configuration TTN du client puis ré-émettez la facture
TTN_CREDENTIALS_MISSING412Le canal de dépôt choisi (soap ou sftp) place le dépôt de notre côté, mais les identifiants TTN du client ne sont pas renseignés. Refusé avant signature : sans eux, la facture serait signée et facturée, puis n'irait nulle part. Renseignez ttn.sandbox.login / ttn.sandbox.password (ou ttn.prod.*). Si ce client n'a pas de compte TTN à lui, utilisez le canal delegated : le service de signature dépose alors sous son propre compte. Le canal delegated n'est jamais concerné par cette erreur.
SIGNING_TOKEN_EXPIRED412Le jeton du service de signature a expiré et son renouvellement automatique a échoué. Réessayer redonnera la même erreur : reprovisionnez le compte de signature du client (PATCH /api/v1/clients/{id} avec "provision": "sandbox" ou "production"), puis ré-émettez. En mode test, error.details.refresh_error donne la raison de l'échec du renouvellement
INVALID_METHOD422Méthode de signature inconnue : method vaut seal, digigo, usb ou mobileid
SIGNER_UNAVAILABLE400Ce mode n'est pas proposé par le service de signature du client (par exemple E-Houwiya) : choisissez-en un autre ou contactez-nous
SIGNER_CIRCUIT_OPEN503Service de signature en panne répétée : circuit breaker activé. Réessayer dans 30-60 sec
RATE_LIMIT429Limite de requêtes par minute dépassée
SIGNING_FAILED503Échec technique du service de signature (timeout, 5xx provider, etc.)
USER_CANCELLED-(DigiGo) Le client final a annulé la signature. Présent dans error de l'objet einvoice au statut rejected

Debug en mode test (clés ck_test_)

En mode test uniquement, les erreurs SIGNING_FAILED retournent un champ error.details.debug contenant le message technique sous-jacent, et GET /einvoices/{id} inclut un bloc debug (état de la signature, erreur de vérification éventuelle) tant que la facture est en cours. Ces champs ne sont jamais présents en mode production (clés live), pour éviter toute fuite d'information.

Exemple de réponse SIGNING_FAILED en mode test
{
  "success": false,
  "data": null,
  "error": {
    "code": "SIGNING_FAILED",
    "message": "La signature électronique a échoué. Veuillez réessayer.",
    "details": {
      "debug": "HTTP 401 Unauthorized"
    }
  }
}

Tableau de résolution des erreurs SIGNING_FAILED

Si vous rencontrez un SIGNING_FAILED, consultez ci-dessous le contenu de error.details.debug pour identifier la cause :

Message dans debugCause probableAction recommandée
HTTP 401
ou Unauthorized
Le jeton de signature configuré pour votre compte est invalide ou expiré. Contactez le support pour faire renouveler votre jeton de signature.
HTTP 402
ou Payment Required
Compte de signature sans crédit ou plan expiré. Contactez le support pour réactiver le service.
HTTP 403
ou Forbidden
L'organisation associée à votre compte n'est pas autorisée pour cet endpoint. Vérifier la method envoyée. Contacter le support si problème persistant.
HTTP 500 ou HTTP 502
Bad Gateway
Le service de signature est temporairement indisponible ou en surcharge. Réessayer dans 1 à 2 minutes. Implémentez un retry avec backoff exponentiel côté client.
HTTP 504
Gateway Timeout
Le service de signature met trop de temps à répondre. Réessayer immédiatement. Si récurrent, contacter le support.
Connection timed out
ou Could not resolve host
Problème réseau côté serveur (souvent une route bloquée par un pare-feu sortant). Réessayer dans quelques minutes. Si persistant, alerter le support.
Idle timeout reached
ou timed out after
Le service de signature met plus de 30 secondes à répondre (charge serveur). Réessayer après 1 minute. Le mode seal est plus rapide que digigo pour les volumes élevés.
Email signataire non configuré L'adresse du signataire n'est pas renseignée sur la fiche du client. Renseignez signer_email (PATCH /api/v1/clients/{id} ou portail) avec l'adresse du certificat DigiGo ou E-Houwiya du signataire.
Passphrase manquante
ou required for seal
Le mode seal exige une passphrase configurée pour l'environnement du client (sandbox ou production). Renseignez seal_passphrase_test / seal_passphrase_prod via l'API (PATCH /clients/{id}) ou le portail, ou basculez vers method: "digigo".
Invoice not found
ou Locked invoice 50010
Tentative de re-signer une facture déjà en cours de signature. Récupérez l'état actuel via GET /einvoices/{id}. Si elle attend encore, renvoyez la même facture à /einvoices/prepare pour récupérer son lien, ou annulez-la (POST /einvoices/{id}/cancel). Si elle est rejected, émettez-la de nouveau : le même numéro est accepté.
Invalid TEIF format
ou erreur de validation XML
Les données envoyées ne respectent pas le format TEIF v1.8.8. Vérifiez vos données : matricule fiscal à 13 caractères pour l’émetteur (7 chiffres + lettre de contrôle + catégorie + nature + 000, ex. 1234567MAM000), forme courte à 8 caractères tolérée pour l’acheteur seulement, TVA dans [0, 7, 13, 19], champs requis.
aucune transaction UUID retournée Réponse anormale du service de signature. Réessayer. Si persistant, contacter le support avec votre invoice_number.
Autre message non listé Erreur non répertoriée - possiblement nouvelle. Contactez le support avec : debug complet, invoice_number, horodatage, et votre Client ID API.
Bonnes pratiques en production
  • Implémentez un retry avec backoff exponentiel pour les erreurs 5xx et timeouts (3 tentatives max : 1s, 3s, 10s)
  • Ne retentez jamais automatiquement les erreurs 4xx (sauf 429 RATE_LIMIT, après le délai indiqué)
  • Conservez l'en-tête X-Request-Id de chaque réponse : citez-le au support, il retrouve votre appel exact

Valider un payload

POST /api/v1/einvoices/validate

Valide la structure du payload sans signer ni déposer chez TTN. Idéal pour debugger votre intégration. Aucun crédit consommé même en mode live.

Corps de la requête

Le même corps pour /einvoices/validate, /einvoices/process et /einvoices/prepare. Ci-dessous l'essentiel ; tous les champs facultatifs (remises, FODEC, devise, retenue, mentions du PDF…) sont décrits dans Objet Invoice, et un exemple avec tous les champs dans Exemple complet.

ChampTypeDescription
invoice_numberrequisstringNuméro de facture côté ERP. 30 caractères au plus, lettres, chiffres, ., - et _ uniquement, ni espace ni / : voir les règles de forme. Le format par défaut d'Odoo, INV/2026/09/0008, est refusé par TTN.
daterequisdate YYYY-MM-DDDate d'émission, jamais dans le futur.
sellerrequisobjectname, address. tax_id facultatif : votre clé le porte déjà, et seuls ses 8 premiers caractères sont comparés si vous l'envoyez.
buyerrequisobjectname, et tax_id (matricule tunisien) ou id_number (CIN, numéro d'entreprise étranger, avec country).
itemsrequisarrayAu moins une ligne : description, quantity, unit_price (HT), vat_rate (0, 7, 13 ou 19). Voir Objet Invoice.
typeoptionnelenumfacture par défaut. Pour un avoir : credit_note et original_invoice_number, voir Avoirs et notes de débit.
pdf_base64optionnelstringVotre PDF de facture encodé en base64. Sans lui, nous le générons. Voir PDF de la facture.

Réponse

La réponse porte deux listes. errors : ce qui empêcherait l'émission, et rend valid faux. warnings : des alertes non bloquantes, qui ne changent ni valid ni le comportement de /einvoices/process et /einvoices/prepare. Affichez-les à l'utilisateur, par exemple dans votre bouton « Vérifier la conformité ».

{
  "success": true,
  "data": {
    "valid": true,
    "errors": [],
    "warnings": [
      "suspension.authorization_number : le numéro de l'attestation d'achat en suspension de TVA manque. C'est la pièce qui justifie la suspension en cas de contrôle."
    ]
  }
}

Avertissements rendus aujourd'hui, pour une vente en suspension de TVA :

SituationPourquoi on vous prévient
Numéro d'attestation absentC'est la pièce qui justifie la suspension en cas de contrôle.
Numéro de bon de commande absentLa mention du bon de commande accompagne la vente en suspension.
Date illisible (date_from, date_to, order_date)Une date hors du format AAAA-MM-JJ n'est ni transmise à TTN ni imprimée.
Période de validité inverséeLa date de début est postérieure à la date de fin.
Facture datée hors de la période de validitéL'attestation ne couvre pas la date de la facture.
Aucune TVA à suspendreToutes les lignes sont à 0 % ou exonérées : envoyez le taux réel (vat_rate: 19), pas exonerated: true.

Signer + déposer (mode SEAL)

POST /api/v1/einvoices/process

Synchrone (~5 secondes). Signature automatique via cachet électronique serveur, dépôt immédiat chez TTN. La réponse contient directement le XML signé (base64), le PDF avec QR code (base64) et la référence TTN. Clé requise : celle du client concerné (ck_) - une clé développeur reçoit 403 DEVELOPER_KEY_NOT_ALLOWED.

Cas d'usage recommandéERP back-office, batch quotidien, volumes élevés. Aucune interaction utilisateur.

Corps de la requête

Le même corps pour /einvoices/validate, /einvoices/process et /einvoices/prepare. Ci-dessous l'essentiel ; tous les champs facultatifs (remises, FODEC, devise, retenue, mentions du PDF…) sont décrits dans Objet Invoice, et un exemple avec tous les champs dans Exemple complet.

ChampTypeDescription
invoice_numberrequisstringNuméro de facture côté ERP. 30 caractères au plus, lettres, chiffres, ., - et _ uniquement, ni espace ni / : voir les règles de forme. Le format par défaut d'Odoo, INV/2026/09/0008, est refusé par TTN.
daterequisdate YYYY-MM-DDDate d'émission, jamais dans le futur.
sellerrequisobjectname, address. tax_id facultatif : votre clé le porte déjà, et seuls ses 8 premiers caractères sont comparés si vous l'envoyez.
buyerrequisobjectname, et tax_id (matricule tunisien) ou id_number (CIN, numéro d'entreprise étranger, avec country).
itemsrequisarrayAu moins une ligne : description, quantity, unit_price (HT), vat_rate (0, 7, 13 ou 19). Voir Objet Invoice.
typeoptionnelenumfacture par défaut. Pour un avoir : credit_note et original_invoice_number, voir Avoirs et notes de débit.
pdf_base64optionnelstringVotre PDF de facture encodé en base64. Sans lui, nous le générons. Voir PDF de la facture.

Codes de réponse

HTTPSignification
200Facture signée et déposée. Réponse complète.
412Cachet non provisionné, ou code PIN du cachet manquant, pour l'environnement de la clé. Voir SEAL_NOT_CONFIGURED.
422METHOD_REQUIRES_PREPARE : DigiGo, clé USB ou E-Houwiya demandés ici. Utilisez POST /einvoices/prepare.
402Crédits insuffisants.
422Validation échouée. Voir error.details.

Préparer une signature (DigiGo, clé USB, E-Houwiya)

POST /api/v1/einvoices/prepare

C'est le point d'entrée de DigiGo, de la clé USB et d'E-Houwiya. POST /einvoices/process ne signe que par cachet serveur : y envoyer "method": "digigo" ou "usb" rend 422 METHOD_REQUIRES_PREPARE.

Asynchrone. Génère le TEIF, crée une transaction de signature et retourne une signing_url vers laquelle vous redirigez le client final (signature à distance ou clé USB locale). Celui-ci signe, puis est redirigé vers votre return_url. Clé requise : celle du client concerné (ck_) - une clé développeur reçoit 403 DEVELOPER_KEY_NOT_ALLOWED. Le signing_id retourné (format sg_...) sert au suivi via GET /einvoices/{signing_id}.

Récupérez le résultat final via :

  • Polling : GET /einvoices/{signing_id} jusqu'à status: validated

Corps de la requête

Le même corps pour /einvoices/validate, /einvoices/process et /einvoices/prepare. Ci-dessous l'essentiel ; tous les champs facultatifs (remises, FODEC, devise, retenue, mentions du PDF…) sont décrits dans Objet Invoice, et un exemple avec tous les champs dans Exemple complet.

ChampTypeDescription
invoice_numberrequisstringNuméro de facture côté ERP. 30 caractères au plus, lettres, chiffres, ., - et _ uniquement, ni espace ni / : voir les règles de forme. Le format par défaut d'Odoo, INV/2026/09/0008, est refusé par TTN.
daterequisdate YYYY-MM-DDDate d'émission, jamais dans le futur.
sellerrequisobjectname, address. tax_id facultatif : votre clé le porte déjà, et seuls ses 8 premiers caractères sont comparés si vous l'envoyez.
buyerrequisobjectname, et tax_id (matricule tunisien) ou id_number (CIN, numéro d'entreprise étranger, avec country).
itemsrequisarrayAu moins une ligne : description, quantity, unit_price (HT), vat_rate (0, 7, 13 ou 19). Voir Objet Invoice.
typeoptionnelenumfacture par défaut. Pour un avoir : credit_note et original_invoice_number, voir Avoirs et notes de débit.
pdf_base64optionnelstringVotre PDF de facture encodé en base64. Sans lui, nous le générons. Voir PDF de la facture.

En plus, pour une signature à la main (DigiGo, clé USB, E-Houwiya)

ChampTypeDescription
methodoptionnelenumseal, digigo, usb, mobileid. Défaut : le mode configuré sur le client. seal est aussi accepté ici et signe aussitôt par cachet serveur, sans signing_url. mobileid = E-Houwiya (Mobile ID) : l'option s'active chez le service de signature pour chaque compte : demandez-la nous avant le premier essai, sans quoi la page de signature ne la propose pas.
return_urlrequis (digigo, usb, mobileid)URL HTTPSURL de votre ERP où le client est redirigé après signature. Obligatoire dès que la signature passe par un navigateur, c'est-à-dire pour digigo, usb et mobileid : sans elle, votre utilisateur termine sa signature et n'a aucun endroit où revenir. Son absence est refusée en 422 VALIDATION_FAILED. Seul seal s'en passe, personne n'étant devant l'écran. Doit être en HTTPS (les URL http:// sont rejetées). Vos propres paramètres de query sont préservés.
Sécurité de la redirection (à lire) Le return_url sert uniquement à ramener l'utilisateur dans votre interface. C'est une redirection navigateur : ne considérez jamais une facture comme signée parce que le navigateur est revenu sur cette URL - elle pourrait être appelée directement. Pour confirmer le statut réel, utilisez toujours un canal authentifié serveur-à-serveur :
  • Polling : GET /einvoices/{signing_id} avec votre clé API (header X-Api-Key).
Pour vous aider à corréler le retour, nous ajoutons automatiquement ?efacture_invoice={invoice_number}&signing_id={signing_id} à votre return_url. Le signing_id (format sg_...) est celui à utiliser avec GET /einvoices/{signing_id}. Utilisez ces paramètres seulement pour retrouver la facture concernée, jamais comme preuve de signature.
Ne présumez jamais où mène la signing_url C'est une adresse opaque : redirigez-y votre utilisateur sans l'analyser. Selon le service de signature qui traite le compte, elle mène soit directement à la page de signature, soit à une page d'attente hébergée sur efacturetn.com qui ouvre cette page puis ramène l'utilisateur sur votre return_url dès que la signature est acquise. Certains services de signature n'honorent aucune adresse de retour, et nous comblons cette absence à votre place.
  • Le contrat ne change pas : vous recevez une signing_url, votre utilisateur revient sur return_url. Changer de service de signature ne demande aucune modification de votre côté.
  • Ne codez donc aucun domaine en dur, n'ouvrez pas cette page dans une iframe, et ne rejouez pas une signing_url expirée : annulez la signature (POST /einvoices/{id}/cancel) puis redemandez POST /einvoices/prepare, ou attendez can_prepare_again_at. Tant qu'elle attend, la renvoyer à /prepare vous rend le même lien, sans second crédit (voir Renvoyer la même facture).
  • Si votre utilisateur ferme la page, rien n'est perdu : la signature est récupérée de notre côté en tâche de fond, la facture poursuit son dépôt TTN comme si le navigateur était revenu. GET /einvoices/{signing_id} reste la source de vérité. Le retour navigateur est un confort, jamais le mécanisme.

Récupérer une e-facture

GET /api/v1/einvoices/{id}

Retourne le statut et les fichiers d'une e-facture. Utilisé principalement pour le polling après POST /einvoices/prepare. Seuls le client propriétaire de la facture et son développeur parent peuvent la consulter : toute autre clé reçoit 403 PERMISSION_DENIED.

Chaque clé ne lit que son propre environnement. Un document émis en test se consulte avec ck_test_, un document émis en production avec ck_live_ ; l'autre sens répond 403 WRONG_ENVIRONMENT, et il en va de même pour POST /einvoices/{id}/resubmit-ttn. Ce n'est pas une restriction de propriété, le document est bien le vôtre : c'est une séparation des environnements, parce qu'une clé de test se distribue plus largement qu'une clé de production et ne doit pas ouvrir vos documents fiscaux réels. Votre clé développeur sk_dev_ ne désigne aucun environnement et voit les deux, chez tous vos clients.

Cycle de vie

draft → teif_generated → pending_signature → signed → submitted_ttn → validated
                                                                                            ↘ rejected
StatutDescription
pending_signatureEn attente que le client signe sur signing_url
signedSignature reçue, dépôt TTN en cours
submitted_ttnDéposé, attente validation TTN
validatedTTN a accepté. xml_base64 et pdf_base64 sont disponibles. Le QR officiel et la référence définitive arrivent quelques minutes plus tard : voir Documents finaux & QR.
rejectedAnnulé ou rejeté : voir les blocs signature et ttn pour savoir quel étage a échoué, et ttn.next_action pour savoir quoi faire (voir ci-dessous)

Deux étages décrits séparément : signature et ttn

Le champ status est la synthèse globale du cycle. Un rejected peut signifier deux choses très différentes : signature refusée, ou facture signée mais dépôt TTN refusé. La réponse contient donc deux blocs dédiés :

{
  "status": "rejected",
  "signature": {
    "status": "signed",                  // pending | signed | rejected
    "signed_xml_available": true
  },
  "ttn": {
    "status": "rejected",                // not_reached | submitted | validated | rejected
    "reference": null,
    "error": { "code": "TTN_DEPOSIT_FAILED", "message": "Erreur TTN: ..." }
  }
}
Scénariostatussignature.statusttn.status
En attente du signatairepending_signaturependingnot_reached
Signature refusée / annuléerejectedrejectednot_reached
Signée, dépôt TTN refusérejectedsignedrejected + error
Signée, déposée (SFTP), réponse TTN en attentesubmitted_ttnsignedsubmitted
Validéevalidatedsignedvalidated + reference

En cas de dépôt refusé (ttn.status: rejected) : le XML signé reste disponible, corrigez la cause (ex. identifiants TTN du client) puis ré-émettez la facture (nouveau /prepare ou /process). Un rejet est final pour ce signing_id.

Stratégie de pollingBackoff exponentiel recommandé : 5s, 10s, 20s, 30s, 60s... avec timeout total de 30 minutes. Arrêtez d'interroger dès que status vaut rejected, ou validated avec reference_confirmee: true. Une signature non terminée en 7 jours passe d'elle-même en rejected (SIGNATURE_EXPIRED). Le service de signature n'est interrogé qu'une fois toutes les 30 secondes par facture : interroger plus souvent rend le même statut.

Après un refus TTN : renvoyer ou re-signer

Quand status = rejected et qu'un document signé existe, l'API dit ce qu'il faut faire, dans ttn.next_action (GET), dans next_action (réponses de /process et /prepare en signed_ttn_rejected). Deux valeurs, pas plus : votre module n'a pas à lire le texte de TTN.

next_actionCe que ça veut direLe bouton à montrer
resubmitLe document signé n'est pas en cause : TTN indisponible, délai dépassé, compte TTN du client inexistant ou désactivé, certificat non déclaré. Une fois la cause levée, le même fichier repart.Renvoyer TTN : POST /einvoices/{id}/resubmit-ttn, sans nouvelle signature, sans crédit.
resignLe document est en cause : schéma violé (par exemple un numéro mal formé), numéro déjà déposé, émetteur différent du compte. Renvoyer le même fichier répondrait la même chose, et l'API le refuse (409 RESIGN_REQUIRED).Corriger puis émettre à nouveau : nouvelle facture, nouvelle signature (un crédit en production). Vérifiez d'abord avec /einvoices/validate.
En cas de doute, l'API répond resubmit. Un renvoi inutile ne coûte rien, TTN redit la même chose et le document est gardé. Une re-signature inutile coûte un crédit et un PIN au signataire. Une signature ne se recolle jamais sur un fichier corrigé : la moindre modification du document invalide la signature, c'est le principe même de la signature électronique.

Renvoyer chez TTN sans re-signer

POST /api/v1/einvoices/{id}/resubmit-ttn

Pour le bouton « Renvoyer TTN » de votre ERP. Quand le dépôt a échoué parce que TTN était indisponible, ou qu'il a été refusé pour une cause extérieure au document, cet appel redépose le document signé tel qu'il a été signé, sans nouvelle signature et sans consommer de crédit.

ÉlémentValeur
{id}Le signing_id rendu par /einvoices/prepare ou /einvoices/process à l'émission, le même que pour GET /einvoices/{id}. Un appel par facture : un client qui a plusieurs documents signés fait un appel par identifiant.
AuthentificationLa clé API du client émetteur, ck_test_ ou ck_live_. Jamais la clé développeur sk_ (403 DEVELOPER_KEY_NOT_ALLOWED). Un identifiant appartenant à un autre compte répond 403 PERMISSION_DENIED.
CorpsVide.

Quand l'utiliser

État de la factureRéponse
rejected avec error.code = TTN_DEPOSIT_FAILED, TTN_REJECTED ou SFTP_DEPOSIT_FAILED200 : le fichier signé est redéposé. Le statut passe à validated (référence provisoire) ou submitted_ttn, puis la confirmation se lit comme d'habitude par GET /einvoices/{id}.
pending_signature, aucun document signé409 NOT_SIGNED : rien à renvoyer, attendez la signature ou refaites un /prepare.
validated ou submitted_ttn, reference_confirmee = false409 AWAITING_TTN : TTN a déjà accepté le dépôt, le renvoyer ferait un doublon. La confirmation est relevée toutes les cinq minutes.
reference_confirmee = true409 ALREADY_VALIDATED : le document est définitif.
Client dont le dépôt est assuré par le service de signature409 DEPOSIT_DELEGATED : ce dépôt ne se rejoue pas par l'API, contactez le support avec le signing_id.
rejected parce que TTN a refusé le document (next_action = resign)409 RESIGN_REQUIRED : renvoyer le même fichier répondrait la même chose. Corrigez la facture et émettez-la à nouveau.
TTN refuse ou ne répond pas502 TTN_DEPOSIT_FAILED avec le message de TTN. La facture reste rejected, vous pouvez réessayer plus tard.

Réponse

{
  "success": true,
  "data": {
    "signing_id": "sg_146c4253ea74656dbedc5b57",
    "invoice_number": "FACT-2026-0012",
    "status": "validated",
    "ttn": { "status": "validated", "reference": "TTN-SOAP-1234567", "reference_confirmee": false },
    "ttn_reference": "TTN-SOAP-1234567",
    "resigned": false,
    "credits_consumed": 0,
    "message": "Document signé renvoyé chez TTN, sans nouvelle signature."
  },
  "error": null
}
Ce que le bouton ne doit pas faire : refaire un /process ou un /prepare pour la même facture. Cela produirait un second document signé, un second crédit consommé, et deux dépôts pour un seul numéro. Le renvoi ne s'affiche que quand status = rejected et qu'un document signé existe (signature.signed_xml_available = true).

Annuler une signature en attente

POST /api/v1/einvoices/{signing_id}/cancel

Arrête une signature ouverte par POST /einvoices/prepare (DigiGo, clé USB, E-Houwiya) et pas encore faite. Clé requise : celle du client concerné, sur l'environnement de la facture. Aucun corps de requête.

Aucun crédit n'est consommé par une annulation, et une facture ne peut pas être débitée deux fois : le crédit n'est pris qu'au moment où une signature réelle revient. Si votre utilisateur signe à l'instant même où vous annulez, nous relisons le statut chez le service de signature avant de répondre : la facture est alors traitée comme signée, avec un seul crédit, et le document vous est conservé.

Réponses

HTTPCodeSignification
200-"status": "cancelled", "credits_consumed": 0. Rien n'a été signé. Le numéro de facture se prépare de nouveau aussitôt.
409ALREADY_SIGNEDTrop tard : la facture était signée. Elle suit son cours normal, un crédit. Lisez-la avec GET /einvoices/{signing_id} ; pour l'annuler comptablement, émettez un avoir.
409SIGNATURE_ALREADY_CLOSEDRien à annuler : la tentative est déjà close (refusée, annulée ou abandonnée). Le numéro est libre.
409CANCEL_NOT_SUPPORTEDLe service de signature de ce client ne permet pas l'annulation. Sans signature, la tentative s'abandonne d'elle-même à error.details.can_prepare_again_at (30 minutes après son ouverture).
502CANCEL_UNCONFIRMEDL'annulation n'a pas pu être confirmée. Rien n'est tenu pour annulé, rien n'est débité : réessayez dans un instant.

Avoirs et notes de débit

Un avoir s’envoie par les mêmes endpoints qu’une facture. Rien de séparé : seuls deux champs changent dans le corps.

ChampValeurObligatoire
type
alias : document_type
credit_note pour un avoir, debit_note pour une note de débit.
Sont aussi acceptés : avoir, creditnote, credit-note, note_debit, debit-note. Absent = facture.
oui
original_invoice_number
alias : parent_invoice_number, facture_origine_numero
Le numéro de la facture annulée ou corrigée. l’un des deux
original_ttn_reference
alias : facture_origine_ttn
La référence TTN de la facture d’origine, si vous l’avez. l’un des deux
Les montants restent POSITIFS. N’envoyez pas de quantités ni de prix négatifs : c’est le type qui porte le sens du document, et le TEIF applique le signe lui-même. Des lignes négatives sur un credit_note produiraient un document qui s’annule deux fois.
{
  "type": "credit_note",
  "original_invoice_number": "F-2026-0042",
  "invoice_number": "AV-2026-0007",
  "date": "2026-09-18",
  "seller": { "name": "MA SOCIETE", "tax_id": "1234567ABM000", "address": "Rue X, Tunis" },
  "buyer":  { "name": "MON CLIENT", "tax_id": "7654321ABM000", "address": "Rue Y, Sfax" },
  "items": [
    { "description": "Retour marchandise", "quantity": 2, "unit_price": 150.000, "vat_rate": 19 }
  ],
  "stamp_duty": false
}

Timbre fiscal et retenue à la source sur un avoir

Ils ne sont jamais répliqués automatiquement depuis la facture d’origine : l’API ne connaît que le corps que vous envoyez. Si votre avoir doit les porter, envoyez-les explicitement (stamp_duty, champs de retenue). Sinon, laissez-les absents.

Documents finaux & QR code

Une e-facture produit quatre documents, et ils n'arrivent pas tous en même temps. C'est la source de confusion la plus fréquente : ne pas confondre ce que vous avez signé avec ce que TTN a validé.

ChampContenuDisponibleÀ archiver
xml_base64 Le TEIF que vous avez signé et que nous avons déposé. Dès la signature non
xml_validettn_base64 Le document final : le même, plus le bloc <RefTtnVal> ajouté par TTN (référence officielle, date de traitement, cachet). Après confirmation oui
ttn.qr_png_base64 Le QR officiel seul, image PNG. Après confirmation si vous composez votre propre document
pdf_base64 Notre PDF, QR déjà apposé. Dès la signature, mis à jour à la confirmation au choix

Le QR ne se fabrique pas

Le QR d'une facture El Fatoora est le Cachet Électronique Visible. C'est TTN qui le produit, sous forme d'image PNG, et il est vérifiable par les outils officiels. Un QR reconstruit de votre côté aura la même allure mais échouera à la vérification : l'empreinte et la clé qu'il contient sont calculées par TTN.

Vous n'avez donc rien à calculer. Soit vous utilisez notre pdf_base64, soit vous apposez ttn.qr_png_base64 telle quelle sur votre propre document, sans ré-encodage ni retouche.

Le QR arrive en différé

TTN remplit le bloc <RefTtnVal> après le dépôt : ce bloc est exclu de votre signature, c'est pourquoi TTN peut y écrire sans l'invalider. Nous ré-interrogeons TTN toutes les cinq minutes pour le récupérer.

"ttn": {
  "status": "validated",
  "reference": "073620200053562920196810312",
  "reference_confirmee": true,          // false = accusé de dépôt, pas encore la référence définitive
  "confirmed_at": "2026-09-01T14:32:07+00:00",
  "qr_png_base64": "iVBORw0KGgo..."     // null tant que reference_confirmee vaut false
}
Le champ qui tranchereference_confirmee vous dit si vous tenez le document définitif. Tant qu'il vaut false, la référence portée n'est qu'un accusé de dépôt et le QR n'est pas encore l'officiel : n'imprimez pas la facture pour votre client.

Comment récupérer la confirmation

Rappelez GET /einvoices/{id} jusqu'à ce que reference_confirmee passe à true. Comptez quelques minutes ; un intervalle de 5 minutes suffit, plus court ne sert à rien.

Deux étapes de dépôt, pas une

Ce que vous lisezCe que cela signifie
status: submitted_ttnTTN a accepté le dépôt mais ne nous a rendu aucun identifiant. La facture est déposée, pas encore confirmée. Continuez d'interroger.
status: validated, reference_confirmee: falseTTN a rendu un identifiant au dépôt, la référence est provisoire. Continuez d'interroger.
status: validated, reference_confirmee: trueLa référence est définitive : allez chercher le QR et le XML validé.

Combien de temps nous gardons ces documents

90 jours, et pas un de plus. Passé ce délai, les quatre documents et le corps de votre requête sont effacés de nos serveurs, ainsi que les journaux d'appels et de transactions correspondants. Nous conservons la ligne de la facture, c'est-à-dire son numéro, ses dates, son statut et sa référence TTN : un signing_id continue donc de répondre, en indiquant la date d'effacement dans documents_purged_at.

Pourquoi nous effaçons. Deux raisons, et aucune n'est une limitation de service :

  • La confidentialité. Les factures de vos clients contiennent leurs montants, leurs partenaires et leurs adresses. Ce sont leurs affaires, pas les nôtres. Garder sans fin ce que nous n'avons plus de raison de garder serait une décision par défaut, et nous préférons ne pas la prendre à leur place.
  • La performance. Une base qui ne fait que grossir finit par ralentir tout le monde, y compris vos appels. Effacer ce qui a servi garde nos temps de réponse là où ils sont.
ConséquenceCe qu'il faut faire
GET /einvoices/{id} rend les champs XML et PDF à null, avec documents_purged_at renseigné.Rien : ce sont vos archives qui font foi.
POST /einvoices/{id}/resubmit-ttn répond 409 DOCUMENTS_PURGED.Émettez la facture à nouveau depuis votre ERP.
C'est à vous d'archiver, et il n'y a pas d'exception. Téléchargez xml_validettn_base64 dès que reference_confirmee passe à true, et conservez-le avec le PDF dans votre système : c'est le document qui porte la référence et le cachet de TTN. Si vous ne les archivez pas de votre côté, vous ne les retrouverez pas chez nous après 90 jours. Le délai s'applique à toutes les factures, y compris celles que TTN n'a jamais confirmées. Nos 90 jours sont un filet de sécurité opérationnel, pas un archivage légal.

Si vous préférez que nous gardions vos archives

Archiver dix ans de factures signées, les mettre à l'abri et savoir les retrouver n'est pas le métier d'un intégrateur. Nous proposons de le faire à votre place, en option, pour chaque client que vous passez en production.

Ce que vous obtenezDétail
Conservation 10 ansLe document signé, le document validé par TTN et le PDF, conservés chiffrés, au lieu des 90 jours.
Accès à tout momentUne clé dédiée pour récupérer les archives de vos clients quand vous en avez besoin, sans passer par notre support.
TarifPar client et par an, en plus du raccordement. Le montant figure dans votre portail développeur, à la page des tarifs.

L'option se commande depuis votre portail développeur, client par client, au moment de la mise en production ou à son renouvellement.

Une copie dans le Google Drive de votre client

Votre client veut garder lui-même un exemplaire de ses factures, dans un espace qui lui appartient. Nous déposons pour lui, dans un compte Google Drive, une archive par e-facture validée, sans que votre intégration n'ait rien à appeler.

QuestionRéponse
Que contient chaque archive ?Un fichier ZIP par e-facture : le document signé, le document validé par TTN, le PDF et un fichier de métadonnées (numéro, dates, référence TTN).
Où est-elle rangée ?Dans un dossier eFactureTN, puis un sous-dossier par client, par année et par mois.
Quand part-elle ?Automatiquement, peu après la validation. En dépôt SOAP, après la confirmation de TTN, pour que l'archive porte le document définitif.
Quel compte Google ?Celui que vous connectez depuis la fiche du client dans votre portail. Un même compte peut servir à plusieurs de vos clients : chacun reçoit son propre dossier.
Sous quelle forme ?Déchiffrée, pour être lisible par son destinataire, et transmise par une connexion sécurisée (HTTPS). Chez nous, les documents restent chiffrés.
Et si l'accès Google est retiré ou expire ?Rien n'est perdu : les archives attendent, vous êtes prévenu par e-mail, et elles repartent seules dès que le compte est reconnecté.
Et les factures déjà émises ?Le portail propose, pour chaque client, le téléchargement des archives d'un mois entier.
TarifPar client et par an, même si plusieurs clients partagent le même compte Google. Un tarif groupé s'applique quand la copie est prise avec l'archivage 10 ans. Les montants figurent dans votre portail développeur.
Une fois déposée, l'archive appartient au compte Google. Sa conservation, son partage et sa suppression relèvent alors de son titulaire. Si vous voulez que nous gardions aussi notre propre exemplaire pendant dix ans, prenez les deux options ensemble.

Solde de crédits

GET /api/v1/credits/balance

Retourne le solde courant de crédits, le total utilisé et le total acheté.

Statut de la plateforme

GET /api/v1/status

Retourne l'état global de la plateforme. Aucune authentification requise, aucun crédit consommé. Utilisez-le comme sonde depuis votre ERP ou votre supervision.

Le seul endpoint publicIl ne demande pas de clé. Les vérifications sont automatiques et horaires : ce statut n'est pas saisi à la main, il reflète l'état réellement mesuré.
curl https://efacturetn.com/api/v1/status

Réponse

{
  "success": true,
  "data": {
    "status": "operational",
    "version": "1.0.0",
    "teif_version": "1.8.8",
    "all_operational": true,
    "last_check": "17/07/2026 09:00"
  },
  "error": null
}

Champs

ChampTypeDescription
statusstringÉtat global. Toujours celui du service le plus dégradé : si un seul composant souffre, vous le voyez ici. Valeurs : operational, degraded, partial_outage, major_outage, maintenance.
all_operationalboolRaccourci : true seulement si tous les services sont pleinement opérationnels.
last_checkstringDate de la dernière vérification automatique.
versionstringVersion de l'API.
teif_versionstringVersion TEIF supportée.

Disponibilité par service

GET /api/v1/status/services

Détail service par service : statut, latence mesurée et taux de disponibilité. Nécessite une clé API. Aucun crédit consommé, en test comme en live.

Paramètres

ChampTypeDescription
daysoptionnelintFenêtre du taux de disponibilité, en jours. Entre 1 et 180. Défaut : 90.
curl "https://efacturetn.com/api/v1/status/services?days=30" \
  -H "X-Api-Key: ck_test_xxxxxxxxxxxx"

Réponse

{
  "success": true,
  "data": {
    "status": "degraded",
    "all_operational": false,
    "last_check": "17/07/2026 09:00",
    "uptime_window_days": 30,
    "services": [
      {
        "slug": "signature",
        "name": "Signature électronique",
        "description": "Service de signature",
        "status": "operational",
        "status_label": "Operationnel",
        "operational": true,
        "latency_ms": 214,
        "detail": null,
        "uptime_percent": 99.86
      },
      {
        "slug": "ttn_prod",
        "name": "TTN production (El Fatoora)",
        "description": "Envoi e-factures (production)",
        "status": "partial_outage",
        "status_label": "Panne partielle",
        "operational": false,
        "latency_ms": null,
        "detail": "Perturbations en cours",
        "uptime_percent": 98.40
      },
      {
        "slug": "ttn_sandbox",
        "name": "TTN sandbox (test)",
        "description": "Environnement de test",
        "status": "operational",
        "status_label": "Operationnel",
        "operational": true,
        "latency_ms": 187,
        "detail": null,
        "uptime_percent": 99.90
      }
    ]
  },
  "error": null
}

Champs d'un service

ChampTypeDescription
slugstringIdentifiant stable du service. Utilisez-le dans votre code : le libellé peut changer, pas le slug. Valeurs : platform, signature, ttn_prod, ttn_sandbox, api, tej, email.
statusstringMêmes valeurs que l'état global.
operationalboolRaccourci pour status === "operational".
latency_msint / nullLatence mesurée. Renseignée uniquement pour les services sondés par le réseau, null sinon.
detailstring / nullMotif, renseigné uniquement quand le service n'est pas opérationnel. null le reste du temps.
uptime_percentfloat / nullTaux de disponibilité sur la fenêtre demandée. null tant qu'aucun historique n'est disponible.

Exemple : couper la file d'envoi quand TTN tombe

$res = json_decode(file_get_contents('https://efacturetn.com/api/v1/status'), true);

if ($res['data']['status'] !== 'operational') {
    // Ne pas envoyer maintenant : reprendre le lot plus tard
    $this->queue->pause('Plateforme e-facture indisponible');
}
Bon réflexeSondez au maximum une fois par minute. Le statut est recalculé chaque heure : interroger plus souvent ne vous apprendra rien de plus et consommera votre quota de requêtes.

Gestion des clients (API)

Un compte développeur peut gérer ses clients directement depuis son ERP, via l'API. Chaque client possède sa configuration TTN (sandbox et production), sa méthode de signature, son plafond de crédits et ses propres clés API.

AuthentificationCes endpoints utilisent la clé de votre compte développeur (pas la clé d'un client). Ils opèrent sur vos clients rattachés. Une clé de client est refusée (403).

Confidentialité des secrets

Les mots de passe TTN et SFTP ne sont jamais renvoyés en clair. En lecture, ils sont masqués par des astérisques dont le nombre correspond à la longueur enregistrée (ex. "password": "********"). Vous pouvez les écrire (création / modification) mais jamais les relire.

Champs d'un client

ChampTypeDescription
namestringNom du client (obligatoire à la création)
matriculestringMatricule fiscal (identifiant TTN)
signer_methodstringseal (cachet serveur), usb (clé locale), digigo (à distance) ou mobileid (E-Houwiya, smartphone)
signer_emailstringEmail du signataire : l'adresse sous laquelle il détient son certificat DigiGo ou E-Houwiya, celle déclarée chez l'autorité de certification. Une autre adresse, même valide, donne sur la page de signature « certificat introuvable pour l'email fourni ». Obligatoire pour digigo et mobileid (sinon SIGNER_EMAIL_MISSING, HTTP 412), inutile pour seal et usb. Il se règle sur le client (ici, ou dans le portail développeur), pas dans la facture : il est lu et transmis au service de signature à chaque signature.
seal_passphrase_teststringPassphrase du cachet électronique sandbox, requise pour la méthode seal en test. Le cachet sandbox et le cachet production sont deux organisations distinctes, avec potentiellement des passphrases différentes. Masquée en lecture.
seal_passphrase_prodstringPassphrase du cachet électronique production, requise pour la méthode seal en production. Masquée en lecture.
usb_signing_tool_urlstringLecture seule. Lien de téléchargement direct de l'outil de signature USB (Windows) à installer sur le poste du signataire. Distribuez-le à vos clients depuis votre ERP.
submission_channelstringQui dépose la facture au TTN, une fois signée. delegated (défaut) : le service de signature dépose lui-même, sous son propre compte TTN : votre client n'a pas besoin d'un compte El Fatoora à lui. soap : nous déposons via le web service TTN du client, la référence est connue immédiatement. sftp : nous déposons le XML signé dans le dossier in de son compte TTN, et la réponse est relevée plus tard dans out : la facture reste alors submitted jusqu'au relevé, sans référence. Toute autre valeur est refusée.
credit_capint / nullPlafond de crédits du client. null = illimité (dans la limite du portefeuille)
ttn.sandbox / ttn.prodobjetlogin + password du compte TTN par environnement
sftp.sandbox / sftp.prodobjethost, port, username, password, in_dir, out_dir (canal SFTP)
provisionstringOptionnel (création / modification). sandbox, production ou both : crée ou met à jour le compte de signature du client, quel que soit son mode de signature. L'opération est idempotente (création la première fois, mise à jour ensuite). Le résultat, y compris le message d'erreur éventuel du service, est renvoyé dans provisioning. Sans provision, tout PATCH met à jour automatiquement les environnements déjà connectés (raison sociale, matricule, accès TTN). L'email du signataire, lui, n'est pas transmis ici : il part avec chaque signature.

En lecture, chaque client renvoie aussi son suivi : credits (cap, used, remaining) et stats (total, validated, rejected). De quoi afficher un mini tableau de bord par client dans votre ERP.

Créer un client

POST /api/v1/clients

Crée un client et retourne ses clés API (ck_test_ / ck_live_). Ces clés restent récupérables à tout moment via GET /clients/{id}. Ajoutez "provision": "sandbox" (ou production / both) pour créer en même temps le compte de signature du client.

POST /api/v1/clients
X-Api-Key: sk_dev_VOTRE_CLE_DEVELOPPEUR
Content-Type: application/json

{
  "name": "Société ABC",
  "matricule": "1234567AAM000",
  "signer_method": "seal",
  "signer_email": "signataire@societe-abc.tn",
  "seal_passphrase_test": "123456",
  "seal_passphrase_prod": "987654",
  "submission_channel": "soap",
  "credit_cap": 500,
  "ttn": {
    "sandbox": { "login": "abc_test", "password": "secret1" },
    "prod":    { "login": "abc_prod", "password": "secret2" }
  }
}

Réponse (201) :

{
  "success": true,
  "data": {
    "id": "018f...-....",
    "name": "Société ABC",
    "matricule": "1234567AAM000",
    "signer_method": "seal",
    "signer_email": "signataire@societe-abc.tn",
    "submission_channel": "soap",
    "active": true,
    "live_enabled": true,
    "credits": { "cap": 500, "used": 0, "remaining": 500 },
    "stats": { "total": 0, "validated": 0, "rejected": 0 },
    "ttn": {
      "sandbox": { "login": "abc_test", "password": "*******" },
      "prod":    { "login": "abc_prod", "password": "*******" }
    },
    "keys": { "test": "ck_test_...", "live": "ck_live_..." }
  }
}

Si vous avez demandé un provision, la réponse contient un bloc provisioning par environnement :

"provisioning": {
  "sandbox": { "success": true,  "message": "Compte de signature créé (environnement test)." },
  "production": { "success": false, "message": "L'organisation existe déjà chez le prestataire..." }
}

La création du client réussit même si le provisioning échoue : corrigez les accès puis relancez via PATCH avec provision. Utilisez ensuite la clé du client (keys.live / keys.test) pour émettre ses e-factures via /einvoices/process ou /einvoices/prepare.

Lister les clients

GET /api/v1/clients

Retourne tous vos clients (secrets masqués) avec leurs crédits et statistiques, plus le solde de votre portefeuille partagé.

{
  "success": true,
  "data": {
    "clients": [
      { "id": "...", "name": "Société ABC",
        "credits": { "cap": 500, "used": 180, "remaining": 320 },
        "stats": { "total": 200, "validated": 190, "rejected": 10 }, ... }
    ],
    "wallet": { "balance": 4200, "total_purchased": 5000 }
  }
}

Lire un client

GET /api/v1/clients/{id}

Retourne un client : configuration, crédits, statistiques et ses clés API (keys.test / keys.live, préfixe ck_). Les mots de passe TTN/SFTP restent masqués, mais les clés API du client sont renvoyées en clair pour vous permettre de les récupérer à tout moment.

Modifier un client

PATCH /api/v1/clients/{id}

Mise à jour partielle : seuls les champs envoyés sont modifiés. Envoyez uniquement un mot de passe TTN/SFTP pour le remplacer ; omettez-le pour le conserver. Ajoutez "provision" pour recréer ou mettre à jour le compte de signature.

PATCH /api/v1/clients/018f...
X-Api-Key: sk_dev_VOTRE_CLE_DEVELOPPEUR

{ "credit_cap": 800, "submission_channel": "sftp",
  "sftp": { "prod": { "host": "sftp.ttn.tn", "port": 22, "username": "abc",
                      "password": "xxx", "in_dir": "/in", "out_dir": "/out" } } }

Supprimer un client • Régénérer les clés

DELETE /api/v1/clients/{id}

Supprime le client.

POST /api/v1/clients/{id}/keys

Régénère les clés API du client (les anciennes cessent immédiatement de fonctionner). Les nouvelles clés sont retournées, et restent récupérables via GET /clients/{id}.

Rate limits

Par défaut : 60 requêtes par minute par clé API. Sliding window, comptage atomique. Configurable sur demande pour les volumes élevés.

En cas de dépassement : réponse 429 RATE_LIMIT. Implémentez un backoff exponentiel côté client.

Objet Invoice

ChampTypeDescription
invoice_numberrequisstringNuméro de facture côté ERP. 30 caractères au plus, lettres, chiffres, ., - et _ uniquement, ni espace ni / : voir les règles de forme. Le format par défaut d'Odoo, INV/2026/09/0008, est refusé par TTN.
daterequisdateDate d'émission YYYY-MM-DD.
due_dateoptionneldateDate d'échéance.
typeoptionnelenumNature du document. facture (défaut), avoir, note_debit, honoraire, proforma. Alias : document_type. Un avoir produit un TEIF de type I-12 et doit référencer sa facture d'origine (voir ci-dessous).
original_invoice_numberavoirstringNuméro de la facture d'origine, requis sur un avoir ou une note de débit : sans lui TTN ne peut pas rattacher le document. Alias : facture_origine_numero, parent_invoice_number. Ajoutez original_ttn_reference si vous connaissez la référence TTN de l'origine.
delivery_dateoptionneldateDate de livraison YYYY-MM-DD, quand elle diffère de la date d'émission.
currencyoptionnelstringDevise ISO 4217. Défaut TND (3 décimales). Pour les factures offshore/export : EUR ou USD (2 décimales). TTN ne reçoit que ces trois devises : toute autre (GBP, CHF…) est refusée en 422 VALIDATION_FAILED, avant signature et sans crédit consommé - conservées telles quelles, aucune conversion. En devise étrangère : ni TVA, ni timbre fiscal (régime export), le montant est en TTC = HT.
seller.namerequisstringRaison sociale du vendeur.
seller.tax_idoptionnelstringMatricule fiscal du vendeur. Facultatif : votre clé le porte déjà, et c'est celui du compte qui part dans le document, au format complet exigé par TTN. Si vous l'envoyez, seuls les 8 premiers caractères sont comparés : 1234567M et 1234567MAM000 sont équivalents. Un contribuable différent est refusé en SELLER_MISMATCH, avant signature.
seller.addressrequisstringAdresse (rue). Aussi : address_line_2, postal_code, city, governorate, country, phone, email.
buyer.namerequisstringRaison sociale / nom du client.
buyer.tax_idrequis*stringMatricule fiscal tunisien du client. *Il est exigé ou buyer.id_number, jamais les deux. Ne l'envoyez pas pour un client étranger : ce champ n'accepte que la forme tunisienne, et un numéro d'un autre pays y est refusé en VALIDATION_FAILED.
buyer.id_numberrequis*stringNuméro d'identification libre, à la place du matricule. C'est ici que vont la CIN d'un particulier et le numéro d'entreprise d'un client étranger (SIREN, SIRET, numéro de TVA intracommunautaire, NIF). Les espaces et la ponctuation sont retirés avant l'envoi. Alias : cin, piece_identite, passport.
buyer.addressoptionnelstringAdresse (rue) du client.
buyer.address_line_2optionnelstringComplément d'adresse.
buyer.postal_codeoptionnelstringCode postal.
buyer.cityoptionnelstringVille.
buyer.governorateoptionnelstringGouvernorat.
buyer.countryoptionnelstringPays (code ISO, défaut TN). Ce champ décide du type d'identifiant envoyé à TTN, donc renseignez-le dès que le client n'est pas tunisien. Avec le pays par défaut, un id_number part comme une carte d'identité nationale, que le schéma officiel oblige à faire exactement 8 caractères : un SIREN de 9 chiffres serait alors refusé au dépôt, après signature. Avec un pays étranger, il part comme identifiant fiscal non tunisien, de forme libre.
buyer.phone / buyer.emailoptionnelstringTéléphone / e-mail du client.
items[].descriptionrequisstringDescription de la ligne.
items[].referenceoptionnelstringRéférence de l'article, imprimée en colonne RÉFÉRENCE. Alias : code, article_code.
items[].unitoptionnelstringUnité de la ligne (piece, kg, jour, heure…). Alias : unite.
items[].quantityrequisnumberQuantité.
items[].unit_pricerequisnumberPrix unitaire HT.
items[].vat_raterequis*enum0, 7, 13, 19. Alias : tva_rate, tax_rate. *Requis sauf si default_vat_rate (facture) est défini ou si la ligne est exonerated. Aucune valeur n'est supposée par défaut : un taux manquant renvoie une erreur de validation. (Ignoré en devise étrangère : pas de TVA.)
items[].discount_percentoptionnelnumberRemise sur la ligne, en % (0–100). Le Total HT de la ligne est diminué d'autant, et la TVA est calculée sur le HT après remise. Alias : remise, remise_percent, discount.
items[].line_total_htoptionnelnumberTotal HT de la ligne tel que VOUS l’avez calculé. Quand il est fourni, il remplace le produit quantité × prix unitaire, et la TVA comme le FODEC s’en déduisent. À utiliser si votre logiciel arrondit autrement que nous : vos totaux et les nôtres cessent alors de diverger de quelques millimes, sans avoir à multiplier les décimales du prix unitaire. Nous prenons votre montant : c’est votre facture. Seul un écart de plus de 1 % avec notre propre calcul est refusé en 422 : un TTC envoyé à la place d’un HT, ou une virgule déplacée. Le message nomme alors les trois montants en cause. Aucune différence d’arrondi ne vous bloque. Alias : total_ht, montant_ht, line_total.
items[].exoneratedoptionnelbooleanLigne exonérée de TVA (taux 0 %). Équivaut à vat_rate: 0. Alias : vat_exempt.
items[].fodecoptionnelbooleanFODEC 1 % sur la ligne (produits assujettis). Calculé sur le HT après remise, et la TVA de la ligne est calculée sur HT + FODEC (règle tunisienne). Apparaît en ligne « FODEC (1%) » dans les totaux du PDF et en taxe I-1603 dans le TEIF. Alias : fodec_applicable, is_fodec. (Ignoré en devise étrangère.)
default_vat_rateoptionnelenumTaux TVA par défaut de la facture (0, 7, 13, 19), appliqué aux lignes sans vat_rate. Évite de répéter le taux sur chaque ligne.
global_discount_percentoptionnelnumberRemise sur toute la facture, en % (0 à 100), appliquée après les remises de ligne. Le Total HT est diminué d'autant et la TVA baisse proportionnellement. Le FODEC n'est pas réduit : il reste calculé sur le HT avant remise globale. Apparaît en ligne « Remise globale X% » dans les totaux du PDF. Alias : remise_globale_percent, remise_globale_pct. Un montant envoyé dans ce champ est refusé (422) : une valeur hors [0, 100] ne peut pas être un pourcentage. Utilisez alors global_discount_amount.
global_discount_amountoptionnelnumberRemise sur toute la facture, en MONTANT (dans la devise de la facture), pour les logiciels qui ne connaissent pas de pourcentage. Elle est rapportée au total HT des lignes, remises de ligne déduites, puis appliquée comme global_discount_percent : mêmes effets, FODEC non réduit compris. Alias : remise_globale, remise_globale_montant, remise_montant. Une remise supérieure au HT est refusée (422) plutôt que de produire un total négatif. N'envoyez qu'une seule des deux formes ; le pourcentage l'emporte si les deux sont présentes.
stamp_dutyoptionnelboolean | numberTimbre fiscal. Présent par défaut (1,000 TND) - mettre false pour le désactiver, ou un montant numérique pour le personnaliser. Alias : timbre, fiscal_stamp.
withholding_tax_rateoptionnelnumberRetenue à la source (ex. 1.5, 3, 10, 15, 20). Base = TTC hors timbre (HT + TVA). Montant déduit du Net à payer.
suspensionoptionnelobjectVente en suspension de TVA. Objet { "enabled": true, "authorization_number": "AUT-2026-777", "date_from": "2026-01-01", "date_to": "2026-12-31", "order_number": "BC-42", "order_date": "2026-01-15" }. L'opération reste taxable : la TVA est calculée mais non facturée, et le net à payer vaut HT + timbre. Dans le TEIF : la ligne garde son taux réel, la TVA due vaut 0 et le montant suspendu part en I-187. Le numéro d'autorisation part en référence I-85 avec sa période de validité, le bon de commande en I-83 avec sa date, et la facture porte la mention SpecialConditions « Vente en suspension de TVA ». Sur le PDF : la TVA suspendue s'affiche à part, sous les totaux, avec les mentions. N'envoyez pas exonerated: true : c'est une exonération, qui déclare un taux de 0 %. /einvoices/validate signale en warnings une attestation ou un bon de commande manquant, une date illisible ou hors validité. Alias : suspension_tva pour un simple booléen, num_autorisation, date_debut, date_fin, bon_commande.
brand_coloroptionnelstringCouleur de marque du PDF généré (hex, ex. #1d4ed8). Défaut : #dc2626. Ignoré si vous fournissez pdf_base64.
payment_methodoptionnelstringMode de règlement affiché sur le PDF (texte libre, ex. Virement bancaire - RIB 12345678901234567890, Chèque, Espèces). Max 255 caractères. Rendu sur le PDF généré (mode B).
custom_noteoptionnelobjectNote libre positionnable (mentions légales, conditions, paragraphe…). Objet { "text": string (max 1000), "position": "top_left" | "bottom_left" | "bottom_center" | "bottom_right" }. Défaut position : bottom_left. Rendu sur le PDF généré (mode B).
invoice_objectoptionnelstringObjet de la facture, affiché « Objet : … » (ex. Prestation de conseil - janvier 2026). Max 255 caractères. Rendu sur le PDF généré (mode B).
termsoptionnelstringConditions générales / conditions de paiement / mentions, affichées dans un bloc « Conditions générales » (ex. Paiement à 30 jours. Pénalité de 1,5%/mois en cas de retard.). Max 1000 caractères. Rendu sur le PDF généré (mode B).
referencesoptionnelstringRéférence libre affichée « Référence » (ex. Bon de commande N° 1234 du 05/01/2026). Max 100 caractères. Rendu sur le PDF généré (mode B).
bank_detailsoptionnelstringCoordonnées bancaires affichées « Coordonnées bancaires : … » (ex. RIB 12345678901234567890 - Banque XYZ). Max 255 caractères. Rendu sur le PDF généré (mode B).
pdf_base64optionnelstringVotre PDF de facture encodé en base64. Voir PDF de la facture.

Règles de forme du document

Ces règles s'appliquent aux trois portes, POST /einvoices/validate, POST /einvoices/process et POST /einvoices/prepare, avec les mêmes messages. Elles refusent avant la signature ce que TTN refuserait après, quand le crédit est déjà consommé. Validez à blanc avec /einvoices/validate : l'appel est gratuit et rend la liste complète des erreurs dans data.errors.

ChampRèglePourquoi
invoice_number1 à 30 caractères, commence par une lettre ou un chiffre, puis lettres, chiffres, ., -, _. Ni espace, ni /, ni :. Exemples acceptés : FACT-2026-0012, INV-2026-09-0008, F202600002.C'est l'assertion du schéma appliqué par TTN, mot pour mot : ^[A-Za-z0-9][A-Za-z0-9._-]{0,29}$. Intégrateurs Odoo : le format par défaut INV/2026/09/0008 est signé puis refusé au dépôt. Envoyez INV-2026-09-0008 ou changez la séquence, et gardez la correspondance sur la facture.
dateDate calendaire valide au format YYYY-MM-DD, pas dans le futur (heure de Tunisie).Le document est signé et horodaté le jour de l'appel ; une date d'émission postérieure est incohérente.
due_dateOptionnelle. Même format, jamais antérieure à date.Une échéance avant l'émission est une erreur de saisie.
typeOptionnel, défaut facture. Valeurs acceptées : facture, invoice, credit_note, avoir, debit_note. Toute autre valeur est refusée.Une nature inconnue retombait en silence sur « facture » : un avoir mal orthographié partait comme une facture.
original_invoice_numberObligatoire pour credit_note et debit_note, mêmes règles de forme que invoice_number.Un avoir corrige un document nommé.
items[].quantityNombre strictement positif, 3 décimales au plus.Le document conserve la quantité sur trois décimales : au-delà, la valeur serait tronquée entre votre ERP et le document signé.
items[].unit_priceNombre positif ou nul, 6 décimales au plus, 15 chiffres entiers au plus.Le schéma TEIF borne un montant à 15 chiffres entiers ; le prix se conserve sur six décimales et s'imprime sur trois.
items[].line_total_htOptionnel, nombre positif ou nul. Quand il est envoyé, il fait foi sur quantité × prix (voir la tolérance d'arrondi de l'objet Invoice).C'est votre HT de ligne qui doit figurer sur le document, pas notre recalcul.
items[].vat_rate0, 7, 13 ou 19, ou default_vat_rate au niveau du document, ou exonerated: true sur la ligne.Un taux ne se devine pas.
items[].discount_percentOptionnel, de 0 à 100.
stamp_dutytrue, false, ou un montant positif.
global_discount_percent / global_discount_amountDe 0 à 100 pour le pourcentage, montant positif pour le montant.
seller.tax_idFacultatif : la clé porte le vendeur. S'il est envoyé, il doit désigner le même contribuable (8 premiers caractères), sinon SELLER_MISMATCH.Le document est signé sous l'identité de la clé.
buyer.tax_idMatricule tunisien, forme longue (1234567ABM000) ou courte (1234567A). À défaut, buyer.id_number, de forme libre.Un numéro étranger placé ici est refusé : ce champ n'accepte que la forme tunisienne.
buyer.id_numberLibre. CIN d'un particulier, ou numéro d'entreprise d'un client étranger.Envoyez-le seul, sans buyer.tax_id, et avec buyer.country.
Ces règles sont les mêmes pour tout le monde. Un document accepté par /validate est accepté par /process et /prepare ; un document refusé le serait aussi par TTN. Ne contournez pas un refus en modifiant le message côté ERP : corrigez la donnée.

Exemple complet (tous les champs)

Payload de référence regroupant l'ensemble des champs supportés : coordonnées structurées vendeur/client (MF, adresse, ville, code postal, gouvernorat, pays, téléphone, e-mail), TVA multi-taux par ligne, taux par défaut, ligne exonérée, timbre fiscal, retenue à la source et couleur du PDF. Ce payload passe la validation sans erreur.

{ "method": "seal", "invoice_number": "F-2026-00042", "date": "2026-06-26", "due_date": "2026-07-26", "default_vat_rate": 19, "stamp_duty": true, "withholding_tax_rate": 1.5, "brand_color": "#1d4ed8", "payment_method": "Virement bancaire - RIB 12345678901234567890", "invoice_object": "Prestation de conseil - janvier 2026", "terms": "Paiement à 30 jours. Pénalité de 1,5%/mois en cas de retard (art. 5 CGV).", "references": "Bon de commande N° 1234 du 05/01/2026", "bank_details": "RIB 12345678901234567890 - Banque XYZ", "custom_note": { "text": "Facture soumise à la TVA. En cas de retard de paiement, une pénalité de 1,5% par mois sera appliquée (art. 5 des CGV).", "position": "bottom_left" }, "seller": { "name": "Mon Entreprise SARL", "tax_id": "1234567ABM000", "address": "Avenue Habib Bourguiba", "address_line_2": "Immeuble El Amen, 3e étage", "postal_code": "1000", "city": "Tunis", "governorate": "Tunis", "country": "TN", "phone": "71234567", "email": "contact@mon-entreprise.tn" }, "buyer": { "name": "Client Demo SARL", "tax_id": "7654321ABM000", "address": "Rue de la Liberté", "postal_code": "3000", "city": "Sfax", "governorate": "Sfax", "country": "TN", "phone": "74111222", "email": "facturation@client-demo.tn" }, "items": [ { "description": "Prestation de conseil", "quantity": 1, "unit_price": 1000.000, "vat_rate": 19 }, { "description": "Licence logicielle annuelle", "quantity": 2, "unit_price": 250.000, "vat_rate": 13 }, { "description": "Produit de première nécessité","quantity": 5, "unit_price": 20.000, "vat_rate": 7 }, { "description": "Service à l'export", "quantity": 1, "unit_price": 500.000, "exonerated": true } ], "return_url": "https://votre-erp.tn/factures/retour" }

Notes :

  • method : seal pour /einvoices/process (signature automatique), digigo, usb ou mobileid pour /einvoices/prepare (signature par le client).
  • Particulier : remplacez buyer.tax_id par buyer.id_number (CIN/passeport) → le PDF affiche « CIN » au lieu de « MF ».
  • Client étranger : même champ, buyer.id_number, et surtout buyer.country. Voir l'exemple export ci-dessous.
  • default_vat_rate s'applique aux lignes sans vat_rate ; chaque ligne peut le surcharger (TVA différente par ligne). Un récap TVA par taux est généré automatiquement.
  • stamp_duty est présent par défaut (1,000 TND) - mettez false pour le retirer.
  • brand_color et les coordonnées ne servent au rendu que pour le PDF généré (mode B). En mode A (pdf_base64), c'est votre PDF qui fait foi.
  • Validez d'abord via POST /einvoices/validate (0 crédit) : tout champ manquant/invalide est retourné dans error.details.
  • Alias de champs acceptés (vendeur/client) : name=nom/raison_sociale, tax_id=matricule_fiscal/mf, address=adresse/rue, postal_code=code_postal/cp, city=ville, governorate=gouvernorat, country=pays, phone=tel. L'address peut aussi être un objet { street, city, postal_code }.

Récap TVA produit pour cet exemple :

TauxBase HTMontant TVA
0 % (exonéré)500,0000,000
7 %100,0007,000
13 %500,00065,000
19 %1 000,000190,000
TOTAL2 100,000262,000

Facture export : un client qui n'a pas de matricule tunisien

Un client étranger n'a pas de matricule fiscal tunisien, et son numéro d'entreprise (SIREN, SIRET, numéro de TVA intracommunautaire, NIF) ne se met pas dans buyer.tax_id : ce champ n'accepte que la forme tunisienne et le refuse. Il se met dans buyer.id_number, seul, avec le pays du client.

Le pays n'est pas décoratif, il commande le type d'identifiant. Laissé à sa valeur par défaut, le numéro part chez TTN comme une carte d'identité nationale, dont le schéma officiel exige exactement 8 caractères. Un SIREN en fait 9 : la facture serait signée, le crédit débité, et le dépôt refusé ensuite. Avec un pays étranger, le numéro part en identifiant fiscal non tunisien, de forme libre.
{ "invoice_number": "F2026-0002", "type": "facture", "date": "2026-07-13", "currency": "EUR", "method": "seal", "seller": { "name": "Mon Entreprise SARL", "address": "Avenue Habib Bourguiba", "city": "Tunis", "country": "TN" }, "buyer": { "name": "Export Client SA", "id_number": "552 100 554", "address": "Rue de Paris", "postal_code": "75002", "city": "Paris", "country": "FR" }, "stamp_duty": false, "items": [ { "description": "Produit export", "quantity": 5, "unit_price": 200.00, "vat_rate": 0 } ] }

Ce que l'export change, et rien d'autre :

  • buyer.id_number porte le numéro, et buyer.tax_id est absent. Les espaces sont retirés avant l'envoi : 552 100 554 devient 552100554.
  • buyer.country porte le code du pays du client. C'est lui qui fait accepter un numéro qui n'a pas la forme tunisienne.
  • currency en devise étrangère : 2 décimales, aucune conversion, les montants sont conservés tels quels.
  • Ni TVA, ni timbre fiscal : vat_rate: 0 sur les lignes, ou exonerated: true, et stamp_duty: false.
  • Le vendeur, lui, ne change pas : c'est toujours le titulaire de votre clé, et son matricule tunisien complet part dans le document.

Ce cas figure en entier dans votre portail, deuxième document du jeu de test TTN, prêt à être envoyé tel quel.

Retenue à la source (RS)

La retenue à la source est l'impôt que l'acheteur retient sur le paiement et reverse à l'État pour le compte du vendeur. Elle ne modifie ni le HT, ni la TVA, ni le Total TTC : elle réduit seulement le Net à payer.

  • Optionnelle : envoyez withholding_tax_rate uniquement quand il y a une retenue. Omettez le champ (ou mettez 0) pour ne pas en appliquer - la ligne « Retenue à la source » n'apparaît alors pas et le Net à payer = Total TTC. Aucun autre indicateur n'est requis.
  • Base de calcul = TTC hors timbre (HT + TVA).
  • Taux courants : 1.5 (achats biens/services > 1000 DT TTC), 3 (honoraires régime réel), 10 (honoraires/loyers/commissions), 15, 20…

Exemple 1 - avec retenue à la source (1,5 %)

{ "invoice_number": "F-2026-00051", "date": "2026-06-27", "stamp_duty": true, "withholding_tax_rate": 1.5, "seller": { "name": "Mon Entreprise SARL", "tax_id": "1234567ABM000", "address": "Tunis" }, "buyer": { "name": "Client Public", "tax_id": "7654321ABM000", "address": "Sfax" }, "items": [ { "description": "Location bus", "quantity": 1, "unit_price": 125.000, "vat_rate": 7 } ] }
LigneMontantCalcul
Total HT125,0001 × 125,000
TVA (7 %)8,7507 % du HT
Timbre fiscal1,000fixe
Total TTC134,750HT + TVA + timbre
Retenue à la source 1,5 %− 2,0061,5 % × (HT + TVA) = 1,5 % × 133,750
NET À PAYER132,744TTC − RS

Exemple 2 - sans retenue à la source

Il suffit de ne pas envoyer withholding_tax_rate (ou de mettre 0) :

{ "invoice_number": "F-2026-00052", "date": "2026-06-27", "stamp_duty": true, "seller": { "name": "Mon Entreprise SARL", "tax_id": "1234567ABM000", "address": "Tunis" }, "buyer": { "name": "Client Privé", "tax_id": "7654321ABM000", "address": "Sfax" }, "items": [ { "description": "Location bus", "quantity": 1, "unit_price": 125.000, "vat_rate": 7 } ] }
LigneMontantCalcul
Total HT125,0001 × 125,000
TVA (7 %)8,7507 % du HT
Timbre fiscal1,000fixe
NET À PAYER (= Total TTC)134,750aucune ligne RS affichée

Vos totaux et les nôtres : line_total_ht

Par défaut, nous calculons le total de chaque ligne nous-mêmes : quantité × prix unitaire, remise déduite. Dans la très grande majorité des cas, c'est exactement ce que vous voulez et vous n'avez rien à faire.

Quand vous en avez besoin

Quand votre logiciel arrive à un total légèrement différent du nôtre. Cela arrive avec des prix unitaires à beaucoup de décimales, et pour deux raisons qui n'ont rien à voir avec un défaut :

  • La précision conservée. Nous gardons six décimales sur un prix unitaire. Un prix comme 8.40336134454 est donc stocké 8.403361. Sur 7 006 unités, ces quelques millionièmes font environ 3 millimes.
  • L'ordre des arrondis. Beaucoup de logiciels arrondissent le total une seule fois, à la fin. Nous arrondissons chaque ligne puis nous sommons, parce qu'une ligne imprimée doit être un vrai montant et que le total doit être la somme de ce qui est imprimé. Cela vaut encore un millime ou deux, parfois dans l'autre sens.

Aucune précision supplémentaire ne supprime le second point : ce n'est pas une question de décimales, c'est une convention. D'où ce champ : envoyez-nous le montant que vous avez calculé, et nous imprimons le même.

Comment l'utiliser

Ajoutez line_total_ht sur la ligne, hors taxes, remise déjà déduite. Il remplace le produit quantité × prix ; la TVA et le FODEC s'en déduisent ensuite normalement.

{ "invoice_number": "F-2026-00700", "date": "2026-09-01", "stamp_duty": true, "seller": { "name": "Ma Société SARL", "tax_id": "1234567AAM000" }, "buyer": { "name": "Client SA", "tax_id": "7654321MAM000" }, "items": [ { "description": "Bon d'achat", "quantity": 7006, "unit_price": 8.40336134454, "vat_rate": 19, "line_total_ht": 58873.950 } ] }

La seule ligne ajoutée est line_total_ht. Tout le reste est votre facture habituelle.

Sans le champAvec le champ
Total ligne HT 58 873,947
(7 006 × 8,403361)
Total ligne HT 58 873,950
(votre montant, repris tel quel)

Pour que le total de la FACTURE tombe juste aussi

Le total HT du document est la somme des lignes que vous envoyez. Si votre logiciel arrondit le total une seule fois, la somme de vos lignes arrondies peut dépasser votre propre total d'un millime. Dans ce cas, reportez ce reliquat sur une ligne, comme votre logiciel le fait déjà pour imprimer votre propre document, et nos deux totaux seront identiques au millime.

Exemple complet, prêt à envoyer

Une facture de trois lignes, avec des prix unitaires à onze décimales - exactement le cas qui fait diverger les totaux. Chaque ligne porte son line_total_ht.

{ "method": "seal", "invoice_number": "F-2026-00700", "date": "2026-09-01", "due_date": "2026-10-01", "currency": "TND", "stamp_duty": true, "seller": { "name": "MA SOCIÉTÉ SARL", "tax_id": "1234567AAM000", "address": "Avenue Habib Bourguiba", "city": "Tunis", "postal_code": "1000" }, "buyer": { "name": "CLIENT SA", "tax_id": "7654321MAM000", "address": "Rue de la Liberté", "city": "Sfax", "postal_code": "3000" }, "items": [ { "description": "Bon d'achat 50 DT", "quantity": 7006, "unit_price": 8.40336134454, "vat_rate": 19, "line_total_ht": 58873.950 }, { "description": "Bon d'achat 25 DT", "quantity": 474, "unit_price": 4.20168067227, "vat_rate": 19, "line_total_ht": 1991.597 }, { "description": "Frais de gestion", "quantity": 42, "unit_price": 3.36134453782, "vat_rate": 19, "line_total_ht": 141.176 } ] }

Ce que vous obtenez, et ce sont les montants que cet envoi produit réellement :

 Avec line_total_htSans
Total HT61 006,72361 006,720
TVA 19 %11 591,27711 591,277
Timbre fiscal1,0001,000
Net à payer72 599,00072 598,997

Avec le champ, le total est exactement celui de votre logiciel. Ici la TVA tombe juste des deux côtés : c’est le HT, et donc le net à payer, qui portent l’écart. Sans lui, trois millimes d'écart, assez pour qu'un rapprochement comptable automatique signale la facture.

Ici, la somme des trois lignes tombe pile sur votre total. Quand ce n'est pas le cas, reportez le reliquat sur une ligne, comme expliqué juste au-dessus.

Ce que nous contrôlons

Nous prenons votre montant : c'est votre facture, vous en êtes l'émetteur. Nous refusons seulement un écart de plus de 1 % avec notre propre calcul, parce qu'aucun arrondi ne produit cela et que la facture part signée chez l'administration. Ce garde-fou attrape les trois erreurs réelles : un TTC envoyé à la place d'un HT (+19 %), une virgule déplacée (×10), une ligne mal appariée. Le refus est un 422 qui nomme les trois montants en cause.

Deux pièges à éviter

  • Envoyez un montant HORS TAXES. Le TTC est l'erreur la plus fréquente, et elle est refusée.
  • Ne l'envoyez pas « au cas où ». Si vos totaux correspondent déjà aux nôtres, omettez le champ : le calcul normal reste la règle, et une ligne de moins est une ligne qui ne peut pas diverger.

PDF de la facture

Le PDF est le document visuel présenté au signataire et archivé après signature (le QR de signature y est apposé automatiquement). Deux modes sont possibles :

ModeQuandRésultat
A - Vous fournissez le PDF recommandé Vous envoyez le champ pdf_base64 (votre PDF déjà mis en page : logo, couleurs, mentions de votre ERP). Votre PDF est signé tel quel. C'est votre document qui fait foi.
B - Génération automatique Vous omettez pdf_base64. Nous générons un PDF neutre mais valide à partir des données de la facture (vendeur, client, lignes, totaux). Sans votre logo.

Recommandation : si votre ERP produit déjà des factures PDF, envoyez-les via pdf_base64 pour conserver votre charte graphique. Contraintes :

  • Encodage base64 du contenu binaire du PDF (préfixe data:application/pdf;base64, toléré mais optionnel).
  • Doit être un PDF valide (commence par %PDF).
  • Taille maximale : 10 Mo (avant encodage).
  • Si le PDF fourni est invalide ou absent, le mode B (génération) prend automatiquement le relais - aucun échec de signature.

Objet Error

Format normalisé pour toutes les erreurs :

{ "success": false, "data": null, "error": { "code": "VALIDATION_FAILED", "message": "Invoice validation failed.", "details": [ "seller.address is required.", "buyer.tax_id or buyer.id_number is required.", "item 1: vat_rate is required (set the line vat_rate, a document default_vat_rate, or \"exonerated\": true)." ] } }

Une question technique ? Contactez l'équipe ou consultez la référence interactive Swagger.

Discuter sur WhatsApp