> ## 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.

# Recommended architecture

> How to structure your backend for a secure LigdiCash integration: proxy, database, callback handling, and polling fallback.

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
```

<Warning>
  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.
</Warning>

## Transactions table structure

Your database must hold enough information to trace every transaction, handle the callback idempotently, and run a fallback poll.

```sql theme={null}
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);
```

<Note>
  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.
</Note>

## 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

```javascript Node.js (Express) theme={null}
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

```javascript Node.js (Express) theme={null}
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/?invoiceToken=${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.

```javascript Polling fallback (cron) theme={null}
// 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/?invoiceToken=${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);
      }
    }
  }
}
```

<Note>
  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.
</Note>

## Logging and audit

Keep a trace of every interaction with LigdiCash to make troubleshooting easier in case of a dispute.

```sql theme={null}
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()
);
```

<Tip>
  Log both outgoing requests to LigdiCash and incoming callbacks. If a transaction is disputed, you'll be able to present a full timestamped history.
</Tip>

## Environment variables

```bash .env theme={null}
# 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

```mermaid theme={null}
flowchart LR
    CLIENT["Customer"]
    FE["Frontend / Mobile app"]
    PROXY["Your backend"]
    DB[("Database")]
    CRON["Polling cron"]
    LC["LigdiCash API"]
    PAY["LigdiCash\npayment page"]

    CLIENT -->|"1. initiates"| FE
    FE -->|"2. POST /initiate"| PROXY
    PROXY -->|"3. INSERT pending"| DB
    PROXY -->|"4. POST /create"| LC
    LC -->|"5. token + pay_url"| PROXY
    PROXY -->|"6. stores token"| DB
    PROXY -->|"7. pay_url"| FE
    FE -->|"8. redirects"| PAY
    CLIENT -->|"9. pays"| PAY
    PAY -->|"10. POST /callback"| PROXY
    PROXY -->|"11. GET /confirm"| LC
    LC -->|"12. status"| PROXY
    PROXY -->|"13. UPDATE status"| DB
    CRON -.->|"poll pending"| DB
    CRON -.->|"GET /confirm"| LC
```

## Related pages

* [Callback security](/en/payment-api/callback/security) — why you must always re-verify
* [Callback idempotency](/en/payment-api/callback/idempotency) — handle the LigdiCash double send
* [transaction\_id pattern](/en/concepts/transaction-id-pattern) — identify transactions reliably
* [Production checklist](/en/guides/production-checklist) — go-live verifications
