> ## Documentation Index
> Fetch the complete documentation index at: https://docs.c2cpayment.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Retry order creation safely. The same Idempotency-Key never creates a second deposit or withdrawal.

## Overview

Network failures happen: a request can time out after C2C has already created the order. The `Idempotency-Key` header makes retries safe. If C2C has already created an order for a key, it returns that order instead of creating a new one.

`POST /api/merchant/orders` (deposits and withdrawals) **requires** an `Idempotency-Key`. Requests without one are rejected with `400`.

```http theme={null}
POST /api/merchant/orders HTTP/1.1
C2C-API-Key: mk_live_...
Idempotency-Key: INV-10045
Content-Type: application/json

{"orderType":"Deposit","amount":1500, ... ,"signature":"..."}
```

## How it works

| Situation | Result |
| - | - |
| New key, order created | `200` with the new order. The key is now bound to that order. |
| Same key again with the **same terms** (same API key) | `200` with the **original** order: same `orderId` and `orderReference`, and the order's **current** `status` and `stage`. No new order is created. |
| Same key again with **different terms** (same API key) | `422` `This Idempotency-Key was already used for a different order request.` No order is created or returned. |
| Same key, but the first request was **rejected** (`400`, `401`, `403`, `409`, `413`, `422`) | Treated as a new key. Rejected requests don't consume the key. |
| Same signed body with a **different** key (same API key, within 24 hours) | `401 SIGNATURE_REPLAYED`. A body's signature is bound to the first key it was accepted with. See [Replay protection](/api-reference/signature-guide#replay-protection). |
| Same key used with a **different** API key | Treated as a new key. Keys are scoped per API key. |

The **terms** compared are `orderType`, `amount`, `walletType`, `currency`, `merchantOrderRef`, `customerAccountNumber` and `customerAccountName`. Other fields (for example `callbackUrl` or `metadata`) aren't compared.

<Warning>
  Never reuse a key for a different order. A retry must send the same terms as the original request, or it's rejected with `422`. Keys created before this check was introduced aren't checked: reusing one of those returns the original order whatever the terms.
</Warning>

<Note>
  The `Idempotency-Key` protects against duplicate *requests*. Your `merchantOrderRef` also protects against duplicate *payments*: only one order that is in progress or completed can use a reference, and a second one is rejected with `409`. See [`merchantOrderRef` rules](/guides/order-status#merchantorderref-rules).
</Note>

## Key rules

* **Format:** any string up to **100 characters**. Use **one key per order**, for example your own order ID or a UUID that you store with your order, and reuse it for every attempt at that order.
* **Lifetime:** keys don't expire. A key stays bound to its order permanently.
* **Signature:** retry with the same body, the same `signature` and the same `Idempotency-Key`. There's nothing to re-sign. Sending the same signed body with a **new** key is rejected as `SIGNATURE_REPLAYED`, even if the first attempt was rejected, so don't switch keys between attempts at the same order.
* **Concurrent duplicates:** if several requests with the same key arrive at the same time, only one order is created and every request gets that order back.

## Safe retry pattern

<CodeGroup>
  ```javascript Node.js theme={null}
  // signBody(): see the Signature Generation Guide
  async function createOrderWithRetry(order, idempotencyKey, maxAttempts = 4) {
    // Sign once: every attempt sends the same body and the same key
    const body = JSON.stringify({ ...order, signature: signBody(order, process.env.C2C_SIGNING_SECRET) });

    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      let response;
      try {
        response = await fetch(`${BASE_URL}/api/merchant/orders`, {
          method: 'POST',
          headers: {
            'C2C-API-Key': process.env.C2C_API_KEY,
            'Content-Type': 'application/json',
            'Idempotency-Key': idempotencyKey,   // same key on every attempt
          },
          body,
          signal: AbortSignal.timeout(15000),
        });
      } catch {
        response = null; // network error or timeout → the order may or may not exist; retrying is safe
      }

      if (response?.ok) return (await response.json()).data;
      if (response && response.status < 500) {
        throw new Error(`Not retryable: HTTP ${response.status} ${await response.text()}`);
      }
      await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
    }
    throw new Error('Order creation still failing after retries. Retry later with the same Idempotency-Key.');
  }
  ```

  ```python Python theme={null}
  import os, time, requests

  # sign_body(): see the Signature Generation Guide
  def create_order_with_retry(order: dict, idempotency_key: str, max_attempts: int = 4) -> dict:
      # Sign once: every attempt sends the same body and the same key
      body = {**order, "signature": sign_body(order, os.environ["C2C_SIGNING_SECRET"])}
      for attempt in range(1, max_attempts + 1):
          try:
              response = requests.post(
                  f"{BASE_URL}/api/merchant/orders",
                  json=body,
                  headers={
                      "C2C-API-Key": os.environ["C2C_API_KEY"],
                      "Idempotency-Key": idempotency_key,   # same key on every attempt
                  },
                  timeout=15,
              )
          except requests.RequestException:
              response = None

          if response is not None and response.ok:
              return response.json()["data"]
          if response is not None and response.status_code < 500:
              raise RuntimeError(f"Not retryable: HTTP {response.status_code} {response.text}")
          time.sleep(2 ** attempt)
      raise RuntimeError("Still failing; retry later with the same Idempotency-Key.")
  ```
</CodeGroup>

<Tip>
  Persist the `Idempotency-Key` with your own order record **before** the first attempt. If your process crashes mid-request, you can resume with the same key and never create a duplicate deposit or payout.
</Tip>

## Related

<CardGroup cols={2}>
  <Card title="Errors" icon="triangle-exclamation" href="/api-reference/errors">
    Which errors are retryable
  </Card>

  <Card title="Signature Generation Guide" icon="signature" href="/api-reference/signature-guide#replay-protection">
    How retries and replays are told apart
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.