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

# Idempotency Keys - Safe Retries for Write Requests

> Use idempotency keys to retry Corsa API write requests safely without creating duplicate records.

Network timeouts, client crashes, and rate-limit retries can leave you unsure whether a write request actually completed. Retrying blindly risks creating duplicate clients, transactions, alerts, or cases. **Idempotency keys** let you retry the same write request safely: Corsa processes it once and replays the original response on any retry within the retention window.

The feature is fully backward compatible. Requests without an idempotency key behave exactly as they do today.

<Note>
  Idempotency keys are distinct from `referenceId`. See [Idempotency keys vs. `referenceId`](#idempotency-keys-vs-referenceid) below.
</Note>

## How It Works

Send an `Idempotency-Key` request header on a write request. Corsa scopes the key to your platform and the specific request, so one customer's key can never affect another, and the same key used for a different request is rejected.

1. **First request** with a new key is processed normally, and its result is stored.
2. **A retry** with the same key and the same request replays the stored response — with no duplicate side effects — and includes the `Idempotent-Replayed: true` response header.
3. **A retry after a server error (`5xx`)** is *not* replayed; Corsa attempts to process the request again.

<Info>
  Idempotency keys are currently supported on resource-creating `POST` requests. Sending the header on other verbs is accepted but has no effect.
</Info>

## Sending an Idempotency Key

Generate a unique string per intended write operation — a UUID (v4) is recommended — and reuse that same value on every retry of that operation.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.corsa.finance/v1/clients/individual" \
    -H "Authorization: Bearer <API_TOKEN>:<API_SECRET>" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: 9f8b2c1e-6a2d-4f3b-9c1a-2e5d7f0a1b3c" \
    -d '{ "referenceId": "cust-001", "firstName": "Ada", "lastName": "Lovelace" }'
  ```

  ```typescript Node.js theme={null}
  import { randomUUID } from 'node:crypto';
  import { CorsaClient } from '@corsa-labs/sdk';

  // One key per intended operation. Reuse it on every retry of THIS create.
  const idempotencyKey = randomUUID();

  const client = new CorsaClient({
    BASE: 'https://api.corsa.finance',
    HEADERS: {
      Authorization: `Bearer ${process.env.API_TOKEN}:${process.env.API_SECRET}`,
      'Idempotency-Key': idempotencyKey,
    },
  });

  const created = await client.clients.createIndividualClient({
    referenceId: 'cust-001',
    firstName: 'Ada',
    lastName: 'Lovelace',
  });
  ```

  ```python Python theme={null}
  import os
  import uuid

  from corsa_sdk import CorsaClient

  # One key per intended operation. Reuse it on every retry of THIS create.
  idempotency_key = str(uuid.uuid4())

  client = CorsaClient(
      base_url="https://api.corsa.finance",
      token=f"{os.environ['API_TOKEN']}:{os.environ['API_SECRET']}",
      headers={"Idempotency-Key": idempotency_key},
  )
  ```
</CodeGroup>

<Warning>
  A single client-wide header sends the **same** key on every request. That is only correct when the client issues one write. For most integrations, set the `Idempotency-Key` **per request** so each operation gets its own key.
</Warning>

### Key Requirements

| Requirement        | Value                                    |
| ------------------ | ---------------------------------------- |
| Header name        | `Idempotency-Key`                        |
| Recommended format | Unique string per operation, e.g. a UUID |
| Maximum length     | 255 characters                           |
| Scope              | Per platform + request path + key        |
| Retention          | 24 hours after completion                |

## Recognizing a Replayed Response

When Corsa replays a stored response, the body and status code are identical to the original, and the response carries an extra header:

```http theme={null}
HTTP/1.1 201 Created
Idempotent-Replayed: true
Content-Type: application/json
```

Check for `Idempotent-Replayed: true` when you need to distinguish a fresh write from a replayed one — for example, when reconciling logs after a retry storm.

## Handling Conflicts

A reused key that does not match the original completed request returns `409 Conflict`. There are two cases:

| Scenario                                            | Status         | Meaning                                           | What to do                                                                            |
| --------------------------------------------------- | -------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Same key, **different** request payload             | `409 Conflict` | The key was already used for a different request. | Use a new key for the new operation. Never reuse a key across different writes.       |
| Same key while the **original is still processing** | `409 Conflict` | Your first request is in flight.                  | Wait, then retry with the **same** key. The in-progress lock clears after 60 seconds. |

### Retry After a Timeout

The core use case: your request times out and you do not know whether it succeeded. Retry with the same key.

* If the first request **completed**, you get the original response back with `Idempotent-Replayed: true` — no duplicate created.
* If the first request is **still processing**, you get `409 Conflict` with an in-progress message; wait and retry with the same key.
* If the first request **failed with a `5xx`**, the retry is processed as a new attempt.

<CodeGroup>
  ```typescript Node.js theme={null}
  import { randomUUID } from 'node:crypto';
  import { CorsaClient, ApiError } from '@corsa-labs/sdk';

  const idempotencyKey = randomUUID();

  async function createWithRetry(attempt = 0): Promise<unknown> {
    const client = new CorsaClient({
      BASE: 'https://api.corsa.finance',
      HEADERS: {
        Authorization: `Bearer ${process.env.API_TOKEN}:${process.env.API_SECRET}`,
        'Idempotency-Key': idempotencyKey, // same key on every retry
      },
    });

    try {
      return await client.clients.createIndividualClient({
        referenceId: 'cust-001',
        firstName: 'Ada',
        lastName: 'Lovelace',
      });
    } catch (err) {
      // 409 while the original is still processing: back off and retry the SAME key.
      if (err instanceof ApiError && err.status === 409 && attempt < 5) {
        await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
        return createWithRetry(attempt + 1);
      }
      throw err;
    }
  }
  ```

  ```python Python theme={null}
  import os
  import time
  import uuid

  from corsa_sdk import CorsaClient
  from corsa_sdk.exceptions import ApiError

  idempotency_key = str(uuid.uuid4())


  def create_with_retry(max_attempts: int = 5):
      client = CorsaClient(
          base_url="https://api.corsa.finance",
          token=f"{os.environ['API_TOKEN']}:{os.environ['API_SECRET']}",
          headers={"Idempotency-Key": idempotency_key},  # same key on every retry
      )

      for attempt in range(max_attempts):
          try:
              return client.clients.create_individual_client(
                  reference_id="cust-001",
                  first_name="Ada",
                  last_name="Lovelace",
              )
          except ApiError as err:
              # 409 while the original is still processing: back off, retry SAME key.
              if err.status == 409 and attempt < max_attempts - 1:
                  time.sleep(2 ** attempt * 0.5)
                  continue
              raise
  ```
</CodeGroup>

## Retention and Expiry

| State                                         | Retained for |
| --------------------------------------------- | ------------ |
| Completed request (success or customer `4xx`) | 24 hours     |
| In-progress lock                              | 60 seconds   |
| Server error (`5xx`)                          | Not stored   |

After 24 hours, Corsa may treat the same key as a brand-new request. If you need retry safety beyond that window, generate a fresh key and treat the operation as new.

## Idempotency Keys vs. `referenceId`

These solve different problems and are often used together.

|            | `Idempotency-Key`                                              | `referenceId`                                        |
| ---------- | -------------------------------------------------------------- | ---------------------------------------------------- |
| Purpose    | Retry safety for a single API call                             | Your source-system identifier for an entity          |
| Lifetime   | 24-hour retry window                                           | Permanent mapping                                    |
| Uniqueness | Per intended operation (e.g. a UUID)                           | Per entity in your system                            |
| On reuse   | Replays the original response, or `409` if the request differs | Enables upsert / entity mapping (endpoint-dependent) |
| Scope      | Any supported write request                                    | Entities that support `referenceId`                  |

Use an `Idempotency-Key` to make a **retry** safe. Use `referenceId` to map a Corsa entity back to a record in **your** system. During imports and backfills, set both: `referenceId` identifies the entity, and a per-row `Idempotency-Key` makes each write retry-safe.

## Error Reference

| Scenario                              | Status                   | Behavior                                                             |
| ------------------------------------- | ------------------------ | -------------------------------------------------------------------- |
| First request with a new key          | Original endpoint status | Processed normally; result stored.                                   |
| Same key, same completed request      | Original endpoint status | Original status/body returned with `Idempotent-Replayed: true`.      |
| Same key, different request           | `409 Conflict`           | Rejected with a key-conflict message; no side effect.                |
| Same key while original is processing | `409 Conflict`           | Rejected with an in-progress message; retry later with the same key. |
| Key longer than 255 characters        | `400 Bad Request`        | Rejected before processing.                                          |
| Original request returned `5xx`       | `5xx`                    | Not cached; a retry is processed again.                              |
| Same key after 24 hours               | Original endpoint status | Treated as a new request.                                            |
