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

# Best Practices

> Build secure, reliable, and performant integrations with Deel APIs

## Overview

Follow these best practices to build production-ready integrations that are secure, reliable, and performant.

* **Security:** Protect credentials and sensitive data.
* **Error Handling:** Handle failures gracefully with retries.
* **Performance:** Optimize API usage and respect rate limits.
* **Reliability:** Build resilient integrations that handle edge cases.

## Authentication & Security

### Credential management

#### Never hardcode credentials

**Don't do this:**

```javascript
const apiKey = "deel_live_abc123"; // ❌ Never hardcode
```

**Do this instead:**

```javascript
const apiKey = process.env.DEEL_API_KEY; // ✅ Use environment variables
```

* Use environment variables or secure vault services
* Never commit credentials to version control
* Add `.env` to your `.gitignore` file

#### Rotate credentials regularly

**Recommended rotation schedule:**

* API tokens: Every 90 days
* OAuth2 access tokens: Refresh proactively (every 25 days)
* Immediately rotate if compromise is suspected

**Why rotate?**

* Employees leave
* Credentials can be accidentally exposed
* Security vulnerabilities can be discovered

#### Use the least privilege principle

Only request the scopes and permissions you absolutely need:

**Too broad:**

```javascript
// Requesting all possible scopes ❌
scopes: "contracts:read contracts:write people:read people:write ..."
```

**Appropriate:**

```javascript
// Only what you need ✅
scopes: "contracts:read" // Just reading contracts
```

* Create separate tokens for different integrations
* Review and remove unused scopes
* Use organization tokens only when necessary

#### Always use HTTPS

All API requests must use HTTPS:

**Incorrect:**

```
http://api.letsdeel.com/rest/contracts ❌
```

**Correct:**

```
https://api.letsdeel.com/rest/contracts ✅
```

* HTTPS encrypts data in transit
* Protects against man-in-the-middle attacks
* Required by Deel API (HTTP requests will fail)

### Webhook security

When receiving webhooks, always verify the signature:

**`Node.js`**

```javascript Node.js
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const hash = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(hash)
  );
}

// In your webhook handler
app.post('/webhooks/deel', (req, res) => {
  const signature = req.headers['x-deel-signature'];
  const payload = JSON.stringify(req.body);

  if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  // Process webhook...
});
```

**`Python`**

```python Python
import hmac
import hashlib

def verify_webhook_signature(payload: str, signature: str, secret: str) -> bool:
    expected_signature = hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_signature)

# In your webhook handler
@app.route('/webhooks/deel', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Deel-Signature')
    payload = request.get_data(as_text=True)

    if not verify_webhook_signature(payload, signature, os.getenv('WEBHOOK_SECRET')):
        return jsonify({'error': 'Invalid signature'}), 401

    # Process webhook...
```

## Error Handling & Retries

> **Warning**
>
> **A 5xx response does not mean the request failed to execute.** The server may have applied a `POST` or `PATCH` before the error occurred, so blindly retrying it can create duplicate resources (for example, a second contract). Only retry 5xx responses unconditionally on `GET` and `DELETE`. For `POST` and `PATCH`, retry a 5xx only if the request carried an [`Idempotency-Key`](/api/idempotency); otherwise, surface the error instead of retrying automatically. `429` responses were never processed, so they're always safe to retry regardless of method.

### Implement exponential backoff

Retry failed requests with exponential backoff, but only when it's safe to do so:

**`Node.js`**

