Skip to main content

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.

This page lists the most frequent errors seen during integration and production, based on real-world feedback from LigdiCash merchants.
Sub-codes (Echec (CodeXX)) are specific to each endpoint. See the sub-codes reference for the full mapping. To decode a sub-code in production, use the wiki field returned in the response.

Authentication failure

Symptom: response_code: "01", response_text: "Echec (Code00)" on every request. Likely causes:
  • The Apikey header is missing or wrong
  • The Authorization header does not follow the Bearer {AUTH_TOKEN} format
  • The authorization token has expired or been revoked
  • The keys you are using belong to a different API project
Solution:
cURL
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"
Check your keys in the LigdiCash dashboard under the relevant API project. If the issue persists, contact support.

Payin or payout not enabled on the account

Symptom: response_code: "01", response_text: "Echec (Code01)" right at transaction creation. Cause: The payin or payout feature is not enabled on your LigdiCash API project. Solution: Contact the LigdiCash team to enable the relevant feature: developper@ligdicash.com or via your Partner Manager. Provide your API project identifier.

Wrong payout route

Symptom: response_code: "01", response_text: "Echec (Code02)" on the withdrawal/create endpoint, with description “Customer not registered on the platform”. Cause: The POST /pay/v01/withdrawal/create endpoint performs a payout to the customer’s LigdiCash wallet. It therefore requires the customer to have an account on the LigdiCash platform. If you want to send funds directly to a mobile money number without a pre-existing LigdiCash account, you must use the other endpoint.
LigdiCash offers two payout routes with distinct uses:
EndpointDestinationRequirement
POST /pay/v01/withdrawal/createCustomer’s LigdiCash walletThe customer must have a LigdiCash account
POST /pay/v01/straight/payoutMobile money number directlyNo LigdiCash account required
Using withdrawal/create for a customer without a LigdiCash account systematically returns Echec (Code02).
See Payout to LigdiCash wallet and Payout to mobile money to pick the right route for your use case.

IP not whitelisted

Symptom: response_code: "01", response_text: "Echec (Code03)" (createInvoice) or "Echec (Code06)" (createWithdrawal / createStraightWithdrawal) — “IP Denied”. Cause: Payout endpoints require the calling server’s IP address to be whitelisted on your LigdiCash project. If your server uses a dynamic IP (one that changes on every restart or reconnection), you will be blocked as soon as the IP changes. Important constraint: LigdiCash limits the number of whitelisted IPs to 3 addresses maximum per project. Solution:
  1. Identify the static IP address of your production server.
  2. Send it to LigdiCash technical support (developper@ligdicash.com) so they can add it to your project’s whitelist — this cannot be done from the merchant dashboard.
  3. If your infrastructure uses dynamic IPs (shared hosting, VPS without static IP, CI/CD), you must either:
    • Route all calls to LigdiCash through an outbound proxy with a static IP
    • Migrate to hosting with a dedicated static IP
The 3-IP cap matters if you have multiple environments (development, staging, production). Reserve the 3 slots for your production servers and test in staging from the same IP if possible. Anticipate this request before going live — the configuration delay depends on LigdiCash support.

Merchant balance too low on a specific operator

Symptom: response_code: "01", response_text: "Echec (Code04)" (createWithdrawal) or "Echec (Code08)" (createStraightWithdrawal) — “Merchant balance low” or “Merchant operator account low balance”. Cause: Your LigdiCash merchant account holds a separate balance per operator. A payout to Orange Money Burkina debits your Orange Burkina balance, not your global balance. If that operator-level balance is too low, the payout fails even when your global balance is positive. Solution: Top up the balance for the relevant operator via the LigdiCash dashboard. Monitor balances per operator independently and set up low-balance alerts for every operator you use.

Amount out of range

Symptom: response_code: "01", response_text: "Echec (Code02)" on createInvoice — “Wrong amount”. Likely causes:
  • Amount below 9 CFA francs or above 2,000,000 CFA francs (LigdiCash global limits)
  • Non-integer amount (500.5 instead of 500 or 501)
  • Customer’s daily mobile money limit reached on the operator side
The CFA franc (XOF) has no sub-unit. The amount must always be an integer. 500.5 will be rejected.
Validate the amount on your server before calling the API:
Node.js
function validateAmount(amount) {
  if (!Number.isInteger(amount)) throw new Error("Amount must be an integer");
  if (amount < 9)        throw new Error("Minimum amount: 9 CFA francs");
  if (amount > 2000000)  throw new Error("Maximum amount: 2,000,000 CFA francs");
}

