Guides

Les cas courants, un par un.

Faire un avoir (remboursement, erreur sur une facture)

Une facture certifiée ne s'annule pas : on émet un avoir (type FA, ou EA pour l'export) qui reprend les lignes à rembourser, avec dans reference le code MECeF/DGI de la facture d'origine sans les tirets. Exemple pour rembourser la livraison de la facture précédente (TEST-CWL5-KAYD-E7UZ-UMJF-G3VK) :

curl -X POST https://sandbox.otampa.com/invoices \
  -H "Authorization: Bearer $TAMPA_API_KEY" \
  -H "Idempotency-Key: avoir-0001" \
  -H "Content-Type: application/json" \
  -d '{
    "taxpayer_id": "3f1c2a8e-5b7d-4c1e-9a2f-6d8e0b4c7a19",
    "invoice": {
      "type": "FA",
      "reference": "TESTCWL5KAYDE7UZUMJFG3VK",
      "operator": {"name": "Caisse 1"},
      "client": {"name": "Client Exemple", "contact": "+229 01 00 00 00 01"},
      "items": [{"name": "Livraison", "price": 1000, "quantity": 1, "taxGroup": "B"}]
    }
  }'
import os

import requests

response = requests.post(
    "https://sandbox.otampa.com/invoices",
    headers={
        "Authorization": f"Bearer {os.environ['TAMPA_API_KEY']}",
        "Idempotency-Key": "avoir-0001",
    },
    json={
        "taxpayer_id": "3f1c2a8e-5b7d-4c1e-9a2f-6d8e0b4c7a19",
        "invoice": {
            "type": "FA",
            "reference": "TESTCWL5KAYDE7UZUMJFG3VK",
            "operator": {"name": "Caisse 1"},
            "client": {"name": "Client Exemple", "contact": "+229 01 00 00 00 01"},
            "items": [{"name": "Livraison", "price": 1000, "quantity": 1, "taxGroup": "B"}]
        }
    },
    timeout=120,
)
print(response.status_code, response.json())
const response = await fetch("https://sandbox.otampa.com/invoices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TAMPA_API_KEY}`,
    "Idempotency-Key": "avoir-0001",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "taxpayer_id": "3f1c2a8e-5b7d-4c1e-9a2f-6d8e0b4c7a19",
    "invoice": {
      "type": "FA",
      "reference": "TESTCWL5KAYDE7UZUMJFG3VK",
      "operator": {"name": "Caisse 1"},
      "client": {"name": "Client Exemple", "contact": "+229 01 00 00 00 01"},
      "items": [{"name": "Livraison", "price": 1000, "quantity": 1, "taxGroup": "B"}]
    }
  }),
});
console.log(response.status, await response.json());
<?php
$ch = curl_init("https://sandbox.otampa.com/invoices");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => "POST",
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("TAMPA_API_KEY"),
        "Idempotency-Key: avoir-0001",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "taxpayer_id" => "3f1c2a8e-5b7d-4c1e-9a2f-6d8e0b4c7a19",
        "invoice" => [
            "type" => "FA",
            "reference" => "TESTCWL5KAYDE7UZUMJFG3VK",
            "operator" => ["name" => "Caisse 1"],
            "client" => ["name" => "Client Exemple", "contact" => "+229 01 00 00 00 01"],
            "items" => [["name" => "Livraison", "price" => 1000, "quantity" => 1, "taxGroup" => "B"]],
        ],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 120,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo $status, "\n", $body, "\n";

Juste après la certification de l'original, la DGI peut répondre l'erreur 11 pendant quelques dizaines de secondes, le temps de retrouver la facture. Attendez 30 secondes et renvoyez la même requête avec la même clé.

Vendre à l'export

Type EV, articles en groupe C (exportation de produits taxables) ou A (exonéré). Le client étranger n'a pas d'IFU béninois : indiquez son nom et son adresse.

curl -X POST https://sandbox.otampa.com/invoices \
  -H "Authorization: Bearer $TAMPA_API_KEY" \
  -H "Idempotency-Key: export-0001" \
  -H "Content-Type: application/json" \
  -d '{
    "taxpayer_id": "3f1c2a8e-5b7d-4c1e-9a2f-6d8e0b4c7a19",
    "invoice": {
      "type": "EV",
      "operator": {"name": "Service export"},
      "client": {"name": "Importateur Lomé", "address": "Lomé, Togo"},
      "items": [{"name": "Noix de cajou (sac)", "price": 45000, "quantity": 10, "taxGroup": "C"}],
      "payment": [{"name": "VIREMENT", "amount": 450000}]
    }
  }'
import os

import requests

