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

# Idempotency and deduplication

> LigdiCash sends two POST requests per event. How to ensure your processing runs only once.

For every transaction event, LigdiCash sends **two POST requests** to your `callback_url`:

* One in `application/x-www-form-urlencoded`
* One in `application/json`

Both contain exactly the same data. Your server will therefore receive two calls for a single event. Without deduplication, you risk processing the same transaction twice — fulfilling an order twice, triggering two refunds, crediting an account twice.

## Deduplication strategy

The simplest method is to mark each processed transaction in your database and ignore subsequent calls for the same transaction.

<Steps>
  <Step title="Extract your transaction_id">
    Parse `custom_data` to extract your `transaction_id` via `keyof_customdata`. See [Parse custom\_data](/en/payment-api/callback/parse-custom-data).
  </Step>

  <Step title="Check whether it has already been processed">
    Before any processing, check whether this identifier already exists in your processed transactions table.
  </Step>

  <Step title="Process and mark atomically">
    If not yet processed, run your processing and mark the transaction as processed in the same database transaction. Atomicity avoids race conditions if the two requests arrive simultaneously.
  </Step>
</Steps>

## Implementation

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

  // Insert with unique constraint — fails if already processed
  const inserted = await db.query(
    'INSERT INTO processed_callbacks (transaction_id) VALUES (?) ON CONFLICT DO NOTHING',
    [transactionId]
  );

  if (inserted.rowCount === 0) {
    // Already processed — second request of the LigdiCash duplicate
    return res.sendStatus(200);
  }

  // First reception — business processing
  await processPayment(transactionId);

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

```php theme={null}
// With a UNIQUE constraint on transaction_id in the database
try {
    $db->insert('processed_callbacks', ['transaction_id' => $transactionId]);
} catch (UniqueConstraintException $e) {
    // Already processed
    http_response_code(200);
    exit;
}

// First reception — business processing
processPayment($transactionId);
```

<Note>
  A database unique constraint is more reliable than a simple `SELECT` followed by an `INSERT` — two simultaneous requests can both pass the `SELECT` before either has performed its `INSERT`.
</Note>

## Related pages

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