Erreurs & statuts
Cette page décrit la façon dont l'API Tchin signale les erreurs, comment les interpréter et comment écrire un code robuste face à chaque situation. La règle d'or est simple : ne vous fiez jamais au seul code HTTP. Une réponse 200 peut contenir success: false, et une confirmation de paiement définitive ne vous parvient que par webhook. Traitez systématiquement les deux niveaux : le statut HTTP et le champ success du corps JSON.
Format d'une erreur métier
Lorsqu'une requête est bien formée mais ne peut aboutir (solde insuffisant, moyen de paiement non pris en charge, ressource introuvable…), l'API renvoie un JSON contenant success: false et un message lisible. Ce format est stable et identique sur tous les endpoints.
{
"success": false,
"message": "Description lisible de l'erreur"
}
Certaines erreurs enrichissent la réponse de champs supplémentaires. Par exemple, un solde insuffisant lors d'un déboursement ajoute un champ balance indiquant le solde disponible sur le pays concerné, ce qui vous permet d'informer précisément l'utilisateur ou de déclencher un réapprovisionnement.
Format d'une erreur de validation (422)
Quand un champ obligatoire est absent, hors bornes ou mal typé, la validation de Laravel intervient avant tout traitement métier et renvoie un code 422 Unprocessable Entity. Le corps contient un message global et un objet errors qui liste, champ par champ, tous les problèmes détectés (chaque champ pointe vers un tableau de messages). C'est le format idéal pour afficher les erreurs à côté des champs d'un formulaire.
{
"message": "The amount field is required. (and 1 more error)",
"errors": {
"amount": [
"The amount field is required."
],
"withdraw_mode": [
"The withdraw mode field is required."
]
}
}
⚠️ Ne confondez pas 422 (validation d'entrée : le format de votre requête est en cause) et 400 (erreur métier : la requête est correcte mais l'opération est refusée, par exemple un solde insuffisant). Les deux se gèrent différemment : le 422 se corrige côté client, le 400 dépend de l'état de votre compte.
Codes HTTP
Voici l'ensemble des codes que l'API peut retourner et la manière de les traiter.
| Code | Signification | Que faire |
|---|---|---|
200 | Requête traitée. | Vérifiez toujours le champ success du corps avant de considérer l'opération réussie. |
400 | Requête invalide au sens métier (champ manquant, solde insuffisant, moyen non pris en charge…). | Lisez message et corrigez la cause. Ne réessayez pas à l'identique. |
401 | Clés API manquantes ou invalides. | Ajoutez / corrigez les en-têtes TCHIN-PUBLIC-KEY et TCHIN-PRIVATE-KEY. |
403 | Compte marchand suspendu. | Contactez le support Tchin : aucune opération n'aboutira tant que le compte est suspendu. |
404 | Ressource introuvable (token de paiement ou disburse_token inexistant). | Vérifiez l'identifiant transmis ; il n'existe pas ou n'appartient pas à votre compte. |
422 | Validation des données échouée. | Parcourez errors et corrigez chaque champ signalé, puis renvoyez la requête. |
500 | Erreur interne côté serveur. | Réessayez plus tard, idéalement avec un délai croissant (backoff). Ne considérez rien comme effectué. |
Erreurs fréquentes — Encaissement
Ces messages concernent la création et le suivi des paiements.
| Message / situation | Code | Cause et solution |
|---|---|---|
| Clés API manquantes. | 401 | Aucun en-tête d'authentification transmis. Ajoutez TCHIN-PUBLIC-KEY et TCHIN-PRIVATE-KEY sur chaque requête. |
| Clés API invalides. | 401 | Les clés ne correspondent à aucun compte. Récupérez-les dans votre tableau de bord (menu API → Voir). |
| Compte suspendu. | 403 | Votre compte marchand est bloqué. Contactez le support ; aucune requête n'aboutira entre-temps. |
| Paiement momentanément indisponible. | 400 | Configuration de paiement incomplète côté Tchin (opérateur non provisionné). Contactez le support. |
| Paiement refusé. | 400 | En test : vérifiez les identifiants du compte de test. En live : le client a annulé ou l'opérateur a rejeté la transaction — le statut définitif arrivera par webhook. |
| Paiement introuvable. | 404 | Le token passé à GET /payments/{token}/status n'existe pas. Vérifiez qu'il provient bien de la réponse de création. |
Champ amount requis / hors bornes. | 422 | Le montant est un entier en FCFA, sans décimale, entre 100 et 100 000 000. Corrigez et renvoyez. |
Erreurs fréquentes — Déboursement
Ces messages concernent l'initiation et l'exécution des retraits. Le déboursement est disponible en mode live uniquement.
| Message / situation | Code | Cause et solution |
|---|---|---|
| withdraw_mode non pris en charge. | 400 | Le code opérateur transmis n'est pas reconnu. Utilisez une valeur exacte de la liste des withdraw_mode (ex. wave-senegal, mtn-ci). |
| Solde insuffisant sur le pays SN. | 400 | Le débit vise le solde du pays associé au withdraw_mode. La réponse inclut un champ balance. Réapprovisionnez le pays concerné, puis réessayez. |
| Déboursement introuvable. | 404 | Le disburse_token n'existe pas ou n'appartient pas à votre compte. Vérifiez la valeur retournée par POST /disburse/initiate. |
| Échec du déboursement. | 400 | L'exécution a échoué côté opérateur. Interrogez le statut ; s'il est failed, relancez une nouvelle transaction (l'ancienne n'a rien débité). |
Champ account_alias requis. | 422 | Renseignez le numéro du bénéficiaire, sans indicatif pays (ex. 771234567), 30 caractères maximum. |
Statuts d'un déboursement
Un déboursement suit un cycle de vie précis. À chaque étape correspond une action recommandée. Rien n'est débité tant que la transaction n'a pas atteint l'état success, et le débit est idempotent (il ne s'applique qu'une fois).
| Statut | Signification | Que faire |
|---|---|---|
created | La transaction est initiée mais pas encore exécutée. Aucun débit. | Appelez POST /disburse/submit avec le disburse_token pour l'exécuter. |
pending | Exécution en cours côté opérateur (traitement asynchrone). | Patientez, puis interrogez POST /disburse/status. Ne resoumettez pas et ne re-débitez pas. |
success | Fonds transférés au bénéficiaire. Le solde du pays est débité. | Terminé : vous pouvez clore la transaction dans votre système. |
failed | Le transfert a échoué. Aucun débit n'a été appliqué. | Informez le bénéficiaire, vérifiez le numéro et l'opérateur, puis relancez une nouvelle transaction si besoin. |
ℹ️ Un statut pending n'est pas un échec. Il signifie « en cours ». Rejouer l'opération à cet instant risquerait de créer un doublon : interrogez plutôt le statut jusqu'à obtenir success ou failed.
Gérer les erreurs en pratique
Le patron suivant centralise le traitement : il distingue les incidents serveur (à réessayer), la validation 422 (à corriger côté client) et les erreurs métier (success: false). Adaptez-le à chaque endpoint.
<?php
// Appel générique à l'API Tchin puis gestion homogène des erreurs.
$ch = curl_init('https://tchin.tech/api/v1/disburse/initiate');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'TCHIN-PUBLIC-KEY: tchin_pk_xxxxxxxx',
'TCHIN-PRIVATE-KEY: tchin_sk_xxxxxxxx',
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'account_alias' => '771234567',
'amount' => 5000,
'withdraw_mode' => 'wave-senegal',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($body, true);
// 1) Erreur réseau / serveur
if ($status >= 500) {
// Réessayez plus tard (avec backoff). Ne considérez rien comme fait.
throw new RuntimeException('Tchin indisponible, réessayez.');
}
// 2) Validation Laravel (422) : champ manquant ou mal formé
if ($status === 422) {
foreach ($data['errors'] ?? [] as $champ => $messages) {
// Affichez $messages[0] à côté du champ $champ
}
return;
}
// 3) Erreur métier : toujours vérifier "success"
if (empty($data['success'])) {
// $data['message'] contient la cause (solde insuffisant, mode non pris en charge…)
// $data['balance'] est présent en cas de solde insuffisant.
error_log('Tchin: ' . ($data['message'] ?? 'erreur inconnue'));
return;
}
// 4) Succès
$disburseToken = $data['disburse_token'];
// Node.js — gestion homogène des réponses Tchin
async function callTchin(path, payload) {
const res = await fetch(`https://tchin.tech/api/v1${path}`, {
method: 'POST',
headers: {
'TCHIN-PUBLIC-KEY': process.env.TCHIN_PUBLIC_KEY,
'TCHIN-PRIVATE-KEY': process.env.TCHIN_PRIVATE_KEY,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(payload),
});
// 500+ : incident serveur, à réessayer avec backoff
if (res.status >= 500) {
throw new Error('Tchin indisponible, réessayez plus tard.');
}
const data = await res.json();
// 422 : erreurs de validation Laravel, champ par champ
if (res.status === 422) {
const details = Object.entries(data.errors || {})
.map(([champ, msgs]) => `${champ}: ${msgs[0]}`)
.join(' | ');
throw new Error(`Validation: ${details}`);
}
// Erreur métier : toujours tester success
if (!data.success) {
const err = new Error(data.message || 'Erreur Tchin');
err.balance = data.balance; // présent si solde insuffisant
throw err;
}
return data;
}
# Python (requests) — gestion homogène des erreurs Tchin
import os
import requests
def call_tchin(path, payload):
res = requests.post(
f"https://tchin.tech/api/v1{path}",
json=payload,
headers={
"TCHIN-PUBLIC-KEY": os.environ["TCHIN_PUBLIC_KEY"],
"TCHIN-PRIVATE-KEY": os.environ["TCHIN_PRIVATE_KEY"],
"Content-Type": "application/json",
"Accept": "application/json",
},
timeout=30,
)
# 500+ : incident serveur — réessayez avec backoff
if res.status_code >= 500:
raise RuntimeError("Tchin indisponible, réessayez plus tard.")
data = res.json()
# 422 : validation Laravel, détaillée champ par champ
if res.status_code == 422:
details = {champ: msgs[0] for champ, msgs in data.get("errors", {}).items()}
raise ValueError(f"Validation: {details}")
# Erreur métier : toujours vérifier "success"
if not data.get("success"):
raise RuntimeError(data.get("message", "Erreur Tchin"))
return data
Conseils de robustesse
- Vérifiez toujours le champ
success, même sur un200: un code HTTP favorable ne garantit pas que l'opération a réussi. - Pour les paiements, la source de vérité est le webhook, pas le retour navigateur ni la seule réponse de création — le client peut fermer l'onglet avant la redirection.
- Pour un déboursement
pending, ne re-soumettez jamais : interrogez le statut. Rejouer une opération en cours peut provoquer un double transfert. - Sur un
500, réessayez avec un délai croissant (backoff) et ne considérez pas l'opération comme effectuée tant que vous n'avez pas confirmé son statut. - Rendez vos traitements idempotents : identifiez chaque paiement par son
reference(token) et n'appliquez l'effet (marquer payé, livrer) qu'une seule fois. - Stockez systématiquement le
tokenou ledisburse_token: c'est votre seul moyen de retrouver et de vérifier une transaction ultérieurement. - Journalisez le
messagerenvoyé lors d'unsuccess: false: il contient la cause exacte et facilite grandement le support.
Pour aller plus loin sur la validation asynchrone des paiements, consultez la page Webhooks.