response = requests.post(
    "https://sandbox.otampa.com/invoices",
    headers={
        "Authorization": f"Bearer {os.environ['TAMPA_API_KEY']}",
        "Idempotency-Key": "export-0001",
    },
    json={
        "taxpayer_id": "3f1c2a8e-5b7d-4c1e-9a2f-6d8e0b4c7a19",
        "invoice": {
            "type": "EV",
            "operator": {"name": "Service export"},
            "client": {"name": "Importateur Lomé", "address": "Lomé, Togo"},
            "items": [
                {
                    "name": "Noix de cajou (sac)",
                    "price": 45000,
                    "quantity": 10,
                    "taxGroup": "C"
                }
            ],
            "payment": [{"name": "VIREMENT", "amount": 450000}]
        }
    },
    timeout=120,
)
print(response.status_code, response.json())
const response = await fetch("https://sandbox.otampa.com/invoices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TAMPA_API_KEY}`,
    "Idempotency-Key": "export-0001",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "taxpayer_id": "3f1c2a8e-5b7d-4c1e-9a2f-6d8e0b4c7a19",
    "invoice": {
      "type": "EV",
      "operator": {"name": "Service export"},
      "client": {"name": "Importateur Lomé", "address": "Lomé, Togo"},
      "items": [{"name": "Noix de cajou (sac)", "price": 45000, "quantity": 10, "taxGroup": "C"}],
      "payment": [{"name": "VIREMENT", "amount": 450000}]
    }
  }),
});
console.log(response.status, await response.json());
<?php
$ch = curl_init("https://sandbox.otampa.com/invoices");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => "POST",
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("TAMPA_API_KEY"),
        "Idempotency-Key: export-0001",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "taxpayer_id" => "3f1c2a8e-5b7d-4c1e-9a2f-6d8e0b4c7a19",
        "invoice" => [
            "type" => "EV",
            "operator" => ["name" => "Service export"],
            "client" => ["name" => "Importateur Lomé", "address" => "Lomé, Togo"],
            "items" => [
                [
                    "name" => "Noix de cajou (sac)",
                    "price" => 45000,
                    "quantity" => 10,
                    "taxGroup" => "C",
                ],
            ],
            "payment" => [["name" => "VIREMENT", "amount" => 450000]],
        ],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 120,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo $status, "\n", $body, "\n";

Facture avec AIB

L'AIB (acompte sur impôt assis sur les bénéfices) s'ajoute au total : "aib": "A" pour 1 %, "B" pour 5 %. La DGI le calcule sur la somme hors taxe de toutes les lignes, exonérées comprises. Ici : 2 000 FCFA exonérés + 3 000 FCFA TTC taxables (2 542 FCFA hors taxe), AIB 5 % de 4 542 = 227 FCFA : total 5 227 FCFA. Ne mettez pas l'AIB dans le prix des lignes.

curl -X POST https://sandbox.otampa.com/invoices \
  -H "Authorization: Bearer $TAMPA_API_KEY" \
  -H "Idempotency-Key: aib-0001" \
  -H "Content-Type: application/json" \
  -d '{
    "taxpayer_id": "3f1c2a8e-5b7d-4c1e-9a2f-6d8e0b4c7a19",
    "invoice": {
      "type": "FV",
      "aib": "B",
      "operator": {"name": "Comptabilité"},
      "client": {"ifu": "0000000000001", "name": "Entreprise Cliente"},
      "items": [
        {"name": "Article exonéré", "price": 1000, "quantity": 2, "taxGroup": "A"},
        {"name": "Article taxable", "price": 1000, "quantity": 3, "taxGroup": "B"}
      ]
    }
  }'
import os

import requests

response = requests.post(
    "https://sandbox.otampa.com/invoices",
    headers={
        "Authorization": f"Bearer {os.environ['TAMPA_API_KEY']}",
        "Idempotency-Key": "aib-0001",
    },
    json={
        "taxpayer_id": "3f1c2a8e-5b7d-4c1e-9a2f-6d8e0b4c7a19",
        "invoice": {
            "type": "FV",
            "aib": "B",
            "operator": {"name": "Comptabilité"},
            "client": {"ifu": "0000000000001", "name": "Entreprise Cliente"},
            "items": [
                {"name": "Article exonéré", "price": 1000, "quantity": 2, "taxGroup": "A"},
                {"name": "Article taxable", "price": 1000, "quantity": 3, "taxGroup": "B"}
            ]
        }
    },
    timeout=120,
)
print(response.status_code, response.json())
const response = await fetch("https://sandbox.otampa.com/invoices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TAMPA_API_KEY}`,
    "Idempotency-Key": "aib-0001",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "taxpayer_id": "3f1c2a8e-5b7d-4c1e-9a2f-6d8e0b4c7a19",
    "invoice": {
      "type": "FV",
      "aib": "B",
      "operator": {"name": "Comptabilité"},
      "client": {"ifu": "0000000000001", "name": "Entreprise Cliente"},
      "items": [
        {"name": "Article exonéré", "price": 1000, "quantity": 2, "taxGroup": "A"},
        {"name": "Article taxable", "price": 1000, "quantity": 3, "taxGroup": "B"}
      ]
    }
  }),
});
console.log(response.status, await response.json());
<?php
$ch = curl_init("https://sandbox.otampa.com/invoices");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => "POST",
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("TAMPA_API_KEY"),
        "Idempotency-Key: aib-0001",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "taxpayer_id" => "3f1c2a8e-5b7d-4c1e-9a2f-6d8e0b4c7a19",
        "invoice" => [
            "type" => "FV",
            "aib" => "B",
            "operator" => ["name" => "Comptabilité"],
            "client" => ["ifu" => "0000000000001", "name" => "Entreprise Cliente"],
            "items" => [
                [
                    "name" => "Article exonéré",
                    "price" => 1000,
                    "quantity" => 2,
                    "taxGroup" => "A",
                ],
                [
                    "name" => "Article taxable",
                    "price" => 1000,
                    "quantity" => 3,
                    "taxGroup" => "B",
                ],
            ],
        ],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 120,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo $status, "\n", $body, "\n";

