Sync Deel users to identity providers

Overview

This guide demonstrates how to automatically sync user accounts from Deel HRIS to identity providers like Okta, Azure AD, or OneLogin using the SCIM 2.0 API. You will learn how to provision identity provider accounts when employees are hired in Deel, update their information when it changes, and de-provision them when employment ends.

This workflow enables Deel to be your single source of truth for employee data, automatically granting access to corporate systems and SSO applications based on Deel employment status.

When to use this workflow

Use this workflow when you need to:

  • Use Deel as the primary source of truth for employee data
  • Automatically provision SSO accounts when employees are hired in Deel
  • Grant access to corporate systems based on Deel employment status
  • Keep identity provider user profiles synchronized with Deel HRIS data
  • Automatically revoke access when contracts end in Deel
  • Sync both employees and contractors to your identity provider
  • Maintain consistent user data across your organization

Prerequisites

Before you begin, ensure you have:

  • A Deel organization account with SCIM API access
  • An identity provider (Okta, Azure AD, OneLogin, etc.) with SCIM or API support
  • A valid Deel API token with Users:read scope
  • API credentials for your identity provider with user provisioning permissions
  • A middleware service or scheduled job to orchestrate the synchronization
  • Administrative access to both systems for configuration

This workflow requires a middleware service to fetch users from Deel and push them to your identity provider. Direct SCIM provisioning from Deel to identity providers is not natively supported and must be implemented via API integration.

Step-by-step workflow

This example demonstrates syncing users from Deel HRIS to Okta, but the same principles apply to Azure AD, OneLogin, and other identity providers with provisioning APIs.

1

Fetch users from Deel HRIS

Query Deel SCIM API to retrieve all active users in your organization.

1import requests
2import os
3from typing import List, Dict
4
5DEEL_SCIM_BASE_URL = "https://api.letsdeel.com/scim/v2"
6DEEL_API_TOKEN = os.getenv("DEEL_API_TOKEN")
7
8def fetch_all_deel_users() -> List[Dict]:
9 """Fetch all users from Deel HRIS with pagination."""
10 all_users = []
11 start_index = 1
12 page_size = 100
13
14 while True:
15 response = requests.get(
16 f"{DEEL_SCIM_BASE_URL}/Users",
17 headers={
18 "Authorization": f"Bearer {DEEL_API_TOKEN}",
19 "Content-Type": "application/scim+json"
20 },
21 params={
22 "startIndex": start_index,
23 "count": page_size
24 }
25 )
26
27 if response.status_code != 200:
28 raise Exception(f"Failed to fetch users: {response.text}")
29
30 data = response.json()
31 users = data.get("Resources", [])
32 all_users.extend(users)
33
34 total_results = data.get("totalResults", 0)
35 if start_index + page_size > total_results:
36 break
37
38 start_index += page_size
39
40 return all_users
41
42# Fetch all users
43deel_users = fetch_all_deel_users()
44print(f"Retrieved {len(deel_users)} users from Deel HRIS")

Example response from Deel:

1{
2 "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
3 "Resources": [
4 {
5 "id": "f8e7d6c5-4b3a-2910-8765-fedcba098765",
6 "userName": "sarah.chen@techcorp.com",
7 "name": {
8 "givenName": "Sarah",
9 "familyName": "Chen"
10 },
11 "title": "Senior Product Manager",
12 "active": true,
13 "emails": [
14 {
15 "value": "sarah.chen@techcorp.com",
16 "type": "work",
17 "primary": true
18 }
19 ],
20 "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
21 "department": "Product",
22 "manager": {
23 "value": "b29a5ff0-bdb5-11ed-afa1-0242ac120002",
24 "displayName": "Michael Torres"
25 }
26 },
27 "urn:ietf:params:scim:schemas:extension:2.0:User": {
28 "startDate": "2024-03-15T00:00:00Z",
29 "hiringStatus": "active"
30 }
31 }
32 ],
33 "totalResults": 1,
34 "startIndex": 1,
35 "itemsPerPage": 100
36}
2

Transform Deel user data

