> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.deel.wtf/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.deel.wtf/_mcp/server.

# Idempotency

> Safely retry API requests without unintended side effects

## What is Idempotency?

Idempotency is the ability to ensure that **the same operation can be made multiple times with the same effect as a single execution**. This is critical for building reliable integrations that can safely retry failed requests without creating duplicate resources or triggering unintended side effects.

* **Safe Retries**: Retry failed requests without creating duplicates
* **Network Resilience**: Handle network failures and timeouts smoothly
* **Prevent Duplicates**: Avoid duplicate contracts, invoices, or payments
* **24-Hour Cache**: Responses are cached and reused for 24 hours

## When to Use Idempotency Keys

Use idempotency keys for any operation that **creates or modifies** resources:

| Operation                    | Use Idempotency Key?                  |
| ---------------------------- | ------------------------------------- |
| Creating a contract          | ✅ Yes                                 |
| Updating contract details    | ✅ Yes                                 |
| Creating invoice adjustments | ✅ Yes                                 |
| Submitting timesheets        | ✅ Yes                                 |
| Fetching contracts (GET)     | ❌ No (already idempotent)             |
| Deleting resources           | ❌ No (DELETE is naturally idempotent) |

> **Note**
>
> Idempotency keys are supported for **POST** and **PATCH** requests only.

## How It Works

When you include an `Idempotency-Key` header in your request:

1. **First request**: Deel processes the request normally and caches the successful response (2xx status codes only)
2. **Duplicate request** (same key within 24 hours): Deel immediately returns the cached response without processing again
3. **Cached responses** include an `x-original-request-id` header indicating the response is from cache

```mermaid
sequenceDiagram
    participant Client
    participant Deel API
    participant Cache

    Client->>Deel API: POST /contracts (Idempotency-Key: abc123)
    Deel API->>Deel API: Process request
    Deel API->>Cache: Store response (24h)
    Deel API->>Client: 201 Created

    Note over Client: Network error, retry

    Client->>Deel API: POST /contracts (Idempotency-Key: abc123)
    Deel API->>Cache: Check for key
    Cache->>Deel API: Found cached response
    Deel API->>Client: 201 Created (x-original-request-id header)
```

## Implementation

### Generating idempotency keys

Always use a **randomly generated UUID v4** for idempotency keys:

**`cURL`**

```bash cURL
# Generate UUID on command line
IDEMPOTENCY_KEY=$(uuidgen)
echo $IDEMPOTENCY_KEY
```

**`Node.js`**

```javascript Node.js
const { v4: uuidv4 } = require('uuid');

// Generate a unique idempotency key
const idempotencyKey = uuidv4();

console.log(idempotencyKey); // e.g., "550e8400-e29b-41d4-a716-446655440000"
```

**`Python`**

```python Python
import uuid

# Generate a unique idempotency key
idempotency_key = str(uuid.uuid4())

print(idempotency_key)  # e.g., "550e8400-e29b-41d4-a716-446655440000"
```

**`Go`**

```go Go
package main

import (
    "github.com/google/uuid"
)

func main() {
    // Generate a unique idempotency key
    idempotencyKey := uuid.New().String()

    fmt.Println(idempotencyKey)
}
```

> **Warning**
>
> **Key Requirements:**
>
> * Must be unique for at least 24 hours
> * Can be up to 64 characters long
> * Use UUID v4 for best results
> * Never reuse keys across different operations

### Making idempotent requests

Include the `Idempotency-Key` header in your POST or PATCH requests:

**`cURL`**

```bash cURL
curl -X POST 'https://api.letsdeel.com/rest/contracts' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \
  -d '{
    "client_id": "org_123",
    "worker_email": "contractor@example.com",
    "job_title": "Backend Developer",
    "scope": "Software Development",
    "rate": 100,
    "rate_type": "hourly"
  }'
```

**`Node.js`**

```javascript Node.js
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');

async function createContract(contractData) {
  const idempotencyKey = uuidv4();

  try {
    const response = await axios.post(
      'https://api.letsdeel.com/rest/contracts',
      contractData,
      {
        headers: {
          'Authorization': `Bearer ${process.env.DEEL_API_TOKEN}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey
        }
      }
    );

    return response.data;
  } catch (error) {
    // Safe to retry with the same idempotency key
    if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') {
      console.log('Retrying with same idempotency key...');
      return createContract(contractData); // Retry logic would use same key
    }
    throw error;
  }
}
```

**`Python`**

```python Python
import requests
import uuid
import os

