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:
| Route | Method | Role |
|---|
/api/payment/initiate | POST | Creates the LigdiCash invoice, stores the token, returns the payment URL |
/api/payment/:id/status | GET | Returns the transaction status (polls LigdiCash if still pending) |
/api/ligdicash/callback | POST | Receives LigdiCash notifications, re-verifies, updates the database |
Initiation route
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
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.
// 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
# 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
Related pages