```javascript Node.js
async function makeRequestWithRetry(url, options, maxRetries = 3) {
  const method = (options.method || 'GET').toUpperCase();
  const hasIdempotencyKey = Boolean(options.headers?.['Idempotency-Key']);
  // A 5xx may have already executed server-side, so only retry it for
  // naturally safe methods or POST/PATCH requests carrying an Idempotency-Key.
  const canRetry5xx = ['GET', 'DELETE'].includes(method) || hasIdempotencyKey;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      // Success
      if (response.ok) {
        return await response.json();
      }

      // Don't retry client errors (4xx except 429)
      if (response.status >= 400 && response.status < 500 && response.status !== 429) {
        throw new Error(`Client error: ${response.status}`);
      }

      // 429 was never processed, so it's always safe to retry.
      const shouldRetry = response.status === 429 || canRetry5xx;

      if (shouldRetry && attempt < maxRetries - 1) {
        const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }

      throw new Error(`Request failed with ${response.status}${shouldRetry ? ` after ${attempt + 1} attempts` : ' and is not safe to retry automatically'}`);

    } catch (error) {
      // Network errors are as ambiguous as a 5xx: the request may have
      // reached the server before the connection dropped.
      if (!canRetry5xx || attempt === maxRetries - 1) throw error;

      const delay = Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}
```

**`Python`**

```python Python
import time
import uuid
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def create_session_with_retries():
    # GET/DELETE never mutate state, so replaying them after a 5xx can't
    # create duplicates. Safe to retry automatically at the session level.
    session = requests.Session()

    retry_strategy = Retry(
        total=3,
        backoff_factor=1,  # 1s, 2s, 4s
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["GET", "DELETE"]
    )

    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("https://", adapter)

    return session

def post_with_manual_retry(session, url, json_body, headers, max_retries=3):
    # POST/PATCH need a manual retry loop: a 5xx doesn't guarantee the write
    # didn't already apply, so only retry when the caller has attached an
    # Idempotency-Key tying the retry to the original attempt.
    for attempt in range(max_retries):
        response = session.post(url, json=json_body, headers=headers)
        if response.status_code < 500:
            return response
        if attempt < max_retries - 1:
            time.sleep(2 ** attempt)  # 1s, 2s, 4s
    return response

# Usage
session = create_session_with_retries()

# Safe: GET retries automatically via the session's Retry strategy
response = session.get(
    'https://api.letsdeel.com/rest/contracts',
    headers={'Authorization': f'Bearer {api_key}'}
)

# Unsafe to auto-retry without a key: attach an Idempotency-Key and use the
# manual retry helper instead of the session's automatic retries
response = post_with_manual_retry(
    session,
    'https://api.letsdeel.com/rest/contracts',
    json_body={'worker_email': 'jane.doe@example.com'},
    headers={
        'Authorization': f'Bearer {api_key}',
        'Idempotency-Key': str(uuid.uuid4())
    }
)
```

### Handle specific error codes

Different errors require different handling strategies:

| Status Code | Meaning             | Recommended Action                                                                                              |
| ----------- | ------------------- | --------------------------------------------------------------------------------------------------------------- |
| `400`       | Bad Request         | Fix request parameters, don't retry                                                                             |
| `401`       | Unauthorized        | Check/refresh token, retry once                                                                                 |
| `403`       | Forbidden           | Check scopes, don't retry                                                                                       |
| `404`       | Not Found           | Resource doesn't exist, don't retry                                                                             |
| `429`       | Rate Limited        | Wait and retry with exponential backoff (safe for all methods)                                                  |
| `500`       | Server Error        | Retry with backoff on `GET`/`DELETE`. On `POST`/`PATCH`, only retry if the request carried an `Idempotency-Key` |
| `503`       | Service Unavailable | Retry with backoff on `GET`/`DELETE`. On `POST`/`PATCH`, only retry if the request carried an `Idempotency-Key` |

### Validate request data

Always validate data before sending to the API:

```javascript
function validateContractData(data) {
  const errors = [];

  if (!data.client_id) {
    errors.push('client_id is required');
  }

  if (!data.worker_email || !isValidEmail(data.worker_email)) {
    errors.push('Valid worker_email is required');
  }

  if (data.rate && (data.rate <= 0 || data.rate > 10000)) {
    errors.push('Rate must be between 0 and 10000');
  }

  if (errors.length > 0) {
    throw new ValidationError(errors.join(', '));
  }

  return true;
}

// Use before making API call
try {
  validateContractData(contractData);
  const response = await createContract(contractData);
} catch (error) {
  if (error instanceof ValidationError) {
    // Handle validation error (don't send to API)
  }
}
```

