Idempotent Requests

Learn how to use idempotency keys to safely retry API requests without duplicating operations.

What is Idempotency?

Idempotency is the property of an API operation where making the same request multiple times produces the same result as making it once. This is critical for payment processing and order management, where duplicate operations can result in double charges or duplicate orders.

DEUNA APIs support idempotency, allowing you to safely retry requests without concern for accidentally generating the same operation twice.

πŸ“˜

Idempotency is entirely optional but strongly recommended for any operation that creates or modifies resources (e.g., payments, orders, refunds).


How It Works

When you send a request with an idempotency key:

  1. First request β€” DEUNA processes the request normally and stores the response (status code + payload) associated with your idempotency key.
  2. Subsequent requests with the same key β€” DEUNA returns the stored response from the original request without reprocessing it.
  3. Different key β€” DEUNA treats it as a new, independent request.
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Client   │────────▢│   DEUNA API   │────────▢│   Provider   β”‚
β”‚           β”‚         β”‚               β”‚         β”‚              β”‚
β”‚  Request  β”‚         β”‚  Check key:   β”‚         β”‚  Process     β”‚
β”‚  + Key    β”‚         β”‚  New? Process β”‚         β”‚  payment     β”‚
β”‚           β”‚         β”‚  Exists? Returnβ”‚        β”‚              β”‚
β”‚           │◀────────│  cached resp  │◀────────│  Response    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Supported HTTP Methods

Idempotency keys are supported for the following HTTP methods:

HTTP MethodIdempotency SupportNotes
POSTβœ… SupportedRequired for payment creation, order creation, refunds
GETβšͺ Not neededGET requests are naturally idempotent
PUTβšͺ Not neededPUT requests are naturally idempotent
PATCHβšͺ Not neededPATCH requests are naturally idempotent
DELETEβšͺ Not neededDELETE requests are naturally idempotent
πŸ“˜

Idempotency keys are primarily used with POST requests, since these are the ones that create new resources.


Implementation Guide

Step 1: Generate an Idempotency Key

Generate a unique key for each distinct operation. We recommend using UUID v4 format.

// JavaScript β€” Generate a UUID v4 idempotency key
const { v4: uuidv4 } = require('uuid');

const idempotencyKey = uuidv4();
// Example output: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
# Python β€” Generate a UUID v4 idempotency key
import uuid

idempotency_key = str(uuid.uuid4())
# Example output: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
⚠️

Important: Each unique business operation must have its own idempotency key. Do NOT reuse keys across different operations.

Step 2: Include the Header in Your Request

Add the X-Idempotency-Key header to your API request.

curl -X POST https://api.deuna.io/v1/orders \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: f47ac10b-58cc-4372-a567-0e02b2c3d479" \
  -d '{
    "order": {
      "order_id": "order-12345",
      "currency": "USD",
      "items_total_amount": 5000,
      "total_amount": 5000
    }
  }'
// JavaScript β€” POST request with idempotency key using fetch
const idempotencyKey = uuidv4();

const response = await fetch('https://api.deuna.io/v1/orders', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'X-Idempotency-Key': idempotencyKey,
  },
  body: JSON.stringify({
    order: {
      order_id: 'order-12345',
      currency: 'USD',
      items_total_amount: 5000,
      total_amount: 5000,
    },
  }),
});

const data = await response.json();
console.log(data);
# Python β€” POST request with idempotency key using requests
import requests
import uuid

idempotency_key = str(uuid.uuid4())

response = requests.post(
    'https://api.deuna.io/v1/orders',
    headers={
        'Authorization': 'Bearer YOUR_API_KEY',
        'Content-Type': 'application/json',
        'X-Idempotency-Key': idempotency_key,
    },
    json={
        'order': {
            'order_id': 'order-12345',
            'currency': 'USD',
            'items_total_amount': 5000,
            'total_amount': 5000,
        }
    }
)

print(response.json())

Step 3: Implement Retry Logic

When a request fails due to network issues or timeouts, retry with the same idempotency key to avoid duplicate operations.

// JavaScript β€” Retry logic with idempotency
async function makeIdempotentRequest(url, payload, maxRetries = 3) {
  const idempotencyKey = uuidv4(); // Generate ONCE per operation
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer YOUR_API_KEY',
          'Content-Type': 'application/json',
          'X-Idempotency-Key': idempotencyKey, // Same key on every retry
        },
        body: JSON.stringify(payload),
      });

      if (response.ok) {
        return await response.json();
      }

      // Don't retry on client errors (4xx) except 408, 429
      if (response.status >= 400 && response.status < 500 
          && response.status !== 408 && response.status !== 429) {
        throw new Error(`Client error: ${response.status}`);
      }

      console.log(`Attempt ${attempt} failed (${response.status}). Retrying...`);
    } catch (error) {
      if (attempt === maxRetries) throw error;
      console.log(`Attempt ${attempt} failed: ${error.message}. Retrying...`);
    }

    // Exponential backoff: 1s, 2s, 4s
    await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt - 1) * 1000));
  }
}
# Python β€” Retry logic with idempotency
import time
import uuid
import requests

