Skip to navigation

Idempotency

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:

OperationUse 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)

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

Implementation

Generating idempotency keys

Always use a randomly generated UUID v4 for idempotency keys:

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

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 -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"
}'

Handling Cached Responses

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

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:

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

How to handle:

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;
}
}

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

Best Practices

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

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

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

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

Each unique operation should have its own idempotency key:

// ❌ 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());

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.

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

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

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

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

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

Possible causes:

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

Solution:

// Store key before first attempt
const key = uuidv4();
// Use same key for all retries
await retryWithKey(operation, key);

Cause: Concurrent requests with same idempotency key

Solution: Wait for Retry-After duration before retrying:

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

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

Solution: Generate a new idempotency key:

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

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