def create_contract(contract_data):
    idempotency_key = str(uuid.uuid4())

    try:
        response = requests.post(
            'https://api.letsdeel.com/rest/contracts',
            json=contract_data,
            headers={
                'Authorization': f'Bearer {os.getenv("DEEL_API_TOKEN")}',
                'Content-Type': 'application/json',
                'Idempotency-Key': idempotency_key
            }
        )
        response.raise_for_status()
        return response.json()

    except requests.exceptions.RequestException as e:
        # Safe to retry with the same idempotency key
        print(f'Request failed: {e}')
        raise
```

## Handling Cached Responses

When Deel returns a cached response, you can identify it by checking for the `x-original-request-id` header:

```javascript
const response = await axios.post(url, data, {
  headers: {
    'Idempotency-Key': idempotencyKey
  }
});

// Check if response is from cache
const originalRequestId = response.headers['x-original-request-id'];

if (originalRequestId) {
  console.log(`Cached response from request: ${originalRequestId}`);
  console.log('This request was not processed again');
} else {
  console.log('New request processed successfully');
}
```

## Handling Concurrent Requests

If you send multiple requests with the same idempotency key **while a request is still in progress**, you'll receive a `429` error:

```json
{
  "error": "Request already in progress",
  "status": 429,
  "headers": {
    "Retry-After": "5"
  }
}
```

**How to handle:**

**`Node.js`**

```javascript Node.js
async function createContractWithRetry(contractData, idempotencyKey) {
  try {
    return await createContract(contractData, idempotencyKey);
  } catch (error) {
    if (error.response?.status === 429) {
      const retryAfter = error.response.headers['retry-after'] || 2;
      console.log(`Request in progress. Waiting ${retryAfter}s...`);

      // Wait and retry
      await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
      return createContract(contractData, idempotencyKey);
    }
    throw error;
  }
}
```

**`Python`**

```python Python
import time
import requests

def create_contract_with_retry(contract_data, idempotency_key):
    try:
        return create_contract(contract_data, idempotency_key)
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 429:
            retry_after = int(e.response.headers.get('Retry-After', 2))
            print(f'Request in progress. Waiting {retry_after}s...')

            # Wait and retry
            time.sleep(retry_after)
            return create_contract(contract_data, idempotency_key)
        raise
```

> **Warning**
>
> If you **intentionally cancel** a request and want to retry, wait **2 minutes** before using the same idempotency key.

## Best Practices

#### Store idempotency keys with requests

When making critical requests, store the idempotency key in your database alongside the request data:

```javascript
// Before making request
const idempotencyKey = uuidv4();
await db.contracts.create({
  data: contractData,
  idempotencyKey: idempotencyKey,
  status: 'pending'
});

// Make request
const response = await createContract(contractData, idempotencyKey);

// Update status
await db.contracts.update({
  where: { idempotencyKey },
  data: { status: 'created', deelId: response.id }
});
```

**Benefits:**

* Track which requests succeeded
* Retry failed requests with same key
* Audit trail of all attempts

#### Use idempotency for all mutations

Always include idempotency keys for operations that create or modify data:

```javascript
// ✅ Good - Using idempotency key
await createContract(data, { idempotencyKey: uuidv4() });
await updateContract(id, data, { idempotencyKey: uuidv4() });
await createInvoiceAdjustment(data, { idempotencyKey: uuidv4() });

// ❌ Bad - No idempotency key
await createContract(data); // Risk of duplicates on retry
```

#### Don't reuse idempotency keys

Each unique operation should have its own idempotency key:

```javascript
// ❌ Bad - Reusing key for different operations
const key = uuidv4();
await createContract(data1, key);
await createContract(data2, key); // Wrong! Will return first contract