Map Deel SCIM attributes to your identity provider’s user schema.

1def transform_deel_to_okta(deel_user: Dict) -> Dict:
2 """Transform Deel SCIM user to Okta user format."""
3 enterprise_ext = deel_user.get(
4 "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {}
5 )
6 deel_ext = deel_user.get(
7 "urn:ietf:params:scim:schemas:extension:2.0:User", {}
8 )
9
10 okta_user = {
11 "profile": {
12 "email": deel_user["userName"],
13 "login": deel_user["userName"],
14 "firstName": deel_user["name"]["givenName"],
15 "lastName": deel_user["name"]["familyName"],
16 "title": deel_user.get("title", ""),
17 "department": enterprise_ext.get("department", ""),
18 "organization": enterprise_ext.get("organization", ""),
19 "employeeNumber": deel_user.get("id"),
20 "primaryPhone": deel_user.get("phoneNumbers", [{}])[0].get("value", ""),
21 },
22 "groupIds": [] # Assign based on department or role
23 }
24
25 # Map department to Okta groups
26 department = enterprise_ext.get("department", "")
27 if department:
28 okta_user["groupIds"].append(f"dept-{department.lower().replace(' ', '-')}")
29
30 return okta_user
31
32# Transform Deel user
33okta_user_data = transform_deel_to_okta(deel_users[0])

Store a mapping between Deel user IDs and identity provider user IDs in a database to efficiently track which users have already been provisioned and need updates versus new provisioning.

3

Provision users in identity provider

Create accounts in your identity provider for users found in Deel HRIS.

Okta provisioning:

1import requests
2import os
3
4OKTA_DOMAIN = "https://dev-12345.okta.com"
5OKTA_API_TOKEN = os.getenv("OKTA_API_TOKEN")
6
7def provision_user_in_okta(okta_user: Dict) -> Dict:
8 """Create a user in Okta."""
9 response = requests.post(
10 f"{OKTA_DOMAIN}/api/v1/users",
11 headers={
12 "Authorization": f"SSWS {OKTA_API_TOKEN}",
13 "Content-Type": "application/json"
14 },
15 params={
16 "activate": True # Automatically activate the user
17 },
18 json=okta_user
19 )
20
21 if response.status_code == 200:
22 return {
23 "success": True,
24 "okta_user_id": response.json()["id"],
25 "email": okta_user["profile"]["email"]
26 }
27 elif response.status_code == 400 and "login" in response.text:
28 # User already exists
29 return {
30 "success": True,
31 "status": "already_exists",
32 "email": okta_user["profile"]["email"]
33 }
34 else:
35 return {
36 "success": False,
37 "error": response.text,
38 "email": okta_user["profile"]["email"]
39 }
40
41# Provision the user
42result = provision_user_in_okta(okta_user_data)
43print(f"Provisioned user in Okta: {result}")

Azure AD provisioning:

