> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.deel.wtf/api/stable/idempotency/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 > Build apps and integrations that extend and enhance the Deel services.