// ✅ Good - Unique key per operation
await createContract(data1, uuidv4());
await createContract(data2, uuidv4());
```

#### Respect the 24-hour cache window

Cached responses last for 24 hours. Plan your retry logic accordingly:

* Immediate retries: Safe (returns cached response)
* Retries within 24 hours: Returns cached response
* After 24 hours: Key can be reused (cache expired)

For intentional operation retries (not network failures), generate a new key.

#### Handle errors appropriately

Only successful responses (2xx) are cached. Failed requests should be retried:

```javascript
async function safeCreateContract(data) {
  const idempotencyKey = uuidv4();
  let attempts = 0;
  const maxAttempts = 3;

  while (attempts < maxAttempts) {
    try {
      return await createContract(data, idempotencyKey);
    } catch (error) {
      attempts++;

      // Retry on network errors or 5xx
      if (error.code === 'ECONNRESET' ||
          error.response?.status >= 500) {
        if (attempts < maxAttempts) {
          await sleep(Math.pow(2, attempts) * 1000);
          continue; // Retry with same key
        }
      }

      // Don't retry on client errors (4xx)
      throw error;
    }
  }
}
```

## Common Scenarios

### Scenario 1: Network Timeout

```javascript
const idempotencyKey = uuidv4();

try {
  // First attempt times out
  await createContract(data, idempotencyKey);
} catch (error) {
  if (error.code === 'ETIMEDOUT') {
    // Safe to retry with same key
    // If first request succeeded, you'll get the cached response
    const result = await createContract(data, idempotencyKey);
    console.log('Retry successful');
  }
}
```

### Scenario 2: Uncertain Request Status

```javascript
const idempotencyKey = uuidv4();

try {
  await createContract(data, idempotencyKey);
} catch (error) {
  // Uncertain if request succeeded
  console.log('Not sure if contract was created');
}

// Check if contract was created by retrying with same key
try {
  const result = await createContract(data, idempotencyKey);

  // Check for cached response header
  if (result.headers['x-original-request-id']) {
    console.log('Contract was created in first attempt');
  } else {
    console.log('Contract just created now');
  }
} catch (error) {
  console.log('Contract definitely not created');
}
```

### Scenario 3: Batch Operations

```javascript
async function createMultipleContracts(contractsData) {
  const results = await Promise.allSettled(
    contractsData.map(data => {
      // Each contract gets unique idempotency key
      const idempotencyKey = uuidv4();

      return createContract(data, idempotencyKey);
    })
  );

  // Process results
  const succeeded = results.filter(r => r.status === 'fulfilled');
  const failed = results.filter(r => r.status === 'rejected');

  console.log(`Created: ${succeeded.length}, Failed: ${failed.length}`);

  // Retry failed ones with same keys (if stored)
  return { succeeded, failed };
}
```

## Troubleshooting

#### Getting duplicates despite using idempotency keys

**Possible causes:**

* Generating new keys for each retry (should reuse key)
* Not including `Idempotency-Key` header
* Using different keys for same operation

**Solution:**

```javascript
// Store key before first attempt
const key = uuidv4();

// Use same key for all retries
await retryWithKey(operation, key);
```

#### Receiving 429 errors

**Cause:** Concurrent requests with same idempotency key

**Solution:** Wait for `Retry-After` duration before retrying:

```javascript
if (error.response?.status === 429) {
  const retryAfter = error.response.headers['retry-after'];
  await sleep(retryAfter * 1000);
  // Retry...
}
```

#### Want to retry with same data but get new result

**Scenario:** First request created wrong resource, want to create a new one

**Solution:** Generate a new idempotency key:

```javascript
// First attempt (wrong data)
await createContract(wrongData, uuidv4());

// New attempt (correct data, new key)
await createContract(correctData, uuidv4()); // New key
```

## Testing Idempotency

Test that your integration handles idempotency correctly:

```javascript
describe('Idempotency', () => {
  it('should not create duplicates on retry', async () => {
    const data = { /* contract data */ };
    const key = uuidv4();

    // First request
    const result1 = await createContract(data, key);

    // Retry with same key
    const result2 = await createContract(data, key);

    // Should return same contract
    expect(result1.id).toBe(result2.id);
    expect(result2.headers['x-original-request-id']).toBeDefined();
  });

  it('should create different resources with different keys', async () => {
    const data = { /* contract data */ };

    const result1 = await createContract(data, uuidv4());
    const result2 = await createContract(data, uuidv4());

    // Should be different contracts
    expect(result1.id).not.toBe(result2.id);
  });
});
```

## Next Steps

#### [Error Handling](/api/best-practices#error-handling--retries)

Implement robust error handling with retries

#### [Rate Limits](/api/rate-limits)

Understand API rate limits

#### [Best Practices](/api/best-practices)

Learn integration best practices

#### [Webhooks](/api/webhooks/quickstart)

Set up webhook event handling