Skip to content
Last updated

Idempotency enables safe re-submission of requests, eliminating the risk of unintentionally duplicating operations.

What is Idempotency?

Idempotency ensures that making the same API call multiple times has the same effect as making it once. This is crucial when:

  • Network timeouts occur
  • You're unsure if a request was processed
  • You need to retry failed requests safely

How It Works

To make an idempotent request, include an Idempotency-Key header with a unique identifier:

curl -L -X POST 'https://api.test.easypay.pt/2.0/single' \
  -H 'AccountId: 2b0f63e2-9fb5-4e52-aca0-b4bf0339bbe6' \
  -H 'ApiKey: eae4aa59-8e5b-4ec2-887d-b02768481a92' \
  -H 'Idempotency-Key: 435e08a0-e5a9-4216-acb5-44d6b96de612' \
  -H 'Content-Type: application/json' \
  --data-raw '{
    "type": "sale",
    "value": 10.00,
    "currency": "EUR",
    "method": "cc"
  }'

What Happens

  1. First Request: Easypay processes the request and stores the response with the idempotency key
  2. Duplicate Requests: Subsequent requests with the same key return the stored response without re-processing
  3. Different Payload: If the request body differs, an error is returned to prevent accidental misuse

Detecting Replayed Requests

When a request is a replay, the response includes an Idempotency-Replay header:

Idempotency-Replay: true

This helps you distinguish between original and replayed requests in your application logic.

Generating Idempotency Keys

Best Practices:

  • Use UUIDs (v4) or ULIDs for idempotency keys
  • Generate a new key for each unique operation
  • Maximum key length: 50 characters
  • Keys should have sufficient entropy to prevent collisions

Examples:

// JavaScript (using UUID v4)
const { v4: uuidv4 } = require('uuid');
const idempotencyKey = uuidv4();
# Python (using UUID v4)
import uuid
idempotency_key = str(uuid.uuid4())
// PHP (using ramsey/uuid)
use Ramsey\Uuid\Uuid;
$idempotencyKey = Uuid::uuid4()->toString();

Key Behavior

Account-Specific Keys

Two requests with the same Idempotency-Key but different AccountId headers are treated as different requests.

Request 1: AccountId: account-1, Idempotency-Key: key-123
Request 2: AccountId: account-2, Idempotency-Key: key-123
Result: Both requests are processed independently

Request Body Comparison

The idempotency layer compares the request body of incoming requests with the original:

  • Same body: Returns the cached response
  • Different body: Returns an error to prevent accidental misuse
{
  "status": "error",
  "message": "Idempotency Error: Request body differs from original request",
  "code": "IDEMPOTENCY_MISMATCH"
}

Transient Errors

Idempotency keys are not stored for transient errors, allowing you to retry safely:

  • 429 Too Many Requests
  • 502 Bad Gateway
  • 503 Service Unavailable

For these errors, you can retry with the same idempotency key.

Key Expiration

Idempotency keys are automatically purged from the system 24 hours after creation. After expiration, the same key can be used for a new request.

When to Use Idempotency

Supported Operations

Idempotency keys are supported on:

  • POST requests
  • PATCH requests

Not Needed For

  • GET requests (inherently idempotent)
  • DELETE requests (inherently idempotent)

Use Cases

Essential:

  • Payment creation
  • Refund processing
  • Any operation that modifies financial data

Recommended:

  • Any POST or PATCH request
  • Operations that create or update resources
  • Requests that may be retried

Retry Strategy with Idempotency

When using idempotency keys, follow this retry strategy:

async function safeRequest(url, payload, maxRetries = 3) {
  const idempotencyKey = generateUUID();

  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          'AccountId': process.env.EASYPAY_ACCOUNT_ID,
          'ApiKey': process.env.EASYPAY_API_KEY,
          'Idempotency-Key': idempotencyKey,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(payload)
      });

      const shouldRetry = response.headers.get('X-Easypay-Should-Retry');

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

      if (shouldRetry === 'true' && i < maxRetries - 1) {
        await sleep(Math.pow(2, i) * 1000); // Exponential backoff
        continue;
      }

      throw new Error(`Request failed: ${response.status}`);

    } catch (error) {
      if (i < maxRetries - 1) {
        await sleep(Math.pow(2, i) * 1000);
        continue;
      }
      throw error;
    }
  }
}

Best Practices

  1. Always Use for Payments: Include idempotency keys on all payment-related requests
  2. Generate Fresh Keys: Create a new idempotency key for each unique operation
  3. Store Keys: Save idempotency keys in your database to track which operations completed
  4. Check Replay Header: Use the Idempotency-Replay header to detect replayed requests
  5. Respect Expiration: Don't reuse keys after 24 hours
  6. Handle Mismatches: If you get an idempotency mismatch error, generate a new key

Example: Creating a Payment Safely

const axios = require('axios');
const { v4: uuidv4 } = require('uuid');

async function createPayment(paymentData) {
  const idempotencyKey = uuidv4();

  try {
    const response = await axios.post(
      'https://api.prod.easypay.pt/2.0/single',
      paymentData,
      {
        headers: {
          'AccountId': process.env.EASYPAY_ACCOUNT_ID,
          'ApiKey': process.env.EASYPAY_API_KEY,
          'Idempotency-Key': idempotencyKey,
          'Content-Type': 'application/json'
        }
      }
    );

    // Check if this was a replayed request
    const wasReplayed = response.headers['idempotency-replay'] === 'true';

    if (wasReplayed) {
      console.log('This payment was already processed');
    }

    return response.data;

  } catch (error) {
    // Check if we should retry
    const shouldRetry = error.response?.headers['x-easypay-should-retry'];

    if (shouldRetry === 'true') {
      // Retry with the same idempotency key
      // ... implement retry logic
    }

    throw error;
  }
}

Next Steps