def make_idempotent_request(url, payload, max_retries=3):
    idempotency_key = str(uuid.uuid4())  # Generate ONCE per operation

    for attempt in range(1, max_retries + 1):
        try:
            response = requests.post(
                url,
                headers={
                    'Authorization': 'Bearer YOUR_API_KEY',
                    'Content-Type': 'application/json',
                    'X-Idempotency-Key': idempotency_key,  # Same key on every retry
                },
                json=payload,
                timeout=30,
            )

            if response.ok:
                return response.json()

            # Don't retry on client errors (4xx) except 408, 429
            if 400 <= response.status_code < 500 \
               and response.status_code not in (408, 429):
                raise Exception(f"Client error: {response.status_code}")

            print(f"Attempt {attempt} failed ({response.status_code}). Retrying...")

        except requests.exceptions.RequestException as e:
            if attempt == max_retries:
                raise
            print(f"Attempt {attempt} failed: {e}. Retrying...")

        # Exponential backoff: 1s, 2s, 4s
        time.sleep(2 ** (attempt - 1))

Best Practices

βœ… Do's

  • Generate a unique key per operation β€” Use UUID v4 or a similar globally unique identifier.
  • Reuse the same key for retries β€” When retrying a failed request, always use the same idempotency key.
  • Store the key alongside the operation β€” Persist the key in your database so you can retry if needed.
  • Use exponential backoff for retries β€” Avoid overwhelming the API with rapid retry attempts.
  • Set a reasonable timeout β€” DEUNA's production timeout is 60 seconds.

❌ Don'ts

  • Don't reuse keys for different operations β€” Each new business operation (new payment, new order) must use a fresh key.
  • Don't use sequential or predictable keys β€” Sequential keys can lead to collisions. Always use random UUIDs.
  • Don't change the request body with the same key β€” Sending a different payload with the same key will result in an error.
  • Don't rely on idempotency as a substitute for proper error handling β€” Always implement proper error handling alongside idempotency.

Common Use Cases

Payment Processing

# Creating a payment β€” use idempotency to prevent double charges
curl -X POST https://api.deuna.io/v1/merchants/payments \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: pay-a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -d '{
    "token": "order_token_here",
    "payment_source": {
      "card_token": "card_token_here"
    }
  }'

Order Creation

# Creating an order β€” use idempotency to prevent duplicate orders
curl -X POST https://api.deuna.io/v1/orders \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ord-a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -d '{
    "order": {
      "order_id": "my-order-001",
      "currency": "USD",
      "items_total_amount": 10000,
      "total_amount": 10000
    }
  }'

Refunds

# Processing a refund β€” use idempotency to prevent double refunds
curl -X POST https://api.deuna.io/v1/merchants/transactions/{transaction_id}/refund \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: ref-a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -d '{
    "reason": "Customer requested refund",
    "amount": 5000
  }'

Error Handling

ScenarioBehaviorRecommended Action
First request succeedsResponse stored with keyStore response, proceed normally
Retry with same key + same bodyReturns cached responseUse the response as if it were fresh
Retry with same key + different bodyReturns 422 Unprocessable EntityGenerate a new key for the new operation
Key has expiredTreated as a new requestGenerate a new key and retry
Network timeoutNo response receivedRetry with the same key
Server error (5xx)Operation may or may not have completedRetry with the same key

Idempotency Key Lifecycle

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Created    │────▢│   Active    │────▢│   Expired   β”‚
β”‚             β”‚     β”‚             β”‚     β”‚             β”‚
β”‚ Key sent in β”‚     β”‚ Response is β”‚     β”‚ Key is no   β”‚
β”‚ first req   β”‚     β”‚ cached and  β”‚     β”‚ longer validβ”‚
β”‚             β”‚     β”‚ returned on β”‚     β”‚             β”‚
β”‚             β”‚     β”‚ retries     β”‚     β”‚             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • Created β€” The key is sent for the first time with a request.
  • Active β€” DEUNA has processed the request and cached the response. Any subsequent request with the same key returns the cached response.
  • Expired β€” After a period of time, the key expires and a new request with the same key would be treated as a new operation.
πŸ“˜

Tip: Always generate a new idempotency key for each distinct business operation, even if a previous key has expired.


Testing in Sandbox

You can test idempotent behavior in the sandbox environment:

  1. Send a request with an idempotency key to the sandbox URL:

    https://api.sandbox.deuna.io
  2. Send the same request again with the same key and verify you receive the exact same response.

  3. Send a request with a different key and verify a new resource is created.

  4. Change the request body with the same key and verify you receive an error.

πŸ“˜

Use the DEUNA Postman Collection for quick testing.


Next Steps

  • DEUNA API Overview β€” Learn about the full API architecture.
  • Response Codes β€” Understand API error codes and how to handle them.
  • Environments β€” Learn about Sandbox and Production environments.
  • Rate Limits β€” Understand API rate limiting policies.
  • Webhooks β€” Set up webhooks for asynchronous event notifications.