## Rate Limiting

Deel enforces a rate limit of **5 requests per second per organization**. This limit is shared across all API tokens in your organization.

> **Note**
>
> **Important**: Rate limits are organization-wide and Deel does not return rate limit headers. Proactive rate limiting through request queuing is essential.

### Key strategies

* **Request Queuing:** Always implement request queuing to stay within limits.
* **Space Out Requests:** Avoid bursts—spread requests over time.
* **Cache Responses:** Reduce unnecessary API calls.
* **Centralize Requests:** Coordinate all API calls across your organization.

**Quick example:**

```javascript
// Queue requests to respect rate limits
const queue = new RateLimitedQueue(5); // 5 requests per second

const result = await queue.add(() => deelAPI.get('/contracts'));
```

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

See the complete [Rate Limits documentation](/api/rate-limits) for detailed strategies, code examples, and troubleshooting.

## Idempotency

Use idempotency keys for POST and PATCH requests to safely retry without creating duplicates.

> **Note**
>
> Idempotency keys prevent duplicate resources when retrying failed requests. Responses are cached for 24 hours.

**Quick example:**

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

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

  return await deelAPI.post('/contracts', contractData, {
    headers: {
      'Idempotency-Key': idempotencyKey
    }
  });
}
```

**Key points:**

* Use UUID v4 for idempotency keys
* Reuse the same key when retrying
* Only successful responses (2xx) are cached
* Keys are valid for 24 hours

#### [Learn More About Idempotency](/api/idempotency)

See the complete [Idempotency documentation](/api/idempotency) for detailed implementation, scenarios, and best practices.

## Data Handling

### Sanitize and validate input

#### Validate user input

Always validate and sanitize data from users:

```javascript
function sanitizeEmail(email) {
  return email.trim().toLowerCase();
}

function validateContractInput(input) {
  return {
    worker_email: sanitizeEmail(input.worker_email),
    job_title: input.job_title.trim().substring(0, 100), // Limit length
    rate: Math.max(0, Number(input.rate)), // Ensure positive number
    // ... other fields
  };
}
```

#### Handle timezone conversions

Always use UTC for dates and times:

```javascript
// Store dates in UTC
const startDate = new Date().toISOString();

// When displaying to users, convert to their timezone
const userTimezone = 'America/New_York';
const displayDate = new Intl.DateTimeFormat('en-US', {
  timeZone: userTimezone,
  dateStyle: 'full',
  timeStyle: 'long'
}).format(new Date(startDate));
```

#### Handle pagination efficiently

For large datasets, use pagination properly:

```javascript
async function getAllContracts() {
  let allContracts = [];
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const response = await deelAPI.get('/contracts', {
      params: {
        page,
        limit: 100 // Max page size
      }
    });

    allContracts = allContracts.concat(response.data);
    hasMore = response.data.length === 100;
    page++;

    // Add small delay to avoid rate limits
    if (hasMore) await sleep(100);
  }

  return allContracts;
}
```

## Testing

### Test in sandbox first

#### Use sandbox for development

Always develop and test against the sandbox environment:

```javascript
const baseURL = process.env.NODE_ENV === 'production'
  ? 'https://api.letsdeel.com/rest'
  : 'https://api-staging.letsdeel.com/rest';
```

#### Test error scenarios

Don't just test happy paths. Test:

* Invalid authentication
* Missing required fields
* Rate limit handling
* Network failures
* Webhook signature verification

#### Validate with production-like data

Use realistic test data that mirrors your production use case:

* Multiple countries and currencies
* Different contract types (EOR, IC, GP)
* Edge cases (long names, special characters)

#### Gradual production rollout

When moving to production:

* Start with a small subset of users/data
* Monitor error rates and performance
* Gradually increase usage
* Keep sandbox testing environment available

## Monitoring & Logging

### Log important events

Implement structured logging for debugging and monitoring:

```javascript
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// Log API calls
logger.info('API Request', {
  method: 'POST',
  endpoint: '/contracts',
  requestId: req.id,
  timestamp: new Date().toISOString()
});

