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