> This page is for version V1 (default).
> For other versions, use one of these documentation indexes:
> - V1 (default): https://docs.ottimate.com/v-1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.ottimate.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.ottimate.com/_mcp/server.

# Idempotency

Network failures, timeouts, and worker restarts make retries unavoidable. The `Idempotency-Key` header lets you retry a write request without risking duplicate side effects: the first successful response is cached for 24 hours, and any subsequent request with the same key is replayed verbatim from cache.

## When to Use

Send `Idempotency-Key` on **every state-changing request** where a duplicate would be problematic — creating invoices, posting payments, bulk imports, etc.

The header is optional. If omitted, the request is processed normally without idempotency protection.

## How It Works

1. The client generates a unique key (UUID/ULID recommended) and sends it in the `Idempotency-Key` header.
2. On the **first** request with that key, the server processes normally and caches the 2xx response.
3. On a **retry** with the same key:
   * **`Content-Type: application/json` (JSON tier)** — body is compared using a SHA-256 hash:
     * Same body → cached response returned with `X-Idempotent-Replayed: true`.
     * Different body → `422 Unprocessable Entity` (catches accidental key reuse with mutated payload).
   * **All other content types** (e.g. multipart file uploads) — key-only match: a retry with the same key returns `422 Unprocessable Entity`, since the binary body cannot be hashed to confirm it matches the original. Use a new key for a new request.
4. If a retry arrives while the first request is still being processed → `409 Conflict`. Retry after the original completes.

## Example

```bash
curl -X POST https://api.ottimate.com/v1/invoices \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{ "vendor_id": "...", "amount": 1234.56 }'
```

If the same request is retried with the same `Idempotency-Key` and body, the response is identical, no new invoice is created, and the response includes:

```
X-Idempotent-Replayed: true
```

## Key Format

| Constraint         | Value                                             |
| ------------------ | ------------------------------------------------- |
| Length             | 1–128 characters                                  |
| Allowed characters | `A–Z`, `a–z`, `0–9`, `.`, `_`, `-`, `+`, `=`, `/` |
| Recommended        | UUIDv4 or ULID                                    |

Invalid keys are rejected with `400 Bad Request`.

## Scope and Lifetime

| Attribute        | Behavior                                                                                |
| ---------------- | --------------------------------------------------------------------------------------- |
| Cache lifetime   | **24 hours** from the first response                                                    |
| Scope            | Per **API key + HTTP method + path + idempotency key**                                  |
| Cached responses | Only 2xx responses are cached. 4xx/5xx responses are not — retries are processed fresh. |
| API key rotation | Rotating your API key invalidates all cached entries scoped to the old key.             |

> **Note**
>
> Because scope includes the path and method, the same `Idempotency-Key` value can be safely reused across different endpoints — they will not collide.

## Response Codes

| Status                                  | Meaning                                                                                                                                                                       |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **2xx** + `X-Idempotent-Replayed: true` | Replay of a cached response. Side effects already happened on the original request.                                                                                           |
| **208 Already Reported**                | The original response was too large to cache (>64 KB) or was streamed. The original operation **did succeed** — fetch the current state via the corresponding `GET` endpoint. |
| **400 Bad Request**                     | Malformed `Idempotency-Key` header (length / charset).                                                                                                                        |
| **409 Conflict**                        | A concurrent request with the same key is still being processed. Wait and retry.                                                                                              |
| **422 Unprocessable Entity**            | Same key was previously used with a different request body (JSON requests only).                                                                                              |

## Applicability

`Idempotency-Key` takes effect on most **`POST`** requests, but not all of them. Every endpoint that supports it lists `Idempotency-Key` in its parameter table, so treat the per-endpoint API reference as authoritative rather than assuming all `POST` endpoints are covered.

The header is ignored (no caching) for:

* Non-`POST` methods (`GET`, `PATCH`, `PUT`, `DELETE` — currently)
* Requests without an API key

Each of the five handles a repeat differently, and the status code alone does not tell you whether a request was the first or a repeat. Check the response body:

| Endpoint                            | What a second identical request does                                                                                                                                        | Tells you apart from the first? |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| `POST /v1/invoices/mark-exported`   | Returns `200` with the invoice under `updated`, the same body as the first request. The invoice stays exported.                                                             | No                              |
| `POST /v1/invoices/{id}/flag`       | Returns `200` with the same success payload. An already-flagged invoice is left as it is and no second history entry is recorded.                                           | No                              |
| `POST /v1/invoices/{id}/approve`    | Returns `200` carrying the **original** `approved_date` and `client_invoice_state: "pending_export"`. The invoice keeps a single approval and one `APPROVED` history entry. | No                              |
| `POST /v1/invoices/mark-unexported` | Returns `200`, but the invoice appears under `skipped` rather than `updated`, with the reason `Invoice is not in exported state.`                                           | Yes, from the body              |
| `POST /v1/invoices/{id}/approvers`  | Returns `400`, with `message.err` set to `Approver is already added to the Invoice.` The approver added by the first request is unchanged.                                  | Yes                             |

Repeating any of these five is safe in the sense that none of them creates a duplicate record — `mark-exported`, `flag` and `approve` are unaffected by a repeat, while `mark-unexported` and `approvers` report that the work was already done.

> **Note**
>
> For multipart file uploads and other non-JSON content types, the server cannot verify the request body without loading the full upload into memory. A retry with the same key always returns `422` — use a new key for each new upload.