Transaction stuck in pending despite a valid OTP

Symptom: The transaction stays in status: "pending" indefinitely even though the customer says they generated the OTP correctly. Cause: For operators in USSD OTP mode (Orange Money Burkina Faso, etc.), the customer generates an OTP from their USSD menu by entering the exact transaction amount. If you configured your integration so that fees are charged to the customer, the amount the customer must enter in the USSD is the base amount plus the fees. If the customer only enters the base amount, the OTP is valid but the expected amount does not match — LigdiCash cannot validate the transaction and leaves it in pending. Example:
  • Transaction amount: 10,000 CFA francs
  • Fees charged to the customer: 200 CFA francs
  • Amount to enter in the USSD: 10,200 CFA francs
  • If the customer enters 10,000 CFA francs → OTP valid but wrong amount → indefinite pending
Solution: If fees are charged to the customer, your interface must clearly display the total amount to enter in the USSD, fees included. Communicate this amount in your on-screen instructions before the customer dials the USSD code.
⚠️ Dial the USSD code with the exact amount: 10,200 CFA francs (10,000 CFA francs + 200 CFA francs in fees)
If you absorb the fees on your side, the customer enters only the transaction amount and you take the cost in your margin.

Misunderstanding the pending status

Symptom: You expect a callback with status: "notcompleted" after an invalid or expired OTP, but no callback arrives — the transaction stays in pending indefinitely. Cause: LigdiCash does not automatically consider a transaction failed because the OTP was wrong. From LigdiCash’s point of view, the transaction can still succeed if the customer provides the right OTP — the waiting window is undefined. LigdiCash only sends a notcompleted callback when the operator itself signals a definitive failure. Until that signal arrives, the transaction stays in pending, whether 5 minutes or several hours pass.
Don’t rely on LigdiCash to decide when a transaction is “expired”. It is your system’s responsibility to define a business timeout and act on it.
Solution: Define a merchant-side timeout (for example 15 minutes after creation) past which you flip the transaction to timeout in your database and notify the customer to start over:
Node.js
// Periodic check of pending transactions
async function checkPendingTransactions() {
  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) {
    // Last call to confirm before giving up
    const confirm = await callConfirm(tx.ligdicash_token);

    if (confirm.status === "completed") {
      await tx.update({ status: "completed" });
    } else {
      // Still pending after 15 min → merchant-side timeout
      await tx.update({ status: "timeout" });
      await notifyCustomer(tx, "Your payment did not go through. Please try again.");
    }
  }
}

Double callback not deduplicated

Symptom: A single transaction triggers two actions in your system (double delivery, double credit, etc.). Cause: LigdiCash systematically sends two POST requests for each callback event: one as application/x-www-form-urlencoded and one as application/json. This behavior is normal and documented. If your callback handler does not deduplicate, it will process both requests. Solution: Check your database to see if the transaction has already been processed before running any business logic:
Node.js
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"); // Already processed
  }

  // ... business logic
}
See Idempotency and deduplication for the complete pattern.

Callback received but transaction not found in database

Symptom: You receive a valid callback but your system can’t find the matching transaction, or calling confirm returns Echec (Code02) — Invoice not found. Likely causes:
  • You’re looking up the transaction with an identifier other than the transaction_id you stored at creation (internal ID, order number, callback token, etc.)
  • You’re using the token from the callback payload to call confirm, when you should use the token from creation
Two different tokens flow through your integration:
TokenWhereUse
Creation tokenReturned in response_text at creationStore in your database, use for confirm
Callback tokenPresent in the callback payloadNever use to call confirm
These two tokens are different and not interchangeable.
Solution: Store the transaction_id you sent in custom_data at creation, and look up the transaction with that same transaction_id in the callback:
Node.js
// On creation — store the transaction_id and the LigdiCash token
await db.transactions.create({
  transaction_id: "TXN-MY-INTERNAL-ID",  // your identifier
  ligdicash_token: response.token,         // LigdiCash creation token
  status: "pending",
});

// In the callback — look up by transaction_id, not by callback token
function extractTransactionId(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 = extractTransactionId(callbackPayload);
const transaction = await db.transactions.findOne({
  where: { transaction_id: transactionId }
});
See transaction_id pattern and Callback security for the complete patterns.