> ## Documentation Index
> Fetch the complete documentation index at: https://developers.ligdicash.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Erreurs courantes et solutions

> Les erreurs les plus fréquentes rencontrées par les marchands lors de l'intégration LigdiCash, leurs causes réelles et comment les résoudre.

Cette page recense les erreurs les plus fréquentes rencontrées en intégration et en production, issues du retour terrain des marchands LigdiCash.

<Note>
  Les sous-codes (`Echec (CodeXX)`) sont propres à chaque endpoint. Consultez [la liste des sous-codes](/erreurs/sous-codes) pour la correspondance complète. Pour décoder un sous-code en production, utilisez le champ `wiki` retourné dans la réponse.
</Note>

***

## Authentification échouée

**Symptôme** : `response_code: "01"`, `response_text: "Echec (Code00)"` sur chaque requête.

**Causes probables** :

* L'en-tête `Apikey` est absent ou incorrect
* L'en-tête `Authorization` ne suit pas le format `Bearer {AUTH_TOKEN}`
* Le token d'autorisation a expiré ou été révoqué
* Les clés utilisées appartiennent à un autre projet API

**Solution** :

```bash cURL theme={null}
curl -X POST https://app.ligdicash.com/pay/v01/... \
  -H "Apikey: {API_KEY}" \
  -H "Authorization: Bearer {AUTH_TOKEN}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json"
```

