Idempotency

Idempotency ensures that retrying a request multiple times has the same effect as making it once. This prevents accidental duplicate charges, transfers, or other critical operations.

What is Idempotency?

Idempotency keys allow you to safely retry API requests without worrying about performing the same operation twice. When you include an idempotency key with a request, Devdraft will return the same result for subsequent requests with the same key.
Idempotency flow diagram showing request deduplication

How It Works

1

Generate Unique Key

Create a unique idempotency key for each operation (recommended: UUID)
2

Include in Request

Add the key to your API request header as Idempotency-Key
3

Safe Retries

If the request fails due to network issues, retry with the same key
4

Consistent Results

Devdraft returns the same response for requests with the same key

Supported Endpoints

Idempotency is supported on all state-changing operations:

Payment Operations

  • Create payment intents
  • Process payments
  • Refund transactions
  • Cancel payments

Account Operations

  • Create customers
  • Update customer data
  • Create invoices
  • Generate payment links

Transfer Operations

  • Initiate transfers
  • Create withdrawals
  • Process deposits
  • Currency conversions

Product Operations

  • Create products
  • Update inventory
  • Process orders
  • Manage catalog

Implementation

// Generate a unique idempotency key
const idempotencyKey = crypto.randomUUID();

// Include in API request
const response = await fetch('https://api.devdraft.ai/v1/payments', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': idempotencyKey
  },
  body: JSON.stringify({
    amount: 1000,
    currency: 'USD',
    customer_id: 'cust_123'
  })
});

Best Practices

Creating Effective Keys:Do:
  • Use UUIDs or other globally unique identifiers
  • Generate new keys for each distinct operation
  • Include operation context if needed (e.g., order_123_payment)
Don’t:
  • Reuse keys across different operations
  • Use predictable sequences (1, 2, 3…)
  • Include sensitive data in keys
// UUID for each operation
const paymentKey = crypto.randomUUID();
const refundKey = crypto.randomUUID();

// Context-specific keys
const orderPaymentKey = `order_${orderId}_${Date.now()}`;
Retry Logic:
async function makeIdempotentRequest(data, maxRetries = 3) {
  const idempotencyKey = crypto.randomUUID();
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch('/api/payments', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Idempotency-Key': idempotencyKey
        },
        body: JSON.stringify(data)
      });
      
      if (response.ok) {
        return await response.json();
      }
      
      // Don't retry client errors (4xx)
      if (response.status >= 400 && response.status < 500) {
        throw new Error(`Client error: ${response.status}`);
      }
      
    } catch (error) {
      if (attempt === maxRetries) {
        throw error;
      }
      
      // Exponential backoff
      await new Promise(resolve => 
        setTimeout(resolve, Math.pow(2, attempt) * 1000)
      );
    }
  }
}
Managing Keys:
  • Store keys temporarily for retry scenarios
  • Log keys for debugging and audit trails
  • Don’t persist keys long-term unless needed
  • Clean up old keys periodically
// Example key storage pattern
class IdempotencyManager {
  constructor() {
    this.pendingKeys = new Map();
  }
  
  generateKey(operation) {
    const key = crypto.randomUUID();
    this.pendingKeys.set(key, {
      operation,
      timestamp: Date.now()
    });
    return key;
  }
  
  cleanupOldKeys(maxAge = 3600000) { // 1 hour
    const now = Date.now();
    for (const [key, data] of this.pendingKeys) {
      if (now - data.timestamp > maxAge) {
        this.pendingKeys.delete(key);
      }
    }
  }
}

Key Behavior

Same Key, Same Result:When you retry a successful request with the same idempotency key:
  • Returns the original response
  • No additional processing occurs
  • Same HTTP status code
  • Identical response body
Response Example
{
  "id": "pay_123456789",
  "amount": 1000,
  "currency": "USD",
  "status": "succeeded",
  "created_at": "2024-01-15T10:30:00Z",
  "idempotency_key": "uuid-original-key"
}

Common Use Cases

Preventing Duplicate Charges:
// E-commerce checkout flow
async function processCheckout(orderData) {
  // Use order ID as part of key for context
  const idempotencyKey = `checkout_${orderData.orderId}_${Date.now()}`;
  
  try {
    const payment = await createPayment({
      amount: orderData.total,
      currency: orderData.currency,
      customer_id: orderData.customerId,
      metadata: { order_id: orderData.orderId }
    }, idempotencyKey);
    
    return { success: true, paymentId: payment.id };
  } catch (error) {
    // Safe to retry with same key on network errors
    if (isNetworkError(error)) {
      return processCheckout(orderData); // Will use same key
    }
    throw error;
  }
}
Handling Duplicate Webhooks:
// Use webhook event ID as idempotency key
app.post('/webhooks/devdraft', async (req, res) => {
  const event = req.body;
  const idempotencyKey = `webhook_${event.id}`;
  
  try {
    // Process webhook with idempotency
    await processWebhookEvent(event, idempotencyKey);
    res.status(200).send('OK');
  } catch (error) {
    if (error.code === 'idempotency_key_used') {
      // Already processed this webhook
      res.status(200).send('Already processed');
    } else {
      res.status(500).send('Error');
    }
  }
});
Processing Multiple Items:
// Each item in batch gets unique key
async function processBatchPayments(payments) {
  const results = [];
  
  for (const payment of payments) {
    const idempotencyKey = `batch_${batchId}_item_${payment.id}`;
    
    try {
      const result = await createPayment(payment, idempotencyKey);
      results.push({ success: true, data: result });
    } catch (error) {
      results.push({ success: false, error: error.message });
    }
  }
  
  return results;
}

Testing Idempotency

1

Create Test Scenario

Set up a test that makes the same request multiple times with the same idempotency key
2

Verify Response Consistency

Ensure all responses are identical (status, body, headers)
3

Check Side Effects

Confirm that the operation only occurred once (e.g., only one charge created)
4

Test Error Scenarios

Verify proper handling of conflicting keys and parameter mismatches
Use idempotency keys for all critical operations, especially in production environments where network issues and retries are common.
Idempotency keys are cached for 24 hours. After this period, using the same key will create a new operation rather than returning the cached result.