Idempotent requests

Retry safely with idempotency keys.

View as Markdown

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 keysAsk

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 headerAsk

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.

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 -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 replayAsk

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 lifetimeAsk

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.

Prop

Type

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).

ConflictsAsk

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.

{
  "error": {
    "type": "invalid_request_error",
    "code": "idempotency_key_reuse",
    "message": "This Idempotency-Key was already used with a different request body.",
    "status": 409
  }
}
CodeStatusMeaningWhat to do
idempotency_in_progress409A request with this key is still processingBack off and retry; the original result will be replayed
idempotency_key_reuse409Key reused with a different request bodyUse a fresh key for the new action
idempotency_key_too_long400Key exceeds 255 charactersShorten 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 stepsAsk

Was this page helpful?