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éponse | Ce qui s'est passé | Que faire |
|---|---|---|
201 ou 200 | Facture certifiée (200 : elle l'était déjà) | Enregistrer l'id et le code MECeF/DGI |
| Pas de réponse | Coupure : vous ne savez pas | Réessayer, même clé |
409 « en cours » | La même facture est en train d'être certifiée | Attendre quelques secondes, même clé |
503 | La DGI ne répond pas, rien n'a été certifié | Réessayer plus tard, même clé |
502 | Confirmation incertaine : la DGI a peut-être certifié la facture | Ne 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 :
| Outil | Comment faire |
|---|---|
| Postman | Té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, Hoppscotch | Importez la même collection, ou la description OpenAPI. |
| Navigateur | La référence interactive : bouton Authorize, collez la clé, puis Try it out sur chaque route. |
| Générer un client | Le 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