Skip to main content

Overview

Network errors, timeouts, and client crashes can leave you unsure whether a request succeeded. Idempotency keys let you safely retry any POST request. If the API has already processed a request with the same key, it returns the cached response instead of executing the operation again.

The Idempotency-Key header

Include an Idempotency-Key header on every POST request. The key is a string of up to 255 characters. UUIDs are recommended.
curl -X POST https://api.leanrails.com/v1/payment_intents \
  -u "$API_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 019532a1-7e2b-4e6a-b8d0-1c3f5a9e7b2d" \
  -d '{
    "amount": 5000,
    "currency": "usd",
    "payment_method": "pm_card_visa"
  }'
GET and DELETE requests are inherently idempotent and do not require the header.

How caching works

When the API receives a POST request with an Idempotency-Key:
  1. It checks whether a response for that key already exists in the cache.
  2. If yes, it returns the cached response with a 200 status (or the original error status) without re-executing the operation.
  3. If no, it processes the request normally and caches the response.
Cached responses are stored for 24 hours, after which the key can be reused.

Same key, different parameters

If you send a request with the same idempotency key but different request body parameters, the API rejects it with a 422 status:
# First request - succeeds
curl -X POST https://api.leanrails.com/v1/payment_intents \
  -u "$API_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: my-unique-key-123" \
  -d '{"amount": 5000, "currency": "usd"}'

# Second request with SAME key but DIFFERENT amount - rejected
curl -X POST https://api.leanrails.com/v1/payment_intents \
  -u "$API_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: my-unique-key-123" \
  -d '{"amount": 9999, "currency": "usd"}'
Error response (422):
{
  "error": {
    "type": "invalid_request_error",
    "code": "idempotency_key_reuse",
    "message": "This idempotency key has already been used with different request parameters.",
    "param": null,
    "doc_url": "https://docs.leanrails.com/errors/idempotency_key_reuse"
  }
}

Best practices for generating keys

UUIDs provide sufficient entropy to avoid collisions and are supported natively in most languages.
// Node.js
const key = crypto.randomUUID();
// => "019532a1-7e2b-4e6a-b8d0-1c3f5a9e7b2d"
# bash / zsh
uuidgen
For some operations, it makes sense to derive the key from a business identifier to ensure a specific operation only happens once.
// Ensure a specific order is only charged once
const key = `charge-order-${orderId}`;
Persist the idempotency key alongside the operation in your database before making the API call. If your process crashes, you can retry with the same key.

Retry strategy with idempotency

A safe retry loop using idempotency keys in JavaScript:
const fetch = require("node-fetch");

async function createPaymentIntent(params, maxRetries = 3) {
  const API_KEY = process.env.API_KEY;
  const credentials = Buffer.from(`${API_KEY}:`).toString("base64");
  const idempotencyKey = crypto.randomUUID();

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch("https://api.leanrails.com/v1/payment_intents", {
        method: "POST",
        headers: {
          Authorization: `Basic ${credentials}`,
          "Content-Type": "application/json",
          "Idempotency-Key": idempotencyKey,
        },
        body: JSON.stringify(params),
      });

      if (response.status === 429 || response.status >= 500) {
        // Retryable — wait with exponential backoff
        await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
        continue;
      }

      return await response.json();
    } catch (err) {
      if (attempt === maxRetries) throw err;
      await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
    }
  }
}

Key constraints

PropertyValue
Maximum length255 characters
Cache duration24 hours
Required onAll POST requests
Reuse with different paramsRejected with 422