# Idempotent requests

> Retry safely with idempotency keys.

Networks fail mid-flight. A timeout, a dropped connection, or an over-eager retry can leave you unsure whether a `POST /payments` actually charged a customer. Idempotency keys let you retry any mutating request safely: VINR remembers the first response and replays it instead of performing the operation twice.

## When to use idempotency keys

Send an idempotency key on every `POST` that creates or mutates a resource — payments, refunds, subscriptions, redemptions, payouts. `GET`, `PUT`, and `DELETE` requests are already idempotent by definition and ignore the header.

The SDK generates a key automatically for create calls, so the safe path is the default path. Supply your own key when you want a single business action (one checkout, one order) to map to exactly one VINR resource even across process restarts and retries.

## Idempotency-Key header

Pass a unique value (we recommend a UUID v4) in the `Idempotency-Key` request header. Keys are scoped to your account and to the endpoint, and may be up to 255 characters.

##### SDK

```ts
import { Vinr } from '@vinr/sdk';
import { randomUUID } from 'node:crypto';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

// Derive a stable key from your own order so retries collapse to one payment.
const key = `order-${order.id}`; // or randomUUID()

const payment = await vinr.payments.create(
  {
    amount: 4500,
    currency: 'EUR',
    description: 'Order #1042',
    returnUrl: 'https://shop.example.com/return',
  },
  { idempotencyKey: key },
);

console.log(payment.id); // pay_... — identical on every retry
```

##### cURL

```bash
curl -X POST https://api.vinr.com/v1/payments \
  -H "X-Api-Key: $VINR_SECRET_KEY" \
  -H "Idempotency-Key: order-1042" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 4500,
    "currency": "EUR",
    "description": "Order #1042",
    "returnUrl": "https://shop.example.com/return"
  }'
```

> Use one key per logical action, not per HTTP attempt. Generate the key once before the first request and reuse the same value for every retry of that action.

## Behavior on replay

The first request for a given key executes normally. VINR stores the resulting status code and response body keyed by the request. Any subsequent request that presents the same key returns the stored response without re-running the operation — so a network retry never produces a second charge.

Replayed responses carry an `Idempotent-Replayed: true` header so you can distinguish a fresh result from a cached one in logs.

### First request

`POST /payments` with `Idempotency-Key: order-1042` creates `pay_3Nf...` and returns `201 Created`. VINR records the key, the request fingerprint, and the response.

### Retry after a timeout

Your client never saw the first response and retries with the same key. VINR recognizes the key, skips creation, and replays the original `201` body with `Idempotent-Replayed: true`. No second payment exists.

### Concurrent retries

If a second request with the same key arrives while the first is still in flight, VINR returns `409 Conflict` with code `idempotency_in_progress`. Back off and retry; the completed result will be replayed once the original finishes.

## Key lifetime

VINR retains idempotency keys for **24 hours** after the first request. Within that window, reusing a key replays the stored response. After it expires the key is forgotten, and reusing it will execute the operation again as if new.

| Field       | Type       | Description                                | Default    |
| ----------- | ---------- | ------------------------------------------ | ---------- |
| `retention` | `duration` | How long a key and its response are cached | `24 hours` |
| `scope`     | `string`   | Keys are unique per account + endpoint     | `—`        |

> Choose keys that you do not need to reuse within 24 hours for a *different* payload. If a key from yesterday's order collides with today's request body, you will get a `409 idempotency_key_reuse` error (see below).

## Conflicts

VINR fingerprints the request body the first time it sees a key. If the same key arrives later with a **different** payload, the request is rejected — this protects you from accidentally charging a new amount under a stale key.

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "idempotency_key_reuse",
    "message": "This Idempotency-Key was already used with a different request body.",
    "status": 409
  }
}
```

| Code                       | Status | Meaning                                     | What to do                                               |
| -------------------------- | ------ | ------------------------------------------- | -------------------------------------------------------- |
| `idempotency_in_progress`  | 409    | A request with this key is still processing | Back off and retry; the original result will be replayed |
| `idempotency_key_reuse`    | 409    | Key reused with a different request body    | Use a fresh key for the new action                       |
| `idempotency_key_too_long` | 400    | Key exceeds 255 characters                  | Shorten the key (use a UUID or hashed order id)          |

When you receive `idempotency_in_progress`, retry with exponential backoff (for example 1s, 2s, 4s) using the **same** key. Never switch keys to escape the conflict — that is exactly how duplicate charges happen.

## Next steps

[API Reference](/docs/api-reference) — Base URLs, authentication, resources, and error codes.

[Errors & retries](/docs/troubleshooting) — Error shapes, HTTP status codes, and safe retry strategies.

[Webhooks](/docs/integration) — Verify and de-duplicate event deliveries alongside idempotent writes.
