Idempotency

When making POST requests to Devdraft API endpoints, you must include an idempotency-key header to ensure your operations are processed safely and can be retried without creating duplicates. This guide explains how to properly implement idempotency keys in your client systems.

What Are Idempotency Keys?Copied!

An idempotency key is a unique identifier that you send with each POST request. If you send the same request multiple times with the same idempotency key, our API will:

  1. Process the operation only once

  2. Return the same response for subsequent requests with that key

  3. Prevent duplicate payments, transfers, or resource creation

When to Use Idempotency KeysCopied!

Required for ALL POST Endpoints

Every POST request to our API requires an idempotency key:

POST /api/v0/payment-intents/stablecoin
POST /api/v0/payment-intents/bank
POST /api/v0/payment-links
POST /api/v0/test-payment

Example Scenarios Where Idempotency Saves You

  1. Network Timeout: Request times out, but you're unsure if it processed

  2. Connection Drop: Network connection fails after sending request

  3. Server Error: You receive a 500 error and need to retry

  4. User Double-Click: User accidentally clicks "Pay" button twice

  5. Application Retry Logic: Your app automatically retries failed requests

How to Generate Idempotency KeysCopied!

Required Format: UUID v4

Your idempotency key must be a valid UUID version 4:

550e8400-e29b-41d4-a716-446655440000

Key Generation Examples

JavaScript/TypeScript
// Method 1: Using crypto.randomUUID() (modern browsers/Node.js 14.17+)
const idempotencyKey = crypto.randomUUID();

// Method 2: Using uuid library
import { v4 as uuidv4 } from 'uuid';
const idempotencyKey = uuidv4();

// Method 3: Custom function for older environments
function generateUUID() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    const r = Math.random() * 16 | 0;
    const v = c === 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}
Python
import uuid

# Generate a new UUID v4
idempotency_key = str(uuid.uuid4())
# Example: "550e8400-e29b-41d4-a716-446655440000"
Java
import java.util.UUID;

// Generate a new UUID v4
String idempotencyKey = UUID.randomUUID().toString();
C#
using System;

// Generate a new UUID v4
string idempotencyKey = Guid.NewGuid().ToString();
PHP
// Using ramsey/uuid library (recommended)
use Ramsey\Uuid\Uuid;

$idempotencyKey = Uuid::uuid4()->toString();

// Or using built-in function (PHP 8.2+)
$idempotencyKey = random_bytes(16);
$idempotencyKey = sprintf('%08s-%04s-4%03s-%04s-%12s',
    bin2hex(substr($idempotencyKey, 0, 4)),
    bin2hex(substr($idempotencyKey, 4, 2)),
    bin2hex(substr($idempotencyKey, 6, 2)) & 0x0fff,
    bin2hex(substr($idempotencyKey, 8, 2)) & 0x3fff | 0x8000,
    bin2hex(substr($idempotencyKey, 10, 6))
);
Go
import "github.com/google/uuid"

// Generate a new UUID v4
idempotencyKey := uuid.New().String()
Ruby
require 'securerandom'

# Generate a new UUID v4
idempotency_key = SecureRandom.uuid

Implementation Best PracticesCopied!

1. One Key Per Unique Operation

✅ Correct: Generate a new key for each distinct operation

// Creating different payment intents
const payment1Key = crypto.randomUUID(); // "550e8400-e29b-41d4-a716-446655440000"
const payment2Key = crypto.randomUUID(); // "6ba7b810-9dad-11d1-80b4-00c04fd430c8"

await createPaymentIntent(payment1Data, payment1Key);
await createPaymentIntent(payment2Data, payment2Key);

❌ Wrong: Reusing the same key for different operations

// DON'T DO THIS
const sameKey = crypto.randomUUID();
await createPaymentIntent(payment1Data, sameKey);
await createPaymentIntent(payment2Data, sameKey); // Will return payment1 response!

2. Store Keys for Retry Scenarios

✅ Recommended: Store the key so you can retry with the same one

class PaymentClient {
  async createPayment(paymentData) {
    // Generate and store the key
    const idempotencyKey = crypto.randomUUID();
    this.storeKeyForRetry('payment', paymentData.orderId, idempotencyKey);
    
    try {
      return await this.makePaymentRequest(paymentData, idempotencyKey);
    } catch (error) {
      // On failure, you can retry with the same key
      console.log(`Retrying with same key: ${idempotencyKey}`);
      throw error; // Let retry logic handle it
    }
  }
  
  private storeKeyForRetry(operation, identifier, key) {
    localStorage.setItem(`idempotency_${operation}_${identifier}`, key);
  }
}

