For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
SupportDeel Home
OverviewPlatformEmployer of RecordContractorsGlobal PayrollHREmbeddedDeel ITAPI ReferenceChangelog
OverviewPlatformEmployer of RecordContractorsGlobal PayrollHREmbeddedDeel ITAPI ReferenceChangelog
  • Resources
    • Blog
    • Community
    • API spec
  • Get Started
    • Quickstart
    • Sandbox
  • Essentials
    • Authentication
    • API versioning
    • OAuth 2.0
    • Rate Limits
    • Idempotency
    • Best Practices
  • Webhooks
    • Introduction
    • Quickstart
    • No Code
    • Events
    • Simulations
  • Partners
    • Introduction
    • Getting Started
    • Publishing to App Store
LogoLogo
SupportDeel Home
On this page
  • What is Idempotency?
  • When to Use Idempotency Keys
  • How It Works
  • Implementation
  • Generating Idempotency Keys
  • Making Idempotent Requests
  • Handling Cached Responses
  • Handling Concurrent Requests
  • Best Practices
  • Common Scenarios
  • Scenario 1: Network Timeout
  • Scenario 2: Uncertain Request Status
  • Scenario 3: Batch Operations
  • Troubleshooting
  • Testing Idempotency
  • Next Steps
Essentials

Idempotency

Was this page helpful?
Previous

Best Practices

Next
Built with

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

Handling Cached Responses

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

1const response = await axios.post(url, data, {
2 headers: {
3 'Idempotency-Key': idempotencyKey
4 }
5});
6
7// Check if response is from cache
8const originalRequestId = response.headers['x-original-request-id'];
9
10if (originalRequestId) {
11 console.log(`Cached response from request: ${originalRequestId}`);
12 console.log('This request was not processed again');
13} else {
14 console.log('New request processed successfully');
15}

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:

1{
2 "error": "Request already in progress",
3 "status": 429,
4 "headers": {
5 "Retry-After": "5"
6 }
7}

How to handle:

1async function createContractWithRetry(contractData, idempotencyKey) {
2 try {
3 return await createContract(contractData, idempotencyKey);
4 } catch (error) {
5 if (error.response?.status === 429) {
6 const retryAfter = error.response.headers['retry-after'] || 2;
7 console.log(`Request in progress. Waiting ${retryAfter}s...`);
8
9 // Wait and retry
10 await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
11 return createContract(contractData, idempotencyKey);
12 }
13 throw error;
14 }
15}

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:

1// Before making request
2const idempotencyKey = uuidv4();
3await db.contracts.create({
4 data: contractData,
5 idempotencyKey: idempotencyKey,
6 status: 'pending'
7});
8
9// Make request
10const response = await createContract(contractData, idempotencyKey);
11
12// Update status
13await db.contracts.update({
14 where: { idempotencyKey },
15 data: { status: 'created', deelId: response.id }
16});

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:

1// ✅ Good - Using idempotency key
2await createContract(data, { idempotencyKey: uuidv4() });
3await updateContract(id, data, { idempotencyKey: uuidv4() });
4await createInvoiceAdjustment(data, { idempotencyKey: uuidv4() });
5
6// ❌ Bad - No idempotency key
7await createContract(data); // Risk of duplicates on retry
Don't reuse idempotency keys

Each unique operation should have its own idempotency key:

1// ❌ Bad - Reusing key for different operations
2const key = uuidv4();
3await createContract(data1, key);
4await createContract(data2, key); // Wrong! Will return first contract
5
6// ✅ Good - Unique key per operation
7await createContract(data1, uuidv4());
8await 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:

1async function safeCreateContract(data) {
2 const idempotencyKey = uuidv4();
3 let attempts = 0;
4 const maxAttempts = 3;
5
6 while (attempts < maxAttempts) {
7 try {
8 return await createContract(data, idempotencyKey);
9 } catch (error) {
10 attempts++;
11
12 // Retry on network errors or 5xx
13 if (error.code === 'ECONNRESET' ||
14 error.response?.status >= 500) {
15 if (attempts < maxAttempts) {
16 await sleep(Math.pow(2, attempts) * 1000);
17 continue; // Retry with same key
18 }
19 }
20
21 // Don't retry on client errors (4xx)
22 throw error;
23 }
24 }
25}

Common Scenarios

Scenario 1: Network Timeout

1const idempotencyKey = uuidv4();
2
3try {
4 // First attempt times out
5 await createContract(data, idempotencyKey);
6} catch (error) {
7 if (error.code === 'ETIMEDOUT') {
8 // Safe to retry with same key
9 // If first request succeeded, you'll get the cached response
10 const result = await createContract(data, idempotencyKey);
11 console.log('Retry successful');
12 }
13}

Scenario 2: Uncertain Request Status

1const idempotencyKey = uuidv4();
2
3try {
4 await createContract(data, idempotencyKey);
5} catch (error) {
6 // Uncertain if request succeeded
7 console.log('Not sure if contract was created');
8}
9
10// Check if contract was created by retrying with same key
11try {
12 const result = await createContract(data, idempotencyKey);
13
14 // Check for cached response header
15 if (result.headers['x-original-request-id']) {
16 console.log('Contract was created in first attempt');
17 } else {
18 console.log('Contract just created now');
19 }
20} catch (error) {
21 console.log('Contract definitely not created');
22}

Scenario 3: Batch Operations

1async function createMultipleContracts(contractsData) {
2 const results = await Promise.allSettled(
3 contractsData.map(data => {
4 // Each contract gets unique idempotency key
5 const idempotencyKey = uuidv4();
6
7 return createContract(data, idempotencyKey);
8 })
9 );
10
11 // Process results
12 const succeeded = results.filter(r => r.status === 'fulfilled');
13 const failed = results.filter(r => r.status === 'rejected');
14
15 console.log(`Created: ${succeeded.length}, Failed: ${failed.length}`);
16
17 // Retry failed ones with same keys (if stored)
18 return { succeeded, failed };
19}

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:

1// Store key before first attempt
2const key = uuidv4();
3
4// Use same key for all retries
5await retryWithKey(operation, key);
Receiving 429 errors

Cause: Concurrent requests with same idempotency key

Solution: Wait for Retry-After duration before retrying:

1if (error.response?.status === 429) {
2 const retryAfter = error.response.headers['retry-after'];
3 await sleep(retryAfter * 1000);
4 // Retry...
5}
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:

1// First attempt (wrong data)
2await createContract(wrongData, uuidv4());
3
4// New attempt (correct data, new key)
5await createContract(correctData, uuidv4()); // New key

Testing Idempotency

Test that your integration handles idempotency correctly:

1describe('Idempotency', () => {
2 it('should not create duplicates on retry', async () => {
3 const data = { /* contract data */ };
4 const key = uuidv4();
5
6 // First request
7 const result1 = await createContract(data, key);
8
9 // Retry with same key
10 const result2 = await createContract(data, key);
11
12 // Should return same contract
13 expect(result1.id).toBe(result2.id);
14 expect(result2.headers['x-original-request-id']).toBeDefined();
15 });
16
17 it('should create different resources with different keys', async () => {
18 const data = { /* contract data */ };
19
20 const result1 = await createContract(data, uuidv4());
21 const result2 = await createContract(data, uuidv4());
22
23 // Should be different contracts
24 expect(result1.id).not.toBe(result2.id);
25 });
26});

Next Steps

Error Handling

Implement robust error handling with retries

Rate Limits

Understand API rate limits

Best Practices

Learn integration best practices

Webhooks

Set up webhook event handling