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

# E-commerce guide: cart → payment → delivery

> Build a complete payment flow with LigdiCash: order creation, mobile money payment, callback handling, and delivery confirmation.

This guide walks through the end-to-end flow for an e-commerce site: from cart validation on the frontend to delivery confirmation on the backend. It uses hosted payin, the simplest integration mode and the one compatible with every operator.

**Prerequisites**: you have a LigdiCash API project with an Apikey and an Auth Token. See [Create an API project](/en/concepts/api-project).

## Flow overview

```
Customer         Frontend          Backend          LigdiCash
  |                 |                  |                 |
  |-- Validate ->   |                  |                 |
  |   cart          |-- POST /pay --> |                  |
  |                 |                  |-- create -----> |
  |                 |                  | <-- token ----- |
  |                 | <-- pay_url ---- |                 |
  |-- Redirect ->   |                  |                 |
  |                 |--- Open URL ---> |                 |
  |  [Payment on the LigdiCash page]   |                 |
  |                 |                  | <-- callback -- |
  |                 |                  |-- confirm ----> |
  |                 |                  | <-- status ---- |
  |                 |                  |-- Confirms      |
  |                 |                  |   order         |
  | <-- Return ---- |                  |                 |
  |   return_url    |-- GET /status -> |                 |
  |                 | <-- result ----- |                 |
```

<Warning>
  Never call the LigdiCash API directly from the frontend. Your Apikey and Auth Token must stay on your backend. See [Recommended architecture](/en/guides/recommended-architecture).
</Warning>

## Step 1 — Store the order before payment

Before creating the LigdiCash invoice, persist the order in your database with status `pending`. You'll need its identifier to track the payment.

```sql theme={null}
INSERT INTO orders (id, customer_id, amount, status, created_at)
VALUES ('order_7f3a9b', 42, 15000, 'pending', NOW());
```

<Tip>
  Use a non-guessable internal identifier (UUID or prefixed ID) as your `transaction_id`. You'll pass it in `custom_data` and use it to identify the order in the callback.
</Tip>

## Step 2 — Create the invoice on the backend

Your backend calls the create-invoice endpoint and stores the returned `token`.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://app.ligdicash.com/pay/v01/redirect/checkout-invoice/create \
    -H "Apikey: {API_KEY}" \
    -H "Authorization: Bearer {AUTH_TOKEN}" \
    -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    -d '{
      "commande": {
        "invoice": {
          "items": [
            {
              "name": "Premium subscription",
              "description": "1 month of Premium access",
              "quantity": 1,
              "unit_price": 15000,
              "total_price": 15000
            }
          ],
          "total_amount": 15000,
          "devise": "XOF",
          "description": "Order #order_7f3a9b",
          "customer": "",
          "customer_firstname": "Aminata",
          "customer_lastname": "Ouedraogo",
          "customer_email": "aminata@example.com",
          "external_id": "",
          "otp": ""
        },
        "store": {
          "name": "My Store",
          "website_url": "https://mystore.com"
        },
        "actions": {
          "cancel_url": "https://mystore.com/payment/cancel",
          "return_url": "https://mystore.com/payment/success",
          "callback_url": "https://mystore.com/api/ligdicash/callback"
        },
        "custom_data": {
          "transaction_id": "order_7f3a9b"
        }
      }
    }'
  ```

  ```javascript Node.js theme={null}
  const response = 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: [
              {
                name: "Premium subscription",
                description: "1 month of Premium access",
                quantity: 1,
                unit_price: 15000,
                total_price: 15000,
              },
            ],
            total_amount: 15000,
            devise: "XOF",
            description: "Order #order_7f3a9b",
            customer: "",
            customer_firstname: "Aminata",
            customer_lastname: "Ouedraogo",
            customer_email: "aminata@example.com",
            external_id: "",
            otp: "",
          },
          store: {
            name: "My Store",
            website_url: "https://mystore.com",
          },
          actions: {
            cancel_url: "https://mystore.com/payment/cancel",
            return_url: "https://mystore.com/payment/success",
            callback_url: "https://mystore.com/api/ligdicash/callback",
          },
          custom_data: {
            transaction_id: "order_7f3a9b",
          },
        },
      }),
    }
  );

  const data = await response.json();
  // data.response_code === "00" → success
  // data.response_text → payment URL
  ```

  ```php PHP theme={null}
  $client = new \GuzzleHttp\Client();

  $response = $client->post(
      'https://app.ligdicash.com/pay/v01/redirect/checkout-invoice/create',
      [
          'headers' => [
              'Apikey'        => $_ENV['LIGDICASH_API_KEY'],
              'Authorization' => 'Bearer ' . $_ENV['LIGDICASH_AUTH_TOKEN'],
              'Accept'        => 'application/json',
              'Content-Type'  => 'application/json',
          ],
          'json' => [
              'commande' => [
                  'invoice' => [
                      'items' => [
                          [
                              'name'        => 'Premium subscription',
                              'description' => '1 month of Premium access',
                              'quantity'    => 1,
                              'unit_price'  => 15000,
                              'total_price' => 15000,
                          ],
                      ],
                      'total_amount'      => 15000,
                      'devise'            => 'XOF',
                      'description'       => 'Order #order_7f3a9b',
                      'customer'          => '',
                      'customer_firstname'=> 'Aminata',
                      'customer_lastname' => 'Ouedraogo',
                      'customer_email'    => 'aminata@example.com',
                      'external_id'       => '',
                      'otp'               => '',
                  ],
                  'store' => [
                      'name'        => 'My Store',
                      'website_url' => 'https://mystore.com',
                  ],
                  'actions' => [
                      'cancel_url'   => 'https://mystore.com/payment/cancel',
                      'return_url'   => 'https://mystore.com/payment/success',
                      'callback_url' => 'https://mystore.com/api/ligdicash/callback',
                  ],
                  'custom_data' => [
                      'transaction_id' => 'order_7f3a9b',
                  ],
              ],
          ],
      ]
  );

  $data = json_decode($response->getBody(), true);
  // $data['response_code'] === '00' → success
  // $data['response_text'] → payment URL
  ```
</CodeGroup>

Success response (`response_code: "00"`):

```json theme={null}
{
  "response_code": "00",
  "response_text": "https://app.ligdicash.com/pay/v01/redirect/checkout-invoice/confirm?token=eyJ0...",
  "description": "Checkout-Invoice created with success.",
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}
```

**Store the `token` in your database** linked to the order. You'll need it to verify the payment.

```sql theme={null}
UPDATE orders
SET ligdicash_token = 'eyJ0eXAiOiJKV1Qi...'
WHERE id = 'order_7f3a9b';
```

## Step 3 — Redirect the customer to the payment page

Return `response_text` (the payment URL) to your frontend, then redirect the customer.

```javascript Frontend (JavaScript) theme={null}
// Backend call → returns the payment URL
const { payUrl } = await fetch("/api/orders/order_7f3a9b/pay", {
  method: "POST",
}).then((r) => r.json());