3. Meaningful Key Patterns (Optional)

While UUID v4 is required, you can add prefixes for organization:

// Add descriptive prefixes (still must be valid UUID v4)
const paymentKey = crypto.randomUUID(); // Store as: "payment_" + paymentKey
const refundKey = crypto.randomUUID();  // Store as: "refund_" + refundKey

// Note: The actual header value must still be the pure UUID
headers: {
  'idempotency-key': paymentKey // NOT "payment_" + paymentKey
}

Complete Request ExamplesCopied!

Create Stablecoin Payment Intent

curl -X POST https://api.devdraft.ai/api/v0/payment-intents/stablecoin \
  -H "Content-Type: application/json" \
  -H "x-client-key: your-client-key" \
  -H "x-client-secret: your-client-secret" \
  -H "idempotency-key: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "sourceCurrency": "usdc",
    "sourceNetwork": "ethereum",
    "destinationCurrency": "eurc",
    "destinationNetwork": "polygon",
    "destinationAddress": "0x742d35Cc6634C0532925a3b8D4C9db96c4b4d8e1",
    "amount": "100.00",
    "customer_email": "customer@example.com"
  }'

JavaScript Fetch Example

async function createStablecoinPaymentIntent(paymentData) {
  const idempotencyKey = crypto.randomUUID();
  
  const response = await fetch('/api/v0/payment-intents/stablecoin', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-client-key': process.env.CLIENT_KEY,
      'x-client-secret': process.env.CLIENT_SECRET,
      'idempotency-key': idempotencyKey,
    },
    body: JSON.stringify(paymentData),
  });
  
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  
  return response.json();
}

Python Requests Example

import requests
import uuid

def create_bank_payment_intent(payment_data):
    idempotency_key = str(uuid.uuid4())
    
    headers = {
        'Content-Type': 'application/json',
        'x-client-key': os.getenv('CLIENT_KEY'),
        'x-client-secret': os.getenv('CLIENT_SECRET'),
        'idempotency-key': idempotency_key,
    }
    
    response = requests.post(
        'https://api.example.com/api/v0/payment-intents/bank',
        headers=headers,
        json=payment_data
    )
    
    response.raise_for_status()
    return response.json()

Response BehaviorCopied!

First Request (201 Created)

POST /api/v0/payment-intents/stablecoin
idempotency-key: 550e8400-e29b-41d4-a716-446655440000

HTTP/1.1 201 Created
{
  "id": "txn_01HZXK8M9N2P3Q4R5S6T7U8V9W",
  "bridge_transfer_id": "transfer_abc123xyz456",
  "state": "pending",
  "amount": "100.00"
}

Subsequent Requests (200 OK)

POST /api/v0/payment-intents/stablecoin
idempotency-key: 550e8400-e29b-41d4-a716-446655440000

HTTP/1.1 200 OK
{
  "id": "txn_01HZXK8M9N2P3Q4R5S6T7U8V9W",
  "bridge_transfer_id": "transfer_abc123xyz456",
  "state": "pending",
  "amount": "100.00"
}

Error HandlingCopied!

Missing Idempotency Key

HTTP/1.1 400 Bad Request
{
  "statusCode": 400,
  "message": "Idempotency key is required for this operation"
}

Invalid UUID Format

HTTP/1.1 400 Bad Request
{
  "statusCode": 400,
  "message": "Idempotency key must be a valid UUID v4 format"
}

Conflict (Same Key, Different Data)

HTTP/1.1 409 Conflict
{
  "statusCode": 409,
  "message": "Idempotency key already used with different parameters"
}

Retry Implementation PatternsCopied!

Basic Retry with Exponential Backoff

async function createPaymentWithRetry(paymentData, maxRetries = 3) {
  const idempotencyKey = crypto.randomUUID();
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await createPayment(paymentData, idempotencyKey);
    } catch (error) {
      if (attempt === maxRetries) throw error;
      
      // Exponential backoff: 1s, 2s, 4s
      const delay = Math.pow(2, attempt - 1) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
      
      console.log(`Retrying payment (attempt ${attempt + 1}) with same key: ${idempotencyKey}`);
    }
  }
}

Advanced Retry with Error Classification

class PaymentClient {
  async createPaymentWithIntelligentRetry(paymentData) {
    const idempotencyKey = crypto.randomUUID();
    
    const retryableErrors = [408, 429, 500, 502, 503, 504];
    const maxRetries = 3;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await this.createPayment(paymentData, idempotencyKey);
      } catch (error) {
        // Don't retry on client errors (400, 401, 403, 404, 409)
        if (error.status < 500 && !retryableErrors.includes(error.status)) {
          throw error;
        }
        
        if (attempt === maxRetries) throw error;
        
        const delay = Math.min(Math.pow(2, attempt - 1) * 1000, 10000); // Cap at 10s
        await this.sleep(delay);
      }
    }
  }
  
  private sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Key ExpirationCopied!

  • TTL: Idempotency keys expire after 24 hours

  • After Expiration: You can reuse the same UUID for a new operation

  • Best Practice: Always generate fresh keys for new operations

