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

# Idempotency

> Make POST requests safe to retry using the Idempotency-Key header.

Network failures happen. A `POST /v1/transactions` request that times out at your end might have succeeded on our side. Without idempotency, retrying would create a duplicate. The `Idempotency-Key` header solves this.

## How it works

Send an `Idempotency-Key` header on any mutating request (`POST`, `PATCH`, `DELETE`). It is **required** on `POST` - every create and every RPC-style action endpoint (`.../reverse`, `.../deliver`, `.../rotate_secret`, and so on). It is **optional but recommended** on `PATCH` and `DELETE`: omit it and the request just runs without dedup or replay protection; send it and you get the identical claim/replay/conflict guarantees `POST` gets. The value is a stable opaque string, 8 to 255 characters.

* **First request:** we process normally and cache the response keyed by `(api key, method, path, idempotency key)`
* **Retry with same key AND same request (body and query string):** we replay the cached response, with no duplicate side effect. Response header `Idempotent-Replayed: true`
* **Retry with same key but a different body or query string:** we return `409` with `type: "idempotency_error"`, `code: "idempotency_key_conflict"` (safety net against bugs). Use a new key to retry safely.
* **Retry while the first request is still in flight:** we return `409` with `code: "idempotency_key_in_use"`, so retry shortly.

Cached responses live for **24 hours**. If the first attempt dies without finishing, a retry with the same key runs again after about 90 seconds.

This is the safety guarantee behind the header. A retry with the same key returns the original result whenever the first attempt finished with a `2xx` or `4xx`. If the first attempt ended in a `5xx`, or the connection broke before it finished, the retry runs again, and a side effect that already happened (for example a delivery that was sent) can happen a second time.

```mermaid theme={null}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E5F6FF','primaryBorderColor':'#36C6FA','primaryTextColor':'#0B3A4A','lineColor':'#2BA8D9','secondaryColor':'#F4FCFF','tertiaryColor':'#FFFFFF','fontFamily':'Manrope, ui-sans-serif, system-ui, sans-serif'}}}%%
sequenceDiagram
    participant App as Your app
    participant API as VEXiON API

    Note over App,API: Attempt 1, new key
    App->>API: POST /v1/transactions<br/>Idempotency-Key: pos_order_00427
    activate API
    Note right of API: No cached response for this key.<br/>Executes the write, caches the result.
    API-->>App: 201 Created<br/>(no Idempotent-Replayed header)
    deactivate API

    Note over App: Network times out before<br/>the response arrives, app retries

    rect rgba(54, 198, 250, 0.12)
    Note over App,API: Attempt 2, same key, same request

    App->>API: POST /v1/transactions<br/>Idempotency-Key: pos_order_00427
    activate API
    Note right of API: Cached response found for this key.<br/>Skips execution entirely.
    API-->>App: 201 Created<br/>Idempotent-Replayed: true
    deactivate API
    end

    Note over App,API: Result: transaction created once,<br/>even though the app sent two requests
```

> [!NOTE]
>
> Attempt 2 never re-runs your write. The API recognizes the key, hands back the exact response it cached from attempt 1, and stamps it `Idempotent-Replayed: true` so your app can tell the two apart in logs even though the body is identical.

> [card] New key
>
> No cached response exists yet. Executes the write, caches the result, returns it fresh.

> [card] Same key, same request
>
> Cached response found and the request (body and query string) matches. Replays it. `Idempotent-Replayed: true`, no re-execution.

> [card] Same key, different request
>
> Cached response found but the body or query string doesn't match. `409 idempotency_key_conflict` - use a new key.

> [card] Same key, still in flight
>
> An earlier request with this key hasn't finished yet. `409 idempotency_key_in_use` - retry shortly.

For the exact codes and messages behind the two `409` cases, see the [Errors reference](https://vexioncards.mintlify.site/api-reference/errors.md).

> [!NOTE]
>
> **Exception: `POST /v1/notifications`.** The 1 per minute broadcast limiter runs before idempotency on this endpoint. A retry inside that window returns `429 rate_limited_per_minute`, not a cached replay. Wait for `Retry-After`, then retry with the same key.

A `5xx` response is not cached: the claim is released, so the same key can be retried and runs fresh. Only `2xx` and `4xx` responses are replayed.

## Which endpoints require it

Every `POST` endpoint requires `Idempotency-Key` - creates and RPC-style actions alike. Omitting it returns `400` with `type: "invalid_request_error"`, `code: "idempotency_key_required"`.

`PATCH` and `DELETE` endpoints accept the header but do not require it: omitting it just means that particular call runs without dedup or replay protection, not an error. We recommend sending it on those too for a fully deterministic retry story, especially for `DELETE` calls you might retry after a network timeout.

Read-only endpoints (`GET`) ignore it.

## Key format

```
Idempotency-Key: <opaque-string>
```

* 8 to 255 characters
* Any characters allowed, though we recommend URL-safe: `A-Z a-z 0-9 - _ .`
* Must be unique per logical operation

## Best practices

**Use a stable value per logical operation, not per retry.** If you're recording a POS purchase, the natural idempotency key is the POS order ID:

```
Idempotency-Key: pos_order_2026_07_01_00427
```

That way, whether your retry loop fires once or ten times, the transaction is created exactly once.

**Never generate a new UUID per attempt**, because that defeats the entire purpose. Retries would each be seen as a new request.

## Example

```javascript Node.js theme={null}
async function recordTransaction(orderId, body, attempt = 1) {
  try {
    const res = await fetch('https://api.vexioncards.one/v1/transactions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.VXN_API_KEY}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': `pos_order_${orderId}`,  // ← stable per logical op
      },
      body: JSON.stringify(body),
    });

    if (res.ok) return res.json();

    // on a 5xx, retry with the SAME key
    if (res.status >= 500 && attempt < 5) {
      await new Promise(r => setTimeout(r, 1000 * attempt));
      return recordTransaction(orderId, body, attempt + 1);
    }

    throw new Error(`HTTP ${res.status}`);
  } catch (err) {
    if (attempt < 5) {
      // Network error, retry with SAME key
      await new Promise(r => setTimeout(r, 1000 * attempt));
      return recordTransaction(orderId, body, attempt + 1);
    }
    throw err;
  }
}
```

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

def record_transaction(order_id, body, max_attempts=5):
    headers = {
        "Authorization": f"Bearer {os.environ['VXN_API_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": f"pos_order_{order_id}",  # stable per logical op
    }
    for attempt in range(1, max_attempts + 1):
        try:
            r = requests.post(
                "https://api.vexioncards.one/v1/transactions",
                headers=headers,
                json=body,
                timeout=10,
            )
            if r.ok:
                return r.json()
            if r.status_code < 500:
                r.raise_for_status()
        except requests.RequestException:
            if attempt == max_attempts:
                raise
        time.sleep(attempt)
```

## Verifying replay

The `Idempotent-Replayed: true` response header tells you the response came from cache instead of a fresh execution:

```http theme={null}
HTTP/1.1 201 Created
Idempotent-Replayed: true
Vexion-Request-Id: req_01jBhYqKsfXr8M2WVCdXyL9pZk

{ "id": "txn_...", ... }
```

Use this to distinguish "we recorded this now" from "we recorded this earlier and are replaying".

## What NOT to do

> [!WARNING]
>
> **Don't** vary the idempotency key per attempt. Duplicate transactions will result.
>
> **Don't** put user-controllable data in the key without hashing. Multiple users could collide on the same key.
>
> **Don't** use the same key for logically different operations. Reserve one key per logical write.


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