telnesstech

Idempotency

Retry a request with an idempotency key, and the operation still happens only once.

With the X-Idempotency-Key header you can retry a request without the risk of a duplicate operation. When a request carries an idempotency key, the operation happens exactly once, even when you send the request many times.

How it works

The first request. We do the operation and cache the whole response against your idempotency key. The cache holds the status code, the headers, and the body.

Every later request. When a request arrives with the same key, we answer with the cached response at once. We do not do the operation again.

Key requirements

One key per operation. Generate a new identifier for each distinct operation. Never reuse a key for a different operation.

The request fingerprint must not change. These parts of a retry must be identical to the first request:

  • The request method, such as POST or PUT.
  • The request URL, with its path and its query parameters.
  • The request body, byte for byte.
  • The request headers that change the result, such as Content-Type and Authorization.

A modified request gets rejected. If you send the same idempotency key with different request data, the API answers 409 Conflict. This rejection catches the mistake of a key reused for another operation.

Response behavior

ScenarioResponse
First request with keyNormal processing, response cached
Retry with identical requestCached response returned (same status, headers, body)
Concurrent identical requests409 Conflict with idempotency_key_locked (retry after brief delay)
Retry with modified request409 Conflict with idempotency_key_mismatch (do not retry)

Error handling guidance

A concurrent collision (idempotency_key_locked) means that another request with the same key is still in progress. This state is temporary. Wait 100 to 500 ms, then send the identical request again.

A request mismatch (idempotency_key_mismatch) means that the key already carried different request parameters. This is a fault in your code. Generate a new idempotency key for the new operation.

Expiration

An idempotency key expires 24 hours after its first use. After that, the same key starts a new operation.

Best practices

  • Generate the key in your own code, before you send the request.
  • Store the key with your request context, so a retry can carry the same key.
  • Use an idempotency key on every operation that is not idempotent by itself: POST and PATCH.
  • Generate the key from cryptographically random values. A timestamp and a sequential identifier are both predictable.

Example

# First request
curl -X POST "{BASE_URL}/orders" \
  -H "X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customerId": "cust_123", "items": [...]}'

# Response: 201 Created
# {"orderId": "1b11f175-f0e6-4b6c-8c4b-4eee806123a3", "status": "CONFIRMED", ...}

# Retry (network timeout, uncertain state)
# Use the same idempotency key and request data
curl -X POST "{BASE_URL}/orders" \
  -H "X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customerId": "cust_123", "items": [...]}'

# Response: 201 Created (identical to first request)
# {"orderId": "1b11f175-f0e6-4b6c-8c4b-4eee806123a3", "status": "CONFIRMED", ...}
# No duplicate order created
const idempotencyKey = '550e8400-e29b-41d4-a716-446655440000';
const orderData = {
  customerId: 'cust_123',
  items: [...]
};

// First request
const response = await fetch('{BASE_URL}/orders', {
  method: 'POST',
  headers: {
    'X-Idempotency-Key': idempotencyKey,
    'X-API-Key': process.env.API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(orderData)
});

const result = await response.json();
// Response: 201 Created
// {"orderId": "1b11f175-f0e6-4b6c-8c4b-4eee806123a3", "status": "CONFIRMED", ...}

// Retry (network timeout, uncertain state)
// Use the same idempotency key and request data
const retryResponse = await fetch('{BASE_URL}/orders', {
  method: 'POST',
  headers: {
    'X-Idempotency-Key': idempotencyKey, // Same key
    'X-API-Key': process.env.API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(orderData) // Same data
});

const retryResult = await retryResponse.json();
// Response: 201 Created (identical to first request)
// {"orderId": "1b11f175-f0e6-4b6c-8c4b-4eee806123a3", "status": "CONFIRMED", ...}
// No duplicate order created