***

title: Idempotency
description: 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.
</Note>

## 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:

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

  ```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
  import uuid

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

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

  ```go Go
  package main

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

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

      fmt.Println(idempotencyKey)
  }
  ```
</CodeGroup>

<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
</Warning>

### Making Idempotent Requests

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

<CodeGroup>
  ```bash cURL
  curl -X POST 'https://api.letsdeel.com/rest/v2/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"
    }'
  ```

  ```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/v2/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
  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/v2/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
  ```
</CodeGroup>

## 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:**

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

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

## Best Practices

<AccordionGroup>
  <Accordion title="Store idempotency keys with requests" icon="database">
    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
  </Accordion>

  <Accordion title="Use idempotency for all mutations" icon="pen">
    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
    ```
  </Accordion>

  <Accordion title="Don't reuse idempotency keys" icon="ban">
    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());
    ```
  </Accordion>

  <Accordion title="Respect the 24-hour cache window" icon="clock">
    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.
  </Accordion>

  <Accordion title="Handle errors appropriately" icon="triangle-exclamation">
    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;
        }
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## 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

<AccordionGroup>
  <Accordion title="Getting duplicates despite using idempotency keys" icon="copy">
    **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);
    ```
  </Accordion>

  <Accordion title="Receiving 429 errors" icon="triangle-exclamation">
    **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...
    }
    ```
  </Accordion>

  <Accordion title="Want to retry with same data but get new result" icon="rotate">
    **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
    ```
  </Accordion>
</AccordionGroup>

## 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

<CardGroup cols={2}>
  <Card title="Error Handling" icon="fa-light triangle-exclamation" href="/api/best-practices#error-handling--retries">
    Implement robust error handling with retries
  </Card>

  <Card title="Rate Limits" icon="fa-light gauge" href="/api/rate-limits">
    Understand API rate limits
  </Card>

  <Card title="Best Practices" icon="fa-light book" href="/api/best-practices">
    Learn integration best practices
  </Card>

  <Card title="Webhooks" icon="fa-light webhook" href="/api/webhooks/quickstart">
    Set up webhook event handling
  </Card>
</CardGroup>