// Log errors with context
logger.error('API Error', {
  error: error.message,
  endpoint: '/contracts',
  statusCode: error.response?.status,
  requestId: req.id
});
```

**What to log:**

* API request/response metadata (not full bodies with sensitive data)
* Error conditions and stack traces
* Authentication failures
* Rate limit warnings
* Webhook deliveries

**What NOT to log:**

* API keys or tokens
* Sensitive personal data
* Full request/response bodies (unless sanitized)

### Set up alerts

Monitor your integration health:

* **Error Rate Alerts:** Alert when error rate exceeds threshold (e.g., >5% of requests)
* **Rate Limit Warnings:** Alert when approaching rate limits (e.g., 80% usage)
* **Webhook Failures:** Alert on webhook delivery failures or signature mismatches
* **Response Time:** Alert on slow API responses (e.g., >2 seconds average)

## Performance Optimization

#### Use connection pooling

Reuse HTTP connections for better performance:

```javascript
const axios = require('axios');
const http = require('http');
const https = require('https');

const deelAPI = axios.create({
  baseURL: 'https://api.letsdeel.com/rest',
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true })
});
```

#### Implement request timeouts

Always set reasonable timeouts:

```javascript
const response = await deelAPI.get('/contracts', {
  timeout: 10000 // 10 second timeout
});
```

#### Minimize payload size

Only request the fields you need:

```javascript
// If API supports field selection
const response = await deelAPI.get('/contracts', {
  params: {
    fields: 'id,status,worker_name,start_date' // Only needed fields
  }
});
```

#### Use async/concurrent requests

When fetching multiple independent resources:

```javascript
// ❌ Sequential (slow)
const contract = await getContract(contractId);
const worker = await getWorker(workerId);
const invoices = await getInvoices(contractId);

// ✅ Concurrent (fast)
const [contract, worker, invoices] = await Promise.all([
  getContract(contractId),
  getWorker(workerId),
  getInvoices(contractId)
]);
```

## Compliance & Privacy

#### Handle PII appropriately

Personal Identifiable Information (PII) requires special care:

* Only collect necessary PII
* Encrypt PII at rest and in transit
* Follow data retention policies
* Implement right to deletion
* Document data flows

#### Comply with data regulations

Ensure compliance with relevant regulations:

* **GDPR** (Europe): Data protection and privacy
* **CCPA** (California): Consumer privacy rights
* **SOC 2**: Information security standards
* Industry-specific regulations

#### Audit trail

Maintain audit logs for compliance:

```javascript
function logAuditEvent(event) {
  auditLogger.info({
    timestamp: new Date().toISOString(),
    userId: event.userId,
    action: event.action, // 'contract_created', 'data_accessed', etc.
    resourceId: event.resourceId,
    ipAddress: event.ipAddress,
    userAgent: event.userAgent
  });
}
```

## Summary Checklist

Before deploying to production, ensure you've implemented:

#### Security Checklist

* [ ] API keys stored in environment variables
* [ ] HTTPS used for all requests
* [ ] Webhook signatures verified
* [ ] Least privilege scopes requested
* [ ] Credential rotation schedule in place
* [ ] Secrets never committed to version control

#### Reliability Checklist

* [ ] Exponential backoff retry logic
* [ ] Idempotency keys used for mutations
* [ ] Error handling for all API calls
* [ ] Request timeouts configured
* [ ] Input validation implemented
* [ ] Edge cases tested

#### Performance Checklist

* [ ] Rate limits respected
* [ ] Request queuing implemented
* [ ] Appropriate caching strategy
* [ ] Connection pooling enabled
* [ ] Pagination handled efficiently
* [ ] Concurrent requests where possible

#### Monitoring Checklist

* [ ] Structured logging implemented
* [ ] Error tracking configured
* [ ] Rate limit monitoring
* [ ] Alerts set up for failures
* [ ] Webhook delivery monitoring
* [ ] Performance metrics tracked

## Next Steps

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

Set up webhook integrations