1import requests
2import os
3
4AZURE_TENANT_ID = os.getenv("AZURE_TENANT_ID")
5AZURE_CLIENT_ID = os.getenv("AZURE_CLIENT_ID")
6AZURE_CLIENT_SECRET = os.getenv("AZURE_CLIENT_SECRET")
7
8def get_azure_access_token() -> str:
9 response = requests.post(
10 f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/oauth2/v2.0/token",
11 data={
12 "client_id": AZURE_CLIENT_ID,
13 "client_secret": AZURE_CLIENT_SECRET,
14 "scope": "https://graph.microsoft.com/.default",
15 "grant_type": "client_credentials"
16 }
17 )
18 return response.json()["access_token"]
19
20def provision_user_in_azure_ad(deel_user: dict) -> dict:
21 access_token = get_azure_access_token()
22 azure_user = {
23 "accountEnabled": deel_user["active"],
24 "displayName": f"{deel_user['name']['givenName']} {deel_user['name']['familyName']}",
25 "mailNickname": deel_user["userName"].split("@")[0],
26 "userPrincipalName": deel_user["userName"],
27 "passwordProfile": {"forceChangePasswordNextSignIn": True, "password": "TempPassword123!"},
28 "givenName": deel_user["name"]["givenName"],
29 "surname": deel_user["name"]["familyName"],
30 "jobTitle": deel_user.get("title", ""),
31 "department": deel_user.get("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {}).get("department", "")
32 }
33 response = requests.post(
34 "https://graph.microsoft.com/v1.0/users",
35 headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
36 json=azure_user
37 )
38 if response.status_code == 201:
39 return {"success": True, "azure_user_id": response.json()["id"]}
40 return {"success": False, "error": response.text}

The user account is now created in your identity provider with access to SSO applications.

4

Sync user updates from Deel

When employee information changes in Deel, update their identity provider profile.

Scenario: Employee receives a promotion in Deel

1def detect_user_changes(deel_user: Dict, stored_user: Dict) -> List[str]:
2 """Detect which fields changed between Deel and stored state."""
3 changes = []
4
5 if deel_user.get("title") != stored_user.get("title"):
6 changes.append("title")
7
8 enterprise_deel = deel_user.get("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {})
9 enterprise_stored = stored_user.get("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {})
10
11 if enterprise_deel.get("department") != enterprise_stored.get("department"):
12 changes.append("department")
13
14 if enterprise_deel.get("manager", {}).get("value") != enterprise_stored.get("manager", {}).get("value"):
15 changes.append("manager")
16
17 return changes
18
19def update_okta_user(okta_user_id: str, updates: Dict) -> Dict:
20 """Update user profile in Okta."""
21 response = requests.post(
22 f"{OKTA_DOMAIN}/api/v1/users/{okta_user_id}",
23 headers={
24 "Authorization": f"SSWS {OKTA_API_TOKEN}",
25 "Content-Type": "application/json"
26 },
27 json={"profile": updates}
28 )
29
30 if response.status_code == 200:
31 return {"success": True, "updated_fields": list(updates.keys())}
32 else:
33 return {"success": False, "error": response.text}
34
35# Detect changes
36current_deel_user = fetch_deel_user_by_id("f8e7d6c5-4b3a-2910-8765-fedcba098765")
37changes = detect_user_changes(current_deel_user, stored_snapshot)
38
39if "title" in changes or "department" in changes:
40 # Update Okta profile
41 updates = {
42 "title": current_deel_user.get("title"),
43 "department": current_deel_user.get("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {}).get("department")
44 }
45 result = update_okta_user(okta_user_id, updates)
46 print(f"Updated Okta user: {result}")

The identity provider profile is updated to reflect the employee’s new role and department.

5

Handle contract terminations

When employment ends in Deel (contract terminated, offboarded), deactivate or remove the user from your identity provider.

Option 1: Deactivate user (recommended)

1def deactivate_okta_user(okta_user_id: str) -> Dict:
2 """Deactivate a user in Okta."""
3 response = requests.post(
4 f"{OKTA_DOMAIN}/api/v1/users/{okta_user_id}/lifecycle/deactivate",
5 headers={
6 "Authorization": f"SSWS {OKTA_API_TOKEN}"
7 }
8 )
9
10 if response.status_code == 200:
11 return {"success": True, "status": "deactivated"}
12 else:
13 return {"success": False, "error": response.text}
14
15# Check if Deel user is inactive
16deel_user = fetch_deel_user_by_id("f8e7d6c5-4b3a-2910-8765-fedcba098765")
17
18if not deel_user.get("active"):
19 # Deactivate in Okta
20 result = deactivate_okta_user(okta_user_id)
21 print(f"Deactivated Okta user: {result}")

Option 2: Delete user (Azure AD)

1def delete_azure_ad_user(azure_user_id: str) -> dict:
2 access_token = get_azure_access_token()
3 response = requests.delete(
4 f"https://graph.microsoft.com/v1.0/users/{azure_user_id}",
5 headers={"Authorization": f"Bearer {access_token}"}
6 )
7 if response.status_code == 204:
8 return {"success": True, "status": "deleted"}
9 return {"success": False, "error": response.text}

Deleting users from identity providers removes their access to all applications and is irreversible. Deactivation is recommended to preserve audit trails and enable potential rehires.

6

Schedule regular synchronization

Set up automated syncs to keep Deel and your identity provider in sync continuously.

Cron job example (runs every 15 minutes):

1import schedule
2import time
3
4def sync_deel_to_idp():
5 print("Starting Deel → Identity Provider sync...")
6 deel_users = fetch_all_deel_users()
7 user_mappings = load_user_mappings_from_database()
8
9 for deel_user in deel_users:
10 deel_user_id = deel_user["id"]
11 email = deel_user["userName"]
12
13 if deel_user_id in user_mappings:
14 idp_user_id = user_mappings[deel_user_id]
15 stored_user = get_stored_user_snapshot(deel_user_id)
16 changes = detect_user_changes(deel_user, stored_user)
17 if changes:
18 update_okta_user(idp_user_id, transform_changes(deel_user, changes))
19 if not deel_user.get("active"):
20 deactivate_okta_user(idp_user_id)
21 else:
22 if deel_user.get("active"):
23 result = provision_user_in_okta(transform_deel_to_okta(deel_user))
24 if result["success"]:
25 save_user_mapping(deel_user_id, result["okta_user_id"])
26
27 save_user_snapshot(deel_user_id, deel_user)
28
29 print("Sync completed successfully")
30
31schedule.every(15).minutes.do(sync_deel_to_idp)
32
33while True:
34 schedule.run_pending()
35 time.sleep(60)

This automated job ensures your identity provider stays synchronized with Deel HRIS data.

Common scenarios

Scenario 1: Syncing only specific contract types

Filter Deel users by employment type (EOR employees, contractors, etc.) before provisioning.

1def filter_users_by_contract_type(deel_users: list, contract_types: list) -> list:
2 filtered_users = []
3 for user in deel_users:
4 deel_ext = user.get("urn:ietf:params:scim:schemas:extension:2.0:User", {})
5 employments = deel_ext.get("employments", [])
6 for employment in employments:
7 if employment.get("contractType") in contract_types and employment.get("active"):
8 filtered_users.append(user)
9 break
10 return filtered_users
11
12deel_users = fetch_all_deel_users()
13eor_employees = filter_users_by_contract_type(deel_users, ["eor"])
14
15for employee in eor_employees:
16 provision_user_in_okta(transform_deel_to_okta(employee))
17
18print(f"Provisioned {len(eor_employees)} EOR employees")

This allows selective provisioning based on employment classification.

Scenario 2: Mapping Deel departments to IdP groups

Automatically assign users to identity provider groups based on their Deel department.

1import os
2
3OKTA_DOMAIN = os.getenv("OKTA_DOMAIN")
4OKTA_API_TOKEN = os.getenv("OKTA_API_TOKEN")
5
6DEPARTMENT_GROUP_MAPPING = {
7 "Engineering": ["00g1a2b3c4d5e6f7g8", "00g9h8i7j6k5l4m3n2"],
8 "Product": ["00g2a3b4c5d6e7f8g9", "00g9h8i7j6k5l4m3n2"],
9 "Sales": ["00g3a4b5c6d7e8f9g0", "00g9h8i7j6k5l4m3n2"],
10 "Design": ["00g4a5b6c7d8e9f0g1", "00g9h8i7j6k5l4m3n2"]
11}
12ALL_EMPLOYEES_GROUP = "00g9h8i7j6k5l4m3n2"
13
14def assign_user_to_groups(okta_user_id: str, group_ids: list):
15 for group_id in group_ids:
16 requests.put(
17 f"{OKTA_DOMAIN}/api/v1/groups/{group_id}/users/{okta_user_id}",
18 headers={"Authorization": f"SSWS {OKTA_API_TOKEN}"}
19 )
20
21deel_user = fetch_deel_user_by_id("f8e7d6c5-4b3a-2910-8765-fedcba098765")
22result = provision_user_in_okta(transform_deel_to_okta(deel_user))
23
24department = deel_user.get("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {}).get("department")
25group_ids = DEPARTMENT_GROUP_MAPPING.get(department, [ALL_EMPLOYEES_GROUP])
26assign_user_to_groups(result["okta_user_id"], group_ids)

Users automatically receive group-based access to applications based on their department.

Scenario 3: Webhook-driven real-time sync

Use Deel webhooks to trigger immediate synchronization when user data changes.

1from flask import Flask, request, jsonify
2
3app = Flask(__name__)
4
5@app.route("/webhooks/deel", methods=["POST"])
6def deel_webhook_handler():
7 event = request.json
8 event_type = event.get("type")
9 user_data = event.get("data", {})
10
11 if event_type == "user.created":
12 deel_user = fetch_deel_user_by_id(user_data.get("id"))
13 result = provision_user_in_okta(transform_deel_to_okta(deel_user))
14 return jsonify({"status": "provisioned", "result": result})
15
16 elif event_type == "user.updated":
17 deel_user_id = user_data.get("id")
18 deel_user = fetch_deel_user_by_id(deel_user_id)
19 idp_user_id = get_idp_user_id_from_mapping(deel_user_id)
20 stored_user = get_stored_user_snapshot(deel_user_id)
21 changes = detect_user_changes(deel_user, stored_user)
22 if changes:
23 update_okta_user(idp_user_id, transform_changes(deel_user, changes))
24 save_user_snapshot(deel_user_id, deel_user)
25 return jsonify({"status": "updated", "changes": changes})
26
27 elif event_type == "user.deactivated":
28 deel_user_id = user_data.get("id")
29 deactivate_okta_user(get_idp_user_id_from_mapping(deel_user_id))
30 return jsonify({"status": "deactivated"})
31
32 return jsonify({"status": "ignored"})
33
34if __name__ == "__main__":
35 app.run(port=8080)

This enables real-time synchronization triggered by Deel events instead of polling.

Scenario 4: Handling manager hierarchies

Sync reporting relationships from Deel to identity provider org charts.

1def sync_manager_relationship(deel_user: dict, okta_user_id: str):
2 enterprise_ext = deel_user.get("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {})
3 manager_info = enterprise_ext.get("manager", {})
4 if not manager_info:
5 return
6 manager_okta_id = get_idp_user_id_from_mapping(manager_info.get("value"))
7 if manager_okta_id:
8 update_okta_user(okta_user_id, {
9 "managerId": manager_okta_id,
10 "manager": manager_info.get("displayName")
11 })
12
13for deel_user in deel_users:
14 okta_user_id = get_idp_user_id_from_mapping(deel_user["id"])
15 sync_manager_relationship(deel_user, okta_user_id)

Organizational hierarchies from Deel are replicated in your identity provider.

Best practices

  • Use a database: Store mappings between Deel user IDs and identity provider user IDs
  • Track sync state: Record last sync timestamps for each user
  • Store user snapshots: Keep previous versions of user data to detect changes efficiently
  • Handle mapping failures: Log users that fail to map and retry later
  • Clean up orphaned records: Remove mappings for users deleted from both systems
  • Backup mappings regularly: Ensure you can restore synchronization state if needed
  • Check before creating: Query identity provider before provisioning to avoid duplicates
  • Use upsert patterns: Update if exists, create if not
  • Handle 409 conflicts: If user already exists, treat as success and update instead
  • Track operation results: Log whether operation was create, update, or skip
  • Retry safely: Ensure retries don’t create duplicate users or trigger errors
  • Validate email uniqueness: Confirm emails are unique before provisioning
  • Use webhooks when possible: Real-time sync via Deel webhooks for immediate updates
  • Schedule regular polls: Run full sync every 15-30 minutes as backup
  • Incremental updates: Only sync users who changed since last sync
  • Off-peak full syncs: Schedule comprehensive syncs during low-traffic hours
  • Rate limit awareness: Throttle requests to respect identity provider API limits
  • Monitor sync duration: Track how long syncs take and optimize if needed
  • Retry transient failures: Implement exponential backoff for network errors
  • Log all errors: Track which users failed to sync and why
  • Alert on repeated failures: Notify administrators of persistent sync issues
  • Continue on individual failures: Don’t stop entire sync if one user fails
  • Validate data before sending: Check required fields are present
  • Handle API changes: Monitor for identity provider API version updates
  • Rotate API tokens: Update Deel and identity provider tokens every 90 days
  • Use dedicated service accounts: Create specific accounts for sync operations
  • Encrypt stored credentials: Secure API tokens in environment variables or vaults
  • Limit token scopes: Grant only Users:read in Deel and user provisioning in IdP
  • Audit sync operations: Log all user provisioning, updates, and deactivations
  • Monitor for unauthorized access: Track API usage patterns for anomalies
  • Validate email formats: Ensure emails are valid before provisioning
  • Check required fields: Confirm firstName, lastName, email are present
  • Normalize data: Standardize department names and titles before mapping
  • Handle missing data: Provide defaults for optional fields
  • Verify after operations: Query identity provider after provisioning to confirm success
  • Run reconciliation reports: Periodically compare Deel and IdP user lists

Troubleshooting

When provisioning a Deel user, the identity provider returns a conflict error indicating the user already exists.

Solution:

  1. Query the identity provider by email to find the existing user
  2. Store the mapping between Deel user ID and identity provider user ID
  3. Update the existing user profile instead of creating a new one
  4. Mark the operation as successful since the desired state is achieved

Prevention: Before provisioning, check if a user with the same email already exists in the identity provider.

Fetching users from Deel SCIM API returns 429 errors due to rate limiting.

Solution:

  1. Implement exponential backoff: wait increasing intervals between retries
  2. Reduce pagination page size from 100 to 50 users per request
  3. Increase time between sync runs (e.g., from 10 to 20 minutes)
  4. Use webhooks instead of polling for real-time updates
  5. Cache Deel user data and only fetch changes since last sync

Best practice: Fetch users during off-peak hours and use incremental syncs based on meta.lastModified timestamps.

Requests to provision or update users in the identity provider fail with 401 Unauthorized errors.

Solution:

  1. Verify API token is correct and not expired (Okta SSWS tokens)
  2. For Azure AD, confirm OAuth2 client credentials are valid and token is refreshed
  3. Check token has required scopes/permissions for user provisioning
  4. Validate token format: SSWS {token} for Okta, Bearer {token} for Azure AD
  5. Test authentication separately before running full sync

Common causes:

  • Expired tokens (Azure AD tokens expire after 1 hour)
  • Incorrect token type (using personal token instead of service token)
  • Insufficient permissions granted to the API token

Manager assignments from Deel are not appearing in the identity provider org chart.

Solution:

  1. Ensure managers are provisioned in the identity provider before assigning them to employees
  2. Sync users in hierarchical order: executives → managers → employees
  3. Store mappings of Deel manager IDs to identity provider user IDs
  4. Run a second pass after initial provisioning to set manager relationships
  5. Handle cases where manager is not found gracefully (skip or log warning)

Example workflow:

  • Pass 1: Provision all users without manager assignments
  • Pass 2: Update each user with their manager reference

Users are provisioned but not automatically added to department-based groups in the identity provider.

Solution:

  1. Verify department values from Deel match your group mapping configuration
  2. Ensure identity provider groups exist before attempting assignment
  3. Call group assignment API after user provisioning succeeds
  4. Normalize department names (trim whitespace, handle case sensitivity)
  5. Provide a default group for users without valid department mappings

Debugging: Log the department value from Deel and the group IDs being assigned to identify mismatches.

Full synchronization of all users takes hours to complete, delaying updates.

Solution:

  1. Implement incremental sync: only process users modified since last sync
  2. Use Deel’s meta.lastModified field to filter recently changed users
  3. Run parallel processing for independent user provisioning operations
  4. Reduce page size and optimize database queries for user mapping lookups
  5. Switch from polling to webhook-driven sync for real-time updates

Optimization: For large organizations (1000+ users), use webhooks for changes and schedule full reconciliation weekly instead of every sync.

Next steps