// Redirect in the same tab (recommended)
window.location.href = payUrl;

// Or in a new tab
// window.open(payUrl, "_blank");
```

<Warning>
  Never open the LigdiCash URL in an `<iframe>`. LigdiCash blocks iframe rendering. Use a same-tab redirect, a new tab, or a popup. See [Common pitfalls](/en/payment-api/hosted-payin/common-pitfalls).
</Warning>

The customer lands on the LigdiCash payment page, picks their mobile money operator, and completes the payment. LigdiCash then redirects them to your `return_url` (success) or `cancel_url` (cancellation).

## Step 4 — Handle the callback

LigdiCash sends a POST notification to your `callback_url` whenever the transaction status changes. Your backend must:

1. Identify the order via `custom_data`
2. Re-verify the status using the `confirm` endpoint (never trust the payload alone)
3. Update the order in the database
4. Reply `200 OK`

```javascript Node.js (Express) theme={null}
app.post("/api/ligdicash/callback", express.json(), async (req, res) => {
  // Reply 200 immediately to avoid retries
  res.sendStatus(200);

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

  if (!entry) return;

  const transactionId = entry.valueof_customdata;

  // Look up the order and its LigdiCash token
  const order = await db.orders.findById(transactionId);
  if (!order || order.status !== "pending") return;

  // Re-verify the status with LigdiCash
  const confirm = await fetch(
    `https://app.ligdicash.com/pay/v01/redirect/checkout-invoice/confirm/?invoiceToken=${order.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 === "completed") {
    await db.orders.update(transactionId, { status: "paid" });
    await delivery.trigger(transactionId);
  } else if (confirm.status === "notcompleted") {
    await db.orders.update(transactionId, { status: "failed" });
  }
  // If "pending": do nothing, wait for the next callback
});
```

<Note>
  LigdiCash sends **two POST requests** to your callback for every event: one as `application/x-www-form-urlencoded` and one as `application/json`. Handle them idempotently. See [Callback idempotency](/en/payment-api/callback/idempotency).
</Note>

## Step 5 — Verify the status on the frontend

After the redirect to your `return_url`, the frontend asks your backend for the final result to display. The `return_url` is a UI signal — it does not prove the payment succeeded.

```javascript Frontend — return_url page theme={null}
// Read transaction_id from the URL (e.g. /payment/success?order=order_7f3a9b)
const params = new URLSearchParams(window.location.search);
const orderId = params.get("order");

const { status } = await fetch(`/api/orders/${orderId}/status`).then(
  (r) => r.json()
);

switch (status) {
  case "paid":
    showConfirmation();
    break;
  case "pending":
    // The callback hasn't arrived yet — show a loader and poll
    showVerificationWait();
    break;
  case "failed":
    showError();
    break;
}
```

```javascript Backend — GET /api/orders/:id/status theme={null}
app.get("/api/orders/:id/status", async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  if (!order) return res.status(404).json({ error: "not_found" });

  // If still pending, ask LigdiCash in real time
  if (order.status === "pending") {
    const confirm = await fetch(
      `https://app.ligdicash.com/pay/v01/redirect/checkout-invoice/confirm/?invoiceToken=${order.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 === "completed") {
      await db.orders.update(order.id, { status: "paid" });
      order.status = "paid";
    }
  }

  res.json({ status: order.status });
});
```

## Recap

<Steps>
  <Step title="Store the order">
    Create the order in your database with status `pending` before any LigdiCash call.
  </Step>

  <Step title="Create the invoice">
    Backend → POST `/pay/v01/redirect/checkout-invoice/create`. Store the returned `token`.
  </Step>

  <Step title="Redirect the customer">
    Frontend → redirect to `response_text` (same tab or popup). Never an iframe.
  </Step>

  <Step title="Handle the callback">
    Reply `200` immediately, then re-verify via `confirm` with the stored token.
  </Step>

  <Step title="Confirm the order">
    Update the status in your database and trigger delivery if `status === "completed"`.
  </Step>

  <Step title="Display the result">
    Frontend → ask your backend for the real status after returning to `return_url`.
  </Step>
</Steps>

## Related pages

* [Create an invoice](/en/payment-api/hosted-payin/create-invoice) — full endpoint parameters
* [Callback security](/en/payment-api/callback/security) — detailed re-verification pattern
* [Parse custom\_data](/en/payment-api/callback/parse-custom-data) — the 3 possible shapes
* [Recommended architecture](/en/guides/recommended-architecture) — full backend structure
