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

# Securing the callback

> Why you should never trust the received payload, and how to re-verify each callback with the token stored at creation.

Your callback URL is a public POST route. Anyone who knows this URL can send a forged payload — with a `status: "completed"` and an amount of their choosing. If you process this payload without verification, you will fulfill an order or trigger a payout based on a payment that never happened.

**The golden rule: never act on the received payload. Always re-verify.**

## The re-verification pattern

<Steps>
  <Step title="At creation — store the token">
    When creating a transaction (payin or payout), store the `token` returned by the API in your database, linked to your `transaction_id`.

    ```json theme={null}
    // Creation response — to be stored
    {
      "response_code": "00",
      "token": "{INVOICE_TOKEN}"
    }
    ```
  </Step>

  <Step title="On receiving the callback — extract the identifier">
    Receive the callback. Extract your `transaction_id` from the `custom_data` array by filtering on `keyof_customdata`.

    ```javascript theme={null}
    const entry = payload.custom_data?.find(
      (item) => item.keyof_customdata === "transaction_id"
    );
    const transactionId = entry?.valueof_customdata;
    ```
  </Step>

  <Step title="Look up the stored token">
    Search your database for the token associated with this `transaction_id`. If no record matches, ignore the callback — it is likely fraudulent.

    ```javascript theme={null}
    const stored = await db.transactions.findOne({ transaction_id: transactionId });
    if (!stored) return; // unsolicited callback
    ```
  </Step>

  <Step title="Call the confirm endpoint with the stored token">
    Call the LigdiCash verification endpoint with the token **you** stored — not the one in the callback (which is always empty for payins).

    ```javascript theme={null}
    // For a hosted payin
    const params = new URLSearchParams({ invoiceToken: stored.token });
    const verify = await fetch(
      `https://app.ligdicash.com/pay/v01/redirect/checkout-invoice/confirm?${params}`,
      { headers: { Apikey: API_KEY, Authorization: `Bearer ${AUTH_TOKEN}` } }
    );
    const result = await verify.json();
    ```
  </Step>

  <Step title="Act on the result of confirm, not on the payload">
    Use only the `status` returned by `confirm` to decide on your action.

    ```javascript theme={null}
    if (result.status === 'completed') {
      await db.orders.markAsPaid(transactionId);
    }
    ```
  </Step>
</Steps>

```mermaid theme={null}
flowchart TD
    A["Callback received — POST /callback_url"] --> B["Extract transaction_id\nfrom custom_data"]
    B --> C{transaction_id\nfound in DB?}
    C -->|No| D["Ignore\nunsolicited callback"]
    C -->|Yes| E{Status in DB?}
    E -->|"completed / notcompleted"| F["Ignore\nduplicate already processed"]
    E -->|pending| G["GET /confirm\nwith the token stored at creation"]
    G --> H{Status returned\nby confirm?}
    H -->|completed| I["Confirm the order\nUpdate the DB"]
    H -->|notcompleted| J["Cancel the order\nUpdate the DB"]
    H -->|pending| K["Do nothing\nWait for the next callback"]
```

## What not to do

```javascript theme={null}
// DANGEROUS — direct payload processing without verification
app.post('/callback', (req, res) => {
  const { status, amount, external_id } = req.body;

  if (status === 'completed') {
    // Anyone can send this payload
    markOrderAsPaid(external_id, amount);
  }
});
```

```javascript theme={null}
// CORRECT — systematic re-verification
app.post('/callback', async (req, res) => {
  const entry = req.body.custom_data?.find(
    (item) => item.keyof_customdata === 'transaction_id'
  );
  const transactionId = entry?.valueof_customdata;
  if (!transactionId) return res.sendStatus(400);

  const stored = await db.transactions.findOne({ transaction_id: transactionId });
  if (!stored) return res.sendStatus(404);

  const result = await confirmWithLigdicash(stored.token);

  if (result.status === 'completed') {
    await markOrderAsPaid(transactionId);
  }

  res.sendStatus(200);
});
```

## Related pages

* [Payin payload](/en/payment-api/callback/payin-payload)
* [Parse custom\_data](/en/payment-api/callback/parse-custom-data)
* [Framework examples](/en/payment-api/callback/framework-examples)
