Idempotency Key Support

Prevent duplicate transactions when network issues cause uncertain request outcomes

Overview

The Preczn API supports idempotent operations for select POST, PATCH, and DELETE endpoints via the Idempotency-Key HTTP header. This feature prevents duplicate resource creation when requests are retried due to network interruptions or timeouts.

While use of idempotency keys is optional, we strongly recommend including them on all transaction requests to protect against duplicate charges.

Why Use Idempotency Keys

Network communication is inherently unreliable. Even when your request successfully reaches Preczn and is processed, the response may fail to reach you due to:

  • Gateway timeouts (504 errors)
  • Network interruptions
  • Load balancer timeouts
  • Connection resets
⚠️

Critical Scenario: The 504 Timeout Trap

A common and costly mistake: Your transaction request receives a 504 Gateway Timeout, leading you to assume the request failed. In reality, the transaction completed successfully on Preczn's backend—the timeout only affected the response.

Without an idempotency key: If you retry the request, you risk creating a duplicate transaction and charging the customer twice.

With an idempotency key: Retrying the same request with the same key returns the original successful response, confirming the transaction completed without creating a duplicate.

When to Always Use Idempotency Keys

ScenarioRisk Without KeyProtection With Key
504 Gateway TimeoutDuplicate transaction if retriedSafe to retry; returns original response
502 Bad GatewayUnknown if processed; may duplicateSafe to retry; no duplicate created
Connection resetRequest may have completedSafe to retry with confidence
Client timeoutTransaction may have succeededRetry returns actual outcome

How It Works

Idempotent Behavior: When you include an idempotency key, the system checks if a successful request with that same key was already processed within the past 24 hours. If found, the original response is returned regardless of subsequent resource changes. New or expired keys trigger normal request processing.

Time-to-Live (TTL): Each idempotency key remains valid for 24 hours from the initial request timestamp.

Implementation

Include the Idempotency-Key header in your requests:

POST /v1/transactions HTTP/1.1
Host: api.preczn.com
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

{
  "amount": 1000,
  "currency": "USD",
  ...
}

Best Practices

1. Generate UUID Keys

Create a unique UUID/GUID for each distinct operation:

const idempotencyKey = crypto.randomUUID();

2. Include on First Attempt

Always include the idempotency key from the initial request, not just on retries. This ensures the key is registered before any potential failure occurs.

// ✅ Correct: Key included on first attempt
async function createTransaction(data) {
  const idempotencyKey = crypto.randomUUID();

  return await fetch('/v1/transactions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey
    },
    body: JSON.stringify(data)
  });
}

3. Retry with the Same Key on Timeouts

📘

Key Point

When you receive a 504, 502, or any timeout error, always retry using the same idempotency key. This is the entire purpose of the feature—it lets you safely retry without risking duplicates.

async function createTransactionWithRetry(data, maxRetries = 3) {
  const idempotencyKey = crypto.randomUUID();

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch('/v1/transactions', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey  // Same key for all attempts
        },
        body: JSON.stringify(data)
      });

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

      // Retry on 502, 503, 504 errors
      if ([502, 503, 504].includes(response.status) && attempt < maxRetries) {
        await sleep(1000 * attempt);  // Exponential backoff
        continue;
      }

      throw new Error(`Request failed: ${response.status}`);
    } catch (error) {
      if (error.name === 'TimeoutError' && attempt < maxRetries) {
        await sleep(1000 * attempt);
        continue;  // Retry with same idempotency key
      }
      throw error;
    }
  }
}

4. Store Keys for Reconciliation

Persist idempotency keys with your transaction records. If you need to verify whether a transaction completed, you can retry with the stored key to get a definitive answer.

5. Avoid Key Reuse Across Operations

Never reuse idempotency keys for different operations or endpoints. Each unique business operation should have its own key.

// ❌ Wrong: Reusing key for different transactions
const sharedKey = crypto.randomUUID();
await createTransaction(order1, sharedKey);
await createTransaction(order2, sharedKey);  // Will return order1's response!

// ✅ Correct: Unique key per transaction
await createTransaction(order1, crypto.randomUUID());
await createTransaction(order2, crypto.randomUUID());

Supported Endpoints

📘

Note

Not all endpoints support idempotency. Consult the API Reference for the complete list of supported endpoints. Generally, all transaction-related POST endpoints support idempotency keys.