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 describes the architectural principles to follow for a robust and secure LigdiCash integration. It targets developers designing the infrastructure around the API.

Core principle: everything goes through the backend

Your LigdiCash credentials (Apikey and Auth Token) must never appear in your frontend, mobile app, or public source code. All requests to the LigdiCash API leave from your server.
Client / Mobile app
       |

Your backend (proxy)   ←→   LigdiCash API
       |

Your database
An exposed Apikey or Auth Token lets anyone create invoices or trigger payouts on your behalf. Store these secrets in server-side environment variables only.

Transactions table structure

Your database must hold enough information to trace every transaction, handle the callback idempotently, and run a fallback poll.
CREATE TABLE payment_transactions (
    id              VARCHAR(64) PRIMARY KEY,   -- your internal transaction_id
    order_id        VARCHAR(64) NOT NULL,
    amount          INTEGER NOT NULL,           -- in CFA francs, integer
    status          VARCHAR(20) NOT NULL DEFAULT 'pending',
                                               -- pending | completed | notcompleted
    ligdicash_token TEXT,                       -- token returned at creation
    ligdicash_ref   VARCHAR(64),               -- operator_id returned in the callback
    callback_received_at TIMESTAMPTZ,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX ON payment_transactions (order_id);
CREATE INDEX ON payment_transactions (status, created_at);
The ligdicash_token field must be stored as soon as the invoice is created. This is the token you’ll use to call the confirm endpoint, both in the callback and in the fallback poll. Do not rely on the token received in the callback — it’s a different token.

Endpoints to expose on the backend

Your backend acts as a proxy between your frontend and the LigdiCash API. At a minimum, expose these three routes:
RouteMethodRole
/api/payment/initiatePOSTCreates the LigdiCash invoice, stores the token, returns the payment URL
/api/payment/:id/statusGETReturns the transaction status (polls LigdiCash if still pending)
/api/ligdicash/callbackPOSTReceives LigdiCash notifications, re-verifies, updates the database

Initiation route

Node.js (Express)
app.post("/api/payment/initiate", async (req, res) => {
  const { orderId, amount, description, customerEmail } = req.body;

  // Generate a unique transaction_id
  const transactionId = `txn_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;

  // Create the transaction in the database
  await db.paymentTransactions.insert({
    id: transactionId,
    order_id: orderId,
    amount,
    status: "pending",
  });

  // Call LigdiCash
  const ligdicash = await fetch(
    "https://app.ligdicash.com/pay/v01/redirect/checkout-invoice/create",
    {
      method: "POST",
      headers: {
        Apikey: process.env.LIGDICASH_API_KEY,
        Authorization: `Bearer ${process.env.LIGDICASH_AUTH_TOKEN}`,
        Accept: "application/json",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        commande: {
          invoice: {
            items: [],
            total_amount: amount,
            devise: "XOF",
            description,
            customer: "",
            customer_email: customerEmail ?? "",
            external_id: "",
            otp: "",
          },
          store: {
            name: process.env.STORE_NAME,
            website_url: process.env.STORE_URL,
          },
          actions: {
            cancel_url: `${process.env.FRONTEND_URL}/payment/cancel?txn=${transactionId}`,
            return_url: `${process.env.FRONTEND_URL}/payment/success?txn=${transactionId}`,
            callback_url: `${process.env.BACKEND_URL}/api/ligdicash/callback`,
          },
          custom_data: { transaction_id: transactionId },
        },
      }),
    }
  ).then((r) => r.json());

  if (ligdicash.response_code !== "00") {
    await db.paymentTransactions.update(transactionId, { status: "notcompleted" });
    return res.status(502).json({ error: "ligdicash_error", detail: ligdicash.description });
  }

  // Store the token
  await db.paymentTransactions.update(transactionId, {
    ligdicash_token: ligdicash.token,
  });

  res.json({
    transaction_id: transactionId,
    pay_url: ligdicash.response_text,
  });
});

Callback route

Node.js (Express)
app.post("/api/ligdicash/callback", express.json(), async (req, res) => {
  // Reply 200 immediately
  res.sendStatus(200);

  // Extract the transaction_id
  const customData = req.body.custom_data ?? [];
  const entry = Array.isArray(customData)
    ? customData.find((e) => e.keyof_customdata === "transaction_id")
    : null;

  if (!entry?.valueof_customdata) return;

  const transactionId = entry.valueof_customdata;
  const txn = await db.paymentTransactions.findById(transactionId);

  // Idempotency: skip if already processed
  if (!txn || txn.status !== "pending") return;

  // Re-verify via confirm
  const confirm = await fetch(
    `https://app.ligdicash.com/pay/v01/redirect/checkout-invoice/confirm?token=${txn.ligdicash_token}`,
    {
      headers: {
        Apikey: process.env.LIGDICASH_API_KEY,
        Authorization: `Bearer ${process.env.LIGDICASH_AUTH_TOKEN}`,
        Accept: "application/json",
      },
    }
  ).then((r) => r.json());

  await db.paymentTransactions.update(transactionId, {
    status: confirm.status ?? "pending",
    callback_received_at: new Date(),
  });

  if (confirm.status === "completed") {
    await orders.confirm(txn.order_id);
  }
});

Polling fallback

LigdiCash does not enforce a retry policy for callbacks. If your endpoint was unavailable or a callback was missed, a transaction can stay pending indefinitely. Set up periodic polling on the backend.
Polling fallback (cron)
// Run every 5 minutes by a cron job
async function pollPendingTransactions() {
  // Transactions pending for more than 2 minutes and less than 24h
  const txns = await db.paymentTransactions.findAll({
    status: "pending",
    created_at: { gte: new Date(Date.now() - 24 * 3600 * 1000) },
    callback_received_at: null,
  });

  for (const txn of txns) {
    if (!txn.ligdicash_token) continue;

    const confirm = await fetch(
      `https://app.ligdicash.com/pay/v01/redirect/checkout-invoice/confirm?token=${txn.ligdicash_token}`,
      {
        headers: {
          Apikey: process.env.LIGDICASH_API_KEY,
          Authorization: `Bearer ${process.env.LIGDICASH_AUTH_TOKEN}`,
          Accept: "application/json",
        },
      }
    ).then((r) => r.json());

    if (confirm.status && confirm.status !== "pending") {
      await db.paymentTransactions.update(txn.id, { status: confirm.status });

      if (confirm.status === "completed") {
        await orders.confirm(txn.order_id);
      }
    }
  }
}
LigdiCash never automatically flips a transaction to notcompleted. A transaction stays pending indefinitely if the customer never finalizes the payment. After 24h, you can consider the transaction expired and close it yourself.

Logging and audit

Keep a trace of every interaction with LigdiCash to make troubleshooting easier in case of a dispute.
CREATE TABLE payment_logs (
    id              BIGSERIAL PRIMARY KEY,
    transaction_id  VARCHAR(64),
    direction       VARCHAR(10) NOT NULL,  -- 'outgoing' | 'incoming'
    type            VARCHAR(30) NOT NULL,  -- 'create' | 'confirm' | 'callback' | 'poll'
    payload         JSONB,
    response        JSONB,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Log both outgoing requests to LigdiCash and incoming callbacks. If a transaction is disputed, you’ll be able to present a full timestamped history.

Environment variables

.env
# LigdiCash credentials — NEVER expose them on the client side
LIGDICASH_API_KEY=your_apikey
LIGDICASH_AUTH_TOKEN=your_auth_token

# URLs
STORE_NAME=My Store
STORE_URL=https://mystore.com
FRONTEND_URL=https://mystore.com
BACKEND_URL=https://api.mystore.com

Summary diagram