Vérifiez vos clés dans le [dashboard LigdiCash](https://client.ligdicash.com) sous le projet API concerné. Si le problème persiste, contactez le support.

***

## Payin ou payout non activé sur le compte

**Symptôme** : `response_code: "01"`, `response_text: "Echec (Code01)"` dès la création de la transaction.

**Cause** : La fonctionnalité payin ou payout n'est pas activée sur votre projet API LigdiCash.

**Solution** :

Contactez l'équipe LigdiCash pour activer la fonctionnalité concernée : [developper@ligdicash.com](mailto:developper@ligdicash.com) ou via votre Partner Manager. Précisez votre identifiant de projet API.

***

## Mauvaise route pour le payout

**Symptôme** : `response_code: "01"`, `response_text: "Echec (Code02)"` sur l'endpoint `withdrawal/create`, avec la description *"Customer not registered on the platform"*.

**Cause** : L'endpoint `POST /pay/v01/withdrawal/create` effectue un payout vers le **wallet LigdiCash** du client. Il exige donc que le client ait un compte enregistré sur la plateforme LigdiCash. Si vous souhaitez envoyer directement vers un numéro mobile money sans compte LigdiCash préalable, vous devez utiliser l'autre endpoint.

<Warning>
  LigdiCash propose deux routes de payout avec des usages distincts :

  | Endpoint                          | Destination                     | Condition                                |
  | --------------------------------- | ------------------------------- | ---------------------------------------- |
  | `POST /pay/v01/withdrawal/create` | Wallet LigdiCash du client      | Le client doit avoir un compte LigdiCash |
  | `POST /pay/v01/straight/payout`   | Numéro mobile money directement | Aucun compte LigdiCash requis            |

  Utiliser `withdrawal/create` pour un client sans compte LigdiCash retourne systématiquement `Echec (Code02)`.
</Warning>

Consultez [Payout vers wallet LigdiCash](/api-paiement/payout/vers-wallet-ligdicash) et [Payout vers mobile money](/api-paiement/payout/vers-mobile-money) pour choisir la bonne route selon votre cas d'usage.

***

## IP non whitelistée

**Symptôme** : `response_code: "01"`, `response_text: "Echec (Code03)"` (createInvoice) ou `"Echec (Code06)"` (createWithdrawal / createStraightWithdrawal) — *"IP Denied"*.

**Cause** : Les endpoints de payout requièrent que l'adresse IP du serveur appelant soit autorisée sur votre projet LigdiCash. Si votre serveur utilise une IP dynamique (qui change à chaque redémarrage ou reconnexion), vous serez bloqué dès que l'IP change.

**Contrainte importante** : LigdiCash limite le nombre d'IPs whitelistées à **3 adresses maximum** par projet.

**Solution** :

1. Identifiez l'adresse IP fixe de votre serveur de production.
2. Transmettez-la au support technique LigdiCash ([developper@ligdicash.com](mailto:developper@ligdicash.com)) pour qu'il l'ajoute à la whitelist de votre projet — cette opération ne peut pas être faite depuis le dashboard marchand.
3. Si votre infrastructure utilise des IPs dynamiques (hébergement mutualisé, VPS sans IP fixe, CI/CD), vous devez soit :
   * Passer par un **proxy sortant à IP fixe** pour tous vos appels vers LigdiCash
   * Migrer vers un hébergement avec IP dédiée fixe

<Note>
  La contrainte des 3 IPs maximum est importante si vous avez plusieurs environnements (développement, staging, production). Réservez les 3 slots à vos serveurs de production et testez en staging depuis le même IP si possible. Anticipez cette demande avant la mise en production — le délai de configuration dépend du support LigdiCash.
</Note>

***

## Solde marchand insuffisant par opérateur

**Symptôme** : `response_code: "01"`, `response_text: "Echec (Code04)"` (createWithdrawal) ou `"Echec (Code08)"` (createStraightWithdrawal) — *"Merchant balance low"* ou *"Merchant operator account low balance"*.

**Cause** : Votre compte marchand LigdiCash dispose d'un solde distinct **par opérateur**. Un payout vers Orange Money Burkina débitera votre solde Orange Burkina, pas votre solde global. Si ce solde opérateur est insuffisant, le payout échoue même si votre solde global est positif.

**Solution** :

Rechargez le solde de l'opérateur concerné via le [dashboard LigdiCash](https://client.ligdicash.com). Surveillez les soldes par opérateur indépendamment et mettez en place des alertes de bas solde pour chaque opérateur que vous utilisez.

***

## Montant hors limites

**Symptôme** : `response_code: "01"`, `response_text: "Echec (Code02)"` sur createInvoice — *"Wrong amount"*.

**Causes probables** :

* Montant inférieur à 9 XOF ou supérieur à 2 000 000 XOF (limites globales LigdiCash)
* Montant non entier (`500.5` au lieu de `500` ou `501`)
* Plafond quotidien du client mobile money atteint côté opérateur

<Warning>
  Le XOF n'a pas de sous-unité. Le montant doit toujours être un entier. `500.5` sera rejeté.
</Warning>

Validez le montant côté serveur avant d'appeler l'API :

```javascript Node.js theme={null}
function validerMontant(montant) {
  if (!Number.isInteger(montant)) throw new Error("Montant non entier");
  if (montant < 9)        throw new Error("Montant minimum : 9 XOF");
  if (montant > 2000000)  throw new Error("Montant maximum : 2 000 000 XOF");
}
```

***

## Transaction en pending malgré un OTP valide

**Symptôme** : La transaction reste indéfiniment à `status: "pending"` alors que le client affirme avoir généré l'OTP correctement.

**Cause** : Pour les opérateurs en mode OTP USSD (Orange Money Burkina Faso...), le client génère un OTP via son menu USSD en saisissant le montant exact de la transaction. Si vous avez configuré votre intégration pour que **les frais de transaction soient à la charge du client**, le montant que le client doit saisir dans son USSD est le montant de base **plus les frais**. Si le client ne saisit que le montant de base, l'OTP est valide mais le montant attendu ne correspond pas — LigdiCash ne peut pas valider la transaction et la laisse en `pending`.

**Exemple** :

* Montant de la transaction : 10 000 XOF
* Frais à la charge du client : 200 XOF
* Montant à saisir dans le USSD : **10 200 XOF**
* Si le client saisit 10 000 XOF → OTP valide mais montant incorrect → `pending` indéfini

**Solution** :

Si les frais sont à la charge du client, votre interface doit afficher clairement le montant total à saisir dans l'USSD, frais inclus. Communiquez ce montant dans vos instructions à l'écran avant que le client ne compose le code USSD.

```
⚠️ Composez le code USSD avec le montant exact : 10 200 XOF (10 000 XOF + 200 XOF de frais)
```

Si les frais sont à votre charge (côté marchand), le client saisit uniquement le montant de la transaction, et vous l'absorbez dans votre marge.

***

## Incompréhension du statut pending

**Symptôme** : Vous attendez un callback avec `status: "notcompleted"` après un OTP invalide ou expiré, mais aucun callback n'arrive — la transaction reste indéfiniment en `pending`.

**Cause** : LigdiCash ne considère pas automatiquement une transaction comme échouée parce que l'OTP était incorrect. Du point de vue de LigdiCash, la transaction peut encore aboutir si le client fournit le bon OTP — la fenêtre d'attente est indéfinie. LigdiCash n'envoie un callback `notcompleted` que lorsque l'opérateur lui-même communique un signal d'échec définitif. Tant que ce signal n'arrive pas, la transaction reste en `pending`, qu'il s'écoule 5 minutes ou plusieurs heures.

<Warning>
  Ne vous fiez pas à LigdiCash pour décider quand une transaction est "expirée". C'est à votre système de définir un timeout métier et d'agir en conséquence.
</Warning>

**Solution** :

Définissez un timeout côté marchand (par exemple 15 minutes après la création) au-delà duquel vous passez la transaction en `timeout` dans votre base et notifiez le client pour relancer la démarche :

```javascript Node.js theme={null}
// Vérification périodique des transactions pending
async function verifierTransactionsPending() {
  const timeout = new Date(Date.now() - 15 * 60 * 1000); // 15 minutes

  const pendingTransactions = await db.transactions.findAll({
    where: {
      status: "pending",
      created_at: { [Op.lt]: timeout },
    },
  });

  for (const tx of pendingTransactions) {
    // Dernier appel à confirm avant d'abandonner
    const confirm = await appellerConfirm(tx.ligdicash_token);

    if (confirm.status === "completed") {
      await tx.update({ status: "completed" });
    } else {
      // Toujours pending après 15 min → timeout côté marchand
      await tx.update({ status: "timeout" });
      await notifierClient(tx, "Votre paiement n'a pas abouti. Veuillez réessayer.");
    }
  }
}
```

***

## Double callback non géré

**Symptôme** : Une même transaction déclenche deux actions dans votre système (double livraison, double crédit de compte...).

**Cause** : LigdiCash envoie systématiquement **deux requêtes POST** pour chaque événement de callback : une en `application/x-www-form-urlencoded` et une en `application/json`. Ce comportement est normal et documenté. Si votre handler callback ne déduplique pas, il traitera les deux requêtes.

**Solution** :

Vérifiez en base si la transaction a déjà été traitée avant tout traitement métier :

```javascript Node.js theme={null}
async function handleCallback(payload) {
  const transactionId = payload.custom_data?.find(
    e => e.keyof_customdata === "transaction_id"
  )?.valueof_customdata;

  const transaction = await db.transactions.findOne({
    where: { transaction_id: transactionId }
  });

  if (transaction?.status === "completed") {
    return res.status(200).send("OK"); // Déjà traité
  }

  // ... traitement métier
}
```

Voir [Idempotence et déduplication](/api-paiement/callback/idempotence) pour le pattern complet.

***

## Callback reçu mais transaction introuvable en base

**Symptôme** : Vous recevez un callback valide mais votre système ne retrouve pas la transaction correspondante, ou l'appel à `confirm` retourne `Echec (Code02) — Invoice not found`.

**Causes probables** :

* Vous recherchez la transaction avec un identifiant autre que le `transaction_id` que vous avez stocké à la création (ID interne, numéro de commande, token callback...)
* Vous utilisez le `token` du payload callback pour appeler `confirm`, alors qu'il faut utiliser le `token` de création

<Warning>
  Deux tokens différents circulent dans votre intégration :

  | Token                 | Où                                          | Usage                                        |
  | --------------------- | ------------------------------------------- | -------------------------------------------- |
  | Token de **création** | Retourné dans `response_text` à la création | À stocker en base, à utiliser pour `confirm` |
  | Token de **callback** | Présent dans le payload callback            | À ne jamais utiliser pour `confirm`          |

  Ces deux tokens sont distincts et non interchangeables.
</Warning>

**Solution** :

Stockez le `transaction_id` que vous avez envoyé dans `custom_data` à la création, et retrouvez la transaction avec ce même `transaction_id` dans le callback :

```javascript Node.js theme={null}
// À la création — stocker le transaction_id et le token LigdiCash
await db.transactions.create({
  transaction_id: "TXN-MON-ID-INTERNE",  // votre identifiant
  ligdicash_token: response.token,         // token LigdiCash de création
  status: "pending",
});

// Dans le callback — retrouver par transaction_id, pas par token callback
function extraireTransactionId(callbackPayload) {
  const customData = callbackPayload.custom_data;
  if (!Array.isArray(customData)) return null;
  const entry = customData.find(e => e.keyof_customdata === "transaction_id");
  return entry?.valueof_customdata ?? null;
}

const transactionId = extraireTransactionId(callbackPayload);
const transaction = await db.transactions.findOne({
  where: { transaction_id: transactionId }
});
```

Voir [Pattern transaction\_id](/concepts/transaction-id-pattern) et [Sécurisation du callback](/api-paiement/callback/securisation) pour les patterns complets.

***

## Pages associées

* [Le champ wiki](/erreurs/champ-wiki) — décoder `Echec (CodeXX)` en message lisible
* [Liste des sous-codes](/erreurs/sous-codes) — référence complète par endpoint
* [Idempotence et déduplication](/api-paiement/callback/idempotence) — gérer les double callbacks
* [Sécurisation du callback](/api-paiement/callback/securisation) — re-vérification via confirm
* [Pattern transaction\_id](/concepts/transaction-id-pattern) — identifier vos transactions de façon fiable
