# Idempotency

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:


```bash
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
// JavaScript (using UUID v4)
const { v4: uuidv4 } = require('uuid');
const idempotencyKey = uuidv4();
```


```python
# Python (using UUID v4)
import uuid
idempotency_key = str(uuid.uuid4())
```


```php
// 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



```json
{
  "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:


```javascript
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


```javascript
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

- [Error Handling](/docs/error-handling) - Learn about error responses and retry strategies
- [Quick Start](/docs/quickstart) - Make your first idempotent API call
- [API Reference](/openapi) - See which endpoints support idempotency