Common Mistakes to AvoidCopied!

❌ Don't Use Sequential or Predictable Keys

// DON'T DO THIS
const badKey = `payment-${Date.now()}`; // Not UUID v4
const badKey2 = `${userId}-${orderId}`; // Not UUID v4

❌ Don't Reuse Keys Across Different Operations

// DON'T DO THIS
const key = crypto.randomUUID();
await createPayment(paymentData, key);
await createRefund(refundData, key); // Wrong! Use different key

❌ Don't Include Extra Data in the Key

// DON'T DO THIS
const key = `payment_${crypto.randomUUID()}_${userId}`;
// The header value must be pure UUID v4

❌ Don't Change Request Data with Same Key

// DON'T DO THIS
const key = crypto.randomUUID();

// First request
await createPayment({ amount: "100.00" }, key);

// Second request with same key but different data
await createPayment({ amount: "200.00" }, key); // Will return 409 Conflict

Testing Your ImplementationCopied!

Test Scenarios

  1. Basic Idempotency

const key = crypto.randomUUID();
const response1 = await createPayment(data, key);
const response2 = await createPayment(data, key);

assert(response1.id === response2.id); // Same response
assert(response1.status === 201);      // First request
assert(response2.status === 200);      // Subsequent request
  1. Different Keys Create Different Resources

const key1 = crypto.randomUUID();
const key2 = crypto.randomUUID();
const response1 = await createPayment(data, key1);
const response2 = await createPayment(data, key2);

assert(response1.id !== response2.id); // Different resources
  1. Conflict Detection

const key = crypto.randomUUID();
await createPayment({ amount: "100" }, key);

// This should return 409 Conflict
try {
  await createPayment({ amount: "200" }, key);
  assert(false, "Should have thrown conflict error");
} catch (error) {
  assert(error.status === 409);
}

SDK Integration ExamplesCopied!

React Hook Example

import { useState } from 'react';

function usePaymentIntent() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const createPaymentIntent = async (paymentData) => {
    setLoading(true);
    setError(null);
    
    const idempotencyKey = crypto.randomUUID();
    
    try {
      const response = await fetch('/api/v0/payment-intents/stablecoin', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-client-key': process.env.REACT_APP_CLIENT_KEY,
          'x-client-secret': process.env.REACT_APP_CLIENT_SECRET,
          'idempotency-key': idempotencyKey,
        },
        body: JSON.stringify(paymentData),
      });
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      return await response.json();
    } catch (err) {
      setError(err.message);
      throw err;
    } finally {
      setLoading(false);
    }
  };
  
  return { createPaymentIntent, loading, error };
}

Node.js Service Example

class PaymentService {
  constructor(clientKey, clientSecret, baseUrl) {
    this.clientKey = clientKey;
    this.clientSecret = clientSecret;
    this.baseUrl = baseUrl;
  }
  
  async createStablecoinPaymentIntent(paymentData) {
    const idempotencyKey = crypto.randomUUID();
    
    const response = await fetch(`${this.baseUrl}/api/v0/payment-intents/stablecoin`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-client-key': this.clientKey,
        'x-client-secret': this.clientSecret,
        'idempotency-key': idempotencyKey,
      },
      body: JSON.stringify(paymentData),
    });
    
    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(`API Error ${response.status}: ${errorData.message}`);
    }
    
    return response.json();
  }
}

// Usage
const paymentService = new PaymentService(
  process.env.CLIENT_KEY,
  process.env.CLIENT_SECRET,
  'https://api.example.com'
);

const paymentIntent = await paymentService.createStablecoinPaymentIntent({
  sourceCurrency: "usdc",
  sourceNetwork: "ethereum",
  destinationCurrency: "eurc",
  destinationNetwork: "polygon",
  amount: "100.00"
});

ChecklistCopied!

Before integrating, ensure you:

  • Generate valid UUID v4 keys using proper libraries

  • Include idempotency-key header in all POST requests

  • Use unique keys for each distinct operation

  • Store keys for retry scenarios

  • Implement proper error handling for 400, 409 errors

  • Test idempotency behavior in your application

  • Handle both 201 (first request) and 200 (subsequent) status codes

  • Don't reuse keys across different operations or with different data