Coupures et reprises

Le réseau peut couper entre votre logiciel et Tampa, ou entre Tampa et la DGI. La règle est simple : réessayer avec la même clé d'idempotence, jamais avec une nouvelle tant que vous ne savez pas ce qui s'est passé.

RéponseCe qui s'est passéQue faire
201 ou 200Facture certifiée (200 : elle l'était déjà)Enregistrer l'id et le code MECeF/DGI
Pas de réponseCoupure : vous ne savez pasRéessayer, même clé
409 « en cours »La même facture est en train d'être certifiéeAttendre quelques secondes, même clé
503La DGI ne répond pas, rien n'a été certifiéRéessayer plus tard, même clé
502Confirmation incertaine : la DGI a peut-être certifié la factureNe pas refaire la facture. Vérifier sur le portail e-MECeF, ou écrire au support avec le request_id. La même clé reste bloquée (409) pour éviter un doublon.

Exemple de reprise automatique, à adapter :

import os
import time

import requests

API = "https://sandbox.otampa.com"
HEADERS = {"Authorization": f"Bearer {os.environ['TAMPA_API_KEY']}"}


def certifier(facture: dict, cle: str) -> dict:
    """Certifie une facture ; réessaie sans risque de doublon grâce à la même clé."""
    for essai in range(5):
        try:
            reponse = requests.post(
                f"{API}/invoices",
                headers={**HEADERS, "Idempotency-Key": cle},
                json=facture,
                timeout=120,
            )
        except requests.RequestException:
            reponse = None  # coupure réseau : on réessaie avec la même clé
        if reponse is not None:
            if reponse.status_code in (200, 201):
                return reponse.json()  # 201 : certifiée ; 200 : déjà certifiée avant
            if reponse.status_code == 502:
                # Confirmation incertaine : ne pas refaire la facture, vérifier d'abord.
                raise RuntimeError("À vérifier sur le portail e-MECeF : " + reponse.text)
            if reponse.status_code not in (409, 503):
                raise RuntimeError(f"Refusée ({reponse.status_code}) : {reponse.text}")
        time.sleep(2**essai)  # 1 s, 2 s, 4 s, 8 s, 16 s
    raise RuntimeError("Tampa ou la DGI ne répond pas : réessayez plus tard, même clé.")
const API = "https://sandbox.otampa.com";

// Certifie une facture ; réessaie sans risque de doublon grâce à la même clé.
export async function certifier(facture, cle) {
  for (let essai = 0; essai < 5; essai++) {
    let reponse = null;
    try {
      reponse = await fetch(`${API}/invoices`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.TAMPA_API_KEY}`,
          "Idempotency-Key": cle,
          "Content-Type": "application/json",
        },
        body: JSON.stringify(facture),
        signal: AbortSignal.timeout(120_000),
      });
    } catch {
      // coupure réseau : on réessaie avec la même clé
    }
    if (reponse) {
      if (reponse.status === 200 || reponse.status === 201) {
        return reponse.json(); // 201 : certifiée ; 200 : déjà certifiée avant
      }
      if (reponse.status === 502) {
        // Confirmation incertaine : ne pas refaire la facture, vérifier d'abord.
        throw new Error("À vérifier sur le portail e-MECeF : " + (await reponse.text()));
      }
      if (reponse.status !== 409 && reponse.status !== 503) {
        throw new Error(`Refusée (${reponse.status}) : ${await reponse.text()}`);
      }
    }
    await new Promise((fin) => setTimeout(fin, 1000 * 2 ** essai)); // 1 s, 2 s, 4 s...
  }
  throw new Error("Tampa ou la DGI ne répond pas : réessayez plus tard, même clé.");
}

Postman et outils

Pour essayer l'API sans écrire de code :

OutilComment faire
PostmanTéléchargez la collection Tampa, puis Import dans Postman. Collez votre clé dans la variable apiKey de la collection. Après « Ajouter une entreprise », son id est gardé tout seul pour les requêtes suivantes.
Insomnia, Bruno, HoppscotchImportez la même collection, ou la description OpenAPI.
NavigateurLa référence interactive : bouton Authorize, collez la clé, puis Try it out sur chaque route.
Générer un clientLe fichier OpenAPI permet de générer un client typé (par exemple avec openapi-generator) dans votre langage.

Dans la collection, chaque envoi de « Certifier une facture » utilise une nouvelle clé d'idempotence, donc crée une nouvelle facture de test.

Une question ? contact@otampa.com