> This page is for API, version 2026-01-01 (Stable) (default).
> For other versions, use one of these documentation indexes:
> - 2026-01-01 (Stable) (default): https://developer.deel.com/api/stable/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.deel.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.deel.com/_mcp/server.

# Sync Deel users to identity providers

> Automate user provisioning from Deel HRIS to Okta, Azure AD, and other identity providers using SCIM

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

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

### Fetch users from Deel HRIS

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

**`Python`**

```python Python
import requests
import os
from typing import List, Dict

DEEL_SCIM_BASE_URL = "https://api.letsdeel.com/scim"
DEEL_API_TOKEN = os.getenv("DEEL_API_TOKEN")

def fetch_all_deel_users() -> List[Dict]:
    """Fetch all users from Deel HRIS with pagination."""
    all_users = []
    start_index = 1
    page_size = 100

    while True:
        response = requests.get(
            f"{DEEL_SCIM_BASE_URL}/Users",
            headers={
                "Authorization": f"Bearer {DEEL_API_TOKEN}",
                "Content-Type": "application/scim+json"
            },
            params={
                "startIndex": start_index,
                "count": page_size
            }
        )

        if response.status_code != 200:
            raise Exception(f"Failed to fetch users: {response.text}")

        data = response.json()
        users = data.get("Resources", [])
        all_users.extend(users)

        total_results = data.get("totalResults", 0)
        if start_index + page_size > total_results:
            break

        start_index += page_size

    return all_users

# Fetch all users
deel_users = fetch_all_deel_users()
print(f"Retrieved {len(deel_users)} users from Deel HRIS")
```

**`Node.js`**

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

const DEEL_SCIM_BASE_URL = 'https://api.letsdeel.com/scim';
const DEEL_API_TOKEN = process.env.DEEL_API_TOKEN;

async function fetchAllDeelUsers() {
  const allUsers = [];
  let startIndex = 1;
  const pageSize = 100;

  while (true) {
    const response = await axios.get(
      `${DEEL_SCIM_BASE_URL}/Users`,
      {
        headers: {
          'Authorization': `Bearer ${DEEL_API_TOKEN}`,
          'Content-Type': 'application/scim+json'
        },
        params: {
          startIndex,
          count: pageSize
        }
      }
    );

    const users = response.data.Resources || [];
    allUsers.push(...users);

    const totalResults = response.data.totalResults || 0;
    if (startIndex + pageSize > totalResults) {
      break;
    }

    startIndex += pageSize;
  }

  return allUsers;
}

// Fetch all users
const deelUsers = await fetchAllDeelUsers();
console.log(`Retrieved ${deelUsers.length} users from Deel HRIS`);
```

**Example response from Deel**:

```json focus={4-5,9-12,20-23}
{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
  "Resources": [
    {
      "id": "f8e7d6c5-4b3a-2910-8765-fedcba098765",
      "userName": "sarah.chen@techcorp.com",
      "name": {
        "givenName": "Sarah",
        "familyName": "Chen"
      },
      "title": "Senior Product Manager",
      "active": true,
      "emails": [
        {
          "value": "sarah.chen@techcorp.com",
          "type": "work",
          "primary": true
        }
      ],
      "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
        "department": "Product",
        "manager": {
          "value": "b29a5ff0-bdb5-11ed-afa1-0242ac120002",
          "displayName": "Michael Torres"
        }
      },
      "urn:ietf:params:scim:schemas:extension:2.0:User": {
        "startDate": "2024-03-15T00:00:00Z",
        "hiringStatus": "active"
      }
    }
  ],
  "totalResults": 1,
  "startIndex": 1,
  "itemsPerPage": 100
}
```

### Transform Deel user data

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

**`Python`**

```python Python
def transform_deel_to_okta(deel_user: Dict) -> Dict:
    """Transform Deel SCIM user to Okta user format."""
    enterprise_ext = deel_user.get(
        "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {}
    )
    deel_ext = deel_user.get(
        "urn:ietf:params:scim:schemas:extension:2.0:User", {}
    )

    okta_user = {
        "profile": {
            "email": deel_user["userName"],
            "login": deel_user["userName"],
            "firstName": deel_user["name"]["givenName"],
            "lastName": deel_user["name"]["familyName"],
            "title": deel_user.get("title", ""),
            "department": enterprise_ext.get("department", ""),
            "organization": enterprise_ext.get("organization", ""),
            "employeeNumber": deel_user.get("id"),
            "primaryPhone": deel_user.get("phoneNumbers", [{}])[0].get("value", ""),
        },
        "groupIds": []  # Assign based on department or role
    }

    # Map department to Okta groups
    department = enterprise_ext.get("department", "")
    if department:
        okta_user["groupIds"].append(f"dept-{department.lower().replace(' ', '-')}")

    return okta_user

# Transform Deel user
okta_user_data = transform_deel_to_okta(deel_users[0])
```

**`Node.js`**

```javascript Node.js
function transformDeelToOkta(deelUser) {
  const enterpriseExt = deelUser['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'] || {};
  const deelExt = deelUser['urn:ietf:params:scim:schemas:extension:2.0:User'] || {};

  const oktaUser = {
    profile: {
      email: deelUser.userName,
      login: deelUser.userName,
      firstName: deelUser.name.givenName,
      lastName: deelUser.name.familyName,
      title: deelUser.title || '',
      department: enterpriseExt.department || '',
      organization: enterpriseExt.organization || '',
      employeeNumber: deelUser.id,
      primaryPhone: (deelUser.phoneNumbers || [])[0]?.value || ''
    },
    groupIds: []
  };

  // Map department to Okta groups
  const department = enterpriseExt.department;
  if (department) {
    oktaUser.groupIds.push(`dept-${department.toLowerCase().replace(/\s+/g, '-')}`);
  }

  return oktaUser;
}

// Transform Deel user
const oktaUserData = transformDeelToOkta(deelUsers[0]);
```

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

### Provision users in identity provider

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

**Okta provisioning**:

**`Python`**

```python Python
import requests
import os

OKTA_DOMAIN = "https://dev-12345.okta.com"
OKTA_API_TOKEN = os.getenv("OKTA_API_TOKEN")

def provision_user_in_okta(okta_user: Dict) -> Dict:
    """Create a user in Okta."""
    response = requests.post(
        f"{OKTA_DOMAIN}/api/v1/users",
        headers={
            "Authorization": f"SSWS {OKTA_API_TOKEN}",
            "Content-Type": "application/json"
        },
        params={
            "activate": True  # Automatically activate the user
        },
        json=okta_user
    )

    if response.status_code == 200:
        return {
            "success": True,
            "okta_user_id": response.json()["id"],
            "email": okta_user["profile"]["email"]
        }
    elif response.status_code == 400 and "login" in response.text:
        # User already exists
        return {
            "success": True,
            "status": "already_exists",
            "email": okta_user["profile"]["email"]
        }
    else:
        return {
            "success": False,
            "error": response.text,
            "email": okta_user["profile"]["email"]
        }

# Provision the user
result = provision_user_in_okta(okta_user_data)
print(f"Provisioned user in Okta: {result}")
```

**`Node.js`**

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

const OKTA_DOMAIN = 'https://dev-12345.okta.com';
const OKTA_API_TOKEN = process.env.OKTA_API_TOKEN;

async function provisionUserInOkta(oktaUser) {
  try {
    const response = await axios.post(
      `${OKTA_DOMAIN}/api/v1/users`,
      oktaUser,
      {
        headers: {
          'Authorization': `SSWS ${OKTA_API_TOKEN}`,
          'Content-Type': 'application/json'
        },
        params: {
          activate: true
        }
      }
    );

    return {
      success: true,
      oktaUserId: response.data.id,
      email: oktaUser.profile.email
    };
  } catch (error) {
    if (error.response?.status === 400 && error.response.data.errorCauses?.some(e => e.errorSummary.includes('login'))) {
      return {
        success: true,
        status: 'already_exists',
        email: oktaUser.profile.email
      };
    }

    return {
      success: false,
      error: error.message,
      email: oktaUser.profile.email
    };
  }
}

// Provision the user
const result = await provisionUserInOkta(oktaUserData);
console.log(`Provisioned user in Okta: ${result}`);
```

**Azure AD provisioning**:

**`Python`**

```python Python
import requests
import os

AZURE_TENANT_ID = os.getenv("AZURE_TENANT_ID")
AZURE_CLIENT_ID = os.getenv("AZURE_CLIENT_ID")
AZURE_CLIENT_SECRET = os.getenv("AZURE_CLIENT_SECRET")

def get_azure_access_token() -> str:
    response = requests.post(
        f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/oauth2/v2.0/token",
        data={
            "client_id": AZURE_CLIENT_ID,
            "client_secret": AZURE_CLIENT_SECRET,
            "scope": "https://graph.microsoft.com/.default",
            "grant_type": "client_credentials"
        }
    )
    return response.json()["access_token"]

def provision_user_in_azure_ad(deel_user: dict) -> dict:
    access_token = get_azure_access_token()
    azure_user = {
        "accountEnabled": deel_user["active"],
        "displayName": f"{deel_user['name']['givenName']} {deel_user['name']['familyName']}",
        "mailNickname": deel_user["userName"].split("@")[0],
        "userPrincipalName": deel_user["userName"],
        "passwordProfile": {"forceChangePasswordNextSignIn": True, "password": "TempPassword123!"},
        "givenName": deel_user["name"]["givenName"],
        "surname": deel_user["name"]["familyName"],
        "jobTitle": deel_user.get("title", ""),
        "department": deel_user.get("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {}).get("department", "")
    }
    response = requests.post(
        "https://graph.microsoft.com/v1.0/users",
        headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
        json=azure_user
    )
    if response.status_code == 201:
        return {"success": True, "azure_user_id": response.json()["id"]}
    return {"success": False, "error": response.text}
```

**`Node.js`**

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

async function getAzureAccessToken() {
  const { data } = await axios.post(
    `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/oauth2/v2.0/token`,
    new URLSearchParams({
      client_id: process.env.AZURE_CLIENT_ID,
      client_secret: process.env.AZURE_CLIENT_SECRET,
      scope: 'https://graph.microsoft.com/.default',
      grant_type: 'client_credentials'
    })
  );
  return data.access_token;
}

async function provisionUserInAzureAD(deelUser) {
  const accessToken = await getAzureAccessToken();
  const enterpriseExt = deelUser['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'] || {};
  const azureUser = {
    accountEnabled: deelUser.active,
    displayName: `${deelUser.name.givenName} ${deelUser.name.familyName}`,
    mailNickname: deelUser.userName.split('@')[0],
    userPrincipalName: deelUser.userName,
    passwordProfile: { forceChangePasswordNextSignIn: true, password: 'TempPassword123!' },
    givenName: deelUser.name.givenName,
    surname: deelUser.name.familyName,
    jobTitle: deelUser.title || '',
    department: enterpriseExt.department || ''
  };
  try {
    const { data } = await axios.post('https://graph.microsoft.com/v1.0/users', azureUser, {
      headers: { Authorization: `Bearer ${accessToken}` }
    });
    return { success: true, azure_user_id: data.id };
  } catch (err) {
    return { success: false, error: err.message };
  }
}
```

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

### Sync user updates from Deel

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

**Scenario: Employee receives a promotion in Deel**

**`Python`**

```python Python
def detect_user_changes(deel_user: Dict, stored_user: Dict) -> List[str]:
    """Detect which fields changed between Deel and stored state."""
    changes = []

    if deel_user.get("title") != stored_user.get("title"):
        changes.append("title")

    enterprise_deel = deel_user.get("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {})
    enterprise_stored = stored_user.get("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {})

    if enterprise_deel.get("department") != enterprise_stored.get("department"):
        changes.append("department")

    if enterprise_deel.get("manager", {}).get("value") != enterprise_stored.get("manager", {}).get("value"):
        changes.append("manager")

    return changes

def update_okta_user(okta_user_id: str, updates: Dict) -> Dict:
    """Update user profile in Okta."""
    response = requests.post(
        f"{OKTA_DOMAIN}/api/v1/users/{okta_user_id}",
        headers={
            "Authorization": f"SSWS {OKTA_API_TOKEN}",
            "Content-Type": "application/json"
        },
        json={"profile": updates}
    )

    if response.status_code == 200:
        return {"success": True, "updated_fields": list(updates.keys())}
    else:
        return {"success": False, "error": response.text}

# Detect changes
current_deel_user = fetch_deel_user_by_id("f8e7d6c5-4b3a-2910-8765-fedcba098765")
changes = detect_user_changes(current_deel_user, stored_snapshot)

if "title" in changes or "department" in changes:
    # Update Okta profile
    updates = {
        "title": current_deel_user.get("title"),
        "department": current_deel_user.get("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {}).get("department")
    }
    result = update_okta_user(okta_user_id, updates)
    print(f"Updated Okta user: {result}")
```

**`Node.js`**

```javascript Node.js
function detectUserChanges(deelUser, storedUser) {
  const changes = [];

  if (deelUser.title !== storedUser.title) {
    changes.push('title');
  }

  const enterpriseDeel = deelUser['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'] || {};
  const enterpriseStored = storedUser['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'] || {};

  if (enterpriseDeel.department !== enterpriseStored.department) {
    changes.push('department');
  }

  if (enterpriseDeel.manager?.value !== enterpriseStored.manager?.value) {
    changes.push('manager');
  }

  return changes;
}

async function updateOktaUser(oktaUserId, updates) {
  const response = await axios.post(
    `${OKTA_DOMAIN}/api/v1/users/${oktaUserId}`,
    { profile: updates },
    {
      headers: {
        'Authorization': `SSWS ${OKTA_API_TOKEN}`,
        'Content-Type': 'application/json'
      }
    }
  );

  return {
    success: true,
    updatedFields: Object.keys(updates)
  };
}

// Detect changes and update
const currentDeelUser = await fetchDeelUserById('f8e7d6c5-4b3a-2910-8765-fedcba098765');
const changes = detectUserChanges(currentDeelUser, storedSnapshot);

if (changes.includes('title') || changes.includes('department')) {
  const updates = {
    title: currentDeelUser.title,
    department: currentDeelUser['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']?.department
  };
  const result = await updateOktaUser(oktaUserId, updates);
  console.log(`Updated Okta user: ${result}`);
}
```

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

### 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)**

**`Python`**

```python Python
def deactivate_okta_user(okta_user_id: str) -> Dict:
    """Deactivate a user in Okta."""
    response = requests.post(
        f"{OKTA_DOMAIN}/api/v1/users/{okta_user_id}/lifecycle/deactivate",
        headers={
            "Authorization": f"SSWS {OKTA_API_TOKEN}"
        }
    )

    if response.status_code == 200:
        return {"success": True, "status": "deactivated"}
    else:
        return {"success": False, "error": response.text}

# Check if Deel user is inactive
deel_user = fetch_deel_user_by_id("f8e7d6c5-4b3a-2910-8765-fedcba098765")

if not deel_user.get("active"):
    # Deactivate in Okta
    result = deactivate_okta_user(okta_user_id)
    print(f"Deactivated Okta user: {result}")
```

**`Node.js`**

```javascript Node.js
async function deactivateOktaUser(oktaUserId) {
  const response = await axios.post(
    `${OKTA_DOMAIN}/api/v1/users/${oktaUserId}/lifecycle/deactivate`,
    {},
    {
      headers: {
        'Authorization': `SSWS ${OKTA_API_TOKEN}`
      }
    }
  );

  return { success: true, status: 'deactivated' };
}

// Check if Deel user is inactive
const deelUser = await fetchDeelUserById('f8e7d6c5-4b3a-2910-8765-fedcba098765');

if (!deelUser.active) {
  const result = await deactivateOktaUser(oktaUserId);
  console.log(`Deactivated Okta user: ${result}`);
}
```

**Option 2: Delete user (Azure AD)**

**`Python`**

```python Python
def delete_azure_ad_user(azure_user_id: str) -> dict:
    access_token = get_azure_access_token()
    response = requests.delete(
        f"https://graph.microsoft.com/v1.0/users/{azure_user_id}",
        headers={"Authorization": f"Bearer {access_token}"}
    )
    if response.status_code == 204:
        return {"success": True, "status": "deleted"}
    return {"success": False, "error": response.text}
```

**`Node.js`**

```javascript Node.js
async function deleteAzureADUser(azureUserId) {
  const accessToken = await getAzureAccessToken();
  try {
    await axios.delete(`https://graph.microsoft.com/v1.0/users/${azureUserId}`, {
      headers: { Authorization: `Bearer ${accessToken}` }
    });
    return { success: true, status: 'deleted' };
  } catch (err) {
    return { success: false, error: err.message };
  }
}
```

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

### Schedule regular synchronization

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

**Cron job example (runs every 15 minutes)**:

**`Python`**

```python Python
import schedule
import time

def sync_deel_to_idp():
    print("Starting Deel → Identity Provider sync...")
    deel_users = fetch_all_deel_users()
    user_mappings = load_user_mappings_from_database()

    for deel_user in deel_users:
        deel_user_id = deel_user["id"]
        email = deel_user["userName"]

        if deel_user_id in user_mappings:
            idp_user_id = user_mappings[deel_user_id]
            stored_user = get_stored_user_snapshot(deel_user_id)
            changes = detect_user_changes(deel_user, stored_user)
            if changes:
                update_okta_user(idp_user_id, transform_changes(deel_user, changes))
            if not deel_user.get("active"):
                deactivate_okta_user(idp_user_id)
        else:
            if deel_user.get("active"):
                result = provision_user_in_okta(transform_deel_to_okta(deel_user))
                if result["success"]:
                    save_user_mapping(deel_user_id, result["okta_user_id"])

        save_user_snapshot(deel_user_id, deel_user)

    print("Sync completed successfully")

schedule.every(15).minutes.do(sync_deel_to_idp)

while True:
    schedule.run_pending()
    time.sleep(60)
```

**`Node.js`**

```javascript Node.js
const cron = require('node-cron');

async function syncDeelToIdp() {
  console.log('Starting Deel → Identity Provider sync...');
  const deelUsers = await fetchAllDeelUsers();
  const userMappings = await loadUserMappingsFromDatabase();

  for (const deelUser of deelUsers) {
    const deelUserId = deelUser.id;
    const idpUserId = userMappings[deelUserId];

    if (idpUserId) {
      const storedUser = await getStoredUserSnapshot(deelUserId);
      const changes = detectUserChanges(deelUser, storedUser);
      if (changes.length) await updateOktaUser(idpUserId, transformChanges(deelUser, changes));
      if (!deelUser.active) await deactivateOktaUser(idpUserId);
    } else if (deelUser.active) {
      const result = await provisionUserInOkta(transformDeelToOkta(deelUser));
      if (result.success) await saveUserMapping(deelUserId, result.okta_user_id);
    }

    await saveUserSnapshot(deelUserId, deelUser);
  }

  console.log('Sync completed successfully');
}

// Run every 15 minutes
cron.schedule('*/15 * * * *', syncDeelToIdp);
```

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.

**`Python`**

```python Python
def filter_users_by_contract_type(deel_users: list, contract_types: list) -> list:
    filtered_users = []
    for user in deel_users:
        deel_ext = user.get("urn:ietf:params:scim:schemas:extension:2.0:User", {})
        employments = deel_ext.get("employments", [])
        for employment in employments:
            if employment.get("contractType") in contract_types and employment.get("active"):
                filtered_users.append(user)
                break
    return filtered_users

deel_users = fetch_all_deel_users()
eor_employees = filter_users_by_contract_type(deel_users, ["eor"])

for employee in eor_employees:
    provision_user_in_okta(transform_deel_to_okta(employee))

print(f"Provisioned {len(eor_employees)} EOR employees")
```

**`Node.js`**

```javascript Node.js
function filterUsersByContractType(deelUsers, contractTypes) {
  return deelUsers.filter(user => {
    const ext = user['urn:ietf:params:scim:schemas:extension:2.0:User'] || {};
    const employments = ext.employments || [];
    return employments.some(e => contractTypes.includes(e.contractType) && e.active);
  });
}

const deelUsers = await fetchAllDeelUsers();
const eorEmployees = filterUsersByContractType(deelUsers, ['eor']);

for (const employee of eorEmployees) {
  await provisionUserInOkta(transformDeelToOkta(employee));
}

console.log(`Provisioned ${eorEmployees.length} 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.

**`Python`**

```python Python
import os

OKTA_DOMAIN = os.getenv("OKTA_DOMAIN")
OKTA_API_TOKEN = os.getenv("OKTA_API_TOKEN")

DEPARTMENT_GROUP_MAPPING = {
    "Engineering": ["00g1a2b3c4d5e6f7g8", "00g9h8i7j6k5l4m3n2"],
    "Product": ["00g2a3b4c5d6e7f8g9", "00g9h8i7j6k5l4m3n2"],
    "Sales": ["00g3a4b5c6d7e8f9g0", "00g9h8i7j6k5l4m3n2"],
    "Design": ["00g4a5b6c7d8e9f0g1", "00g9h8i7j6k5l4m3n2"]
}
ALL_EMPLOYEES_GROUP = "00g9h8i7j6k5l4m3n2"

def assign_user_to_groups(okta_user_id: str, group_ids: list):
    for group_id in group_ids:
        requests.put(
            f"{OKTA_DOMAIN}/api/v1/groups/{group_id}/users/{okta_user_id}",
            headers={"Authorization": f"SSWS {OKTA_API_TOKEN}"}
        )

deel_user = fetch_deel_user_by_id("f8e7d6c5-4b3a-2910-8765-fedcba098765")
result = provision_user_in_okta(transform_deel_to_okta(deel_user))

department = deel_user.get("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {}).get("department")
group_ids = DEPARTMENT_GROUP_MAPPING.get(department, [ALL_EMPLOYEES_GROUP])
assign_user_to_groups(result["okta_user_id"], group_ids)
```

**`Node.js`**

```javascript Node.js
const DEPARTMENT_GROUP_MAPPING = {
  Engineering: ['00g1a2b3c4d5e6f7g8', '00g9h8i7j6k5l4m3n2'],
  Product: ['00g2a3b4c5d6e7f8g9', '00g9h8i7j6k5l4m3n2'],
  Sales: ['00g3a4b5c6d7e8f9g0', '00g9h8i7j6k5l4m3n2'],
  Design: ['00g4a5b6c7d8e9f0g1', '00g9h8i7j6k5l4m3n2']
};
const ALL_EMPLOYEES_GROUP = '00g9h8i7j6k5l4m3n2';

async function assignUserToGroups(oktaUserId, groupIds) {
  for (const groupId of groupIds) {
    await axios.put(
      `${process.env.OKTA_DOMAIN}/api/v1/groups/${groupId}/users/${oktaUserId}`,
      {},
      { headers: { Authorization: `SSWS ${process.env.OKTA_API_TOKEN}` } }
    );
  }
}

const deelUser = await fetchDeelUserById('f8e7d6c5-4b3a-2910-8765-fedcba098765');
const result = await provisionUserInOkta(transformDeelToOkta(deelUser));

const enterpriseExt = deelUser['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'] || {};
const groupIds = DEPARTMENT_GROUP_MAPPING[enterpriseExt.department] || [ALL_EMPLOYEES_GROUP];
await assignUserToGroups(result.okta_user_id, groupIds);
```

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.

**`Python`**

```python Python
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/webhooks/deel", methods=["POST"])
def deel_webhook_handler():
    event = request.json
    event_type = event.get("type")
    user_data = event.get("data", {})

    if event_type == "user.created":
        deel_user = fetch_deel_user_by_id(user_data.get("id"))
        result = provision_user_in_okta(transform_deel_to_okta(deel_user))
        return jsonify({"status": "provisioned", "result": result})

    elif event_type == "user.updated":
        deel_user_id = user_data.get("id")
        deel_user = fetch_deel_user_by_id(deel_user_id)
        idp_user_id = get_idp_user_id_from_mapping(deel_user_id)
        stored_user = get_stored_user_snapshot(deel_user_id)
        changes = detect_user_changes(deel_user, stored_user)
        if changes:
            update_okta_user(idp_user_id, transform_changes(deel_user, changes))
            save_user_snapshot(deel_user_id, deel_user)
        return jsonify({"status": "updated", "changes": changes})

    elif event_type == "user.deactivated":
        deel_user_id = user_data.get("id")
        deactivate_okta_user(get_idp_user_id_from_mapping(deel_user_id))
        return jsonify({"status": "deactivated"})

    return jsonify({"status": "ignored"})

if __name__ == "__main__":
    app.run(port=8080)
```

**`Node.js`**

```javascript Node.js
const express = require('express');
const app = express();
app.use(express.json());

app.post('/webhooks/deel', async (req, res) => {
  const { type: eventType, data: userData } = req.body;

  if (eventType === 'user.created') {
    const deelUser = await fetchDeelUserById(userData.id);
    const result = await provisionUserInOkta(transformDeelToOkta(deelUser));
    return res.json({ status: 'provisioned', result });
  }

  if (eventType === 'user.updated') {
    const deelUser = await fetchDeelUserById(userData.id);
    const idpUserId = await getIdpUserIdFromMapping(userData.id);
    const storedUser = await getStoredUserSnapshot(userData.id);
    const changes = detectUserChanges(deelUser, storedUser);
    if (changes.length) {
      await updateOktaUser(idpUserId, transformChanges(deelUser, changes));
      await saveUserSnapshot(userData.id, deelUser);
    }
    return res.json({ status: 'updated', changes });
  }

  if (eventType === 'user.deactivated') {
    const idpUserId = await getIdpUserIdFromMapping(userData.id);
    await deactivateOktaUser(idpUserId);
    return res.json({ status: 'deactivated' });
  }

  res.json({ status: 'ignored' });
});

app.listen(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.

**`Python`**

```python Python
def sync_manager_relationship(deel_user: dict, okta_user_id: str):
    enterprise_ext = deel_user.get("urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", {})
    manager_info = enterprise_ext.get("manager", {})
    if not manager_info:
        return
    manager_okta_id = get_idp_user_id_from_mapping(manager_info.get("value"))
    if manager_okta_id:
        update_okta_user(okta_user_id, {
            "managerId": manager_okta_id,
            "manager": manager_info.get("displayName")
        })

for deel_user in deel_users:
    okta_user_id = get_idp_user_id_from_mapping(deel_user["id"])
    sync_manager_relationship(deel_user, okta_user_id)
```

**`Node.js`**

```javascript Node.js
async function syncManagerRelationship(deelUser, oktaUserId) {
  const enterpriseExt = deelUser['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'] || {};
  const managerInfo = enterpriseExt.manager;
  if (!managerInfo) return;

  const managerOktaId = await getIdpUserIdFromMapping(managerInfo.value);
  if (managerOktaId) {
    await updateOktaUser(oktaUserId, {
      managerId: managerOktaId,
      manager: managerInfo.displayName
    });
  }
}

for (const deelUser of deelUsers) {
  const oktaUserId = await getIdpUserIdFromMapping(deelUser.id);
  await syncManagerRelationship(deelUser, oktaUserId);
}
```

Organizational hierarchies from Deel are replicated in your identity provider.

## Best practices

#### Store user mappings persistently

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

#### Implement idempotent operations

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

#### Optimize sync frequency

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

#### Handle errors gracefully

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

#### Security and authentication

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

#### Data consistency and validation

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

#### User already exists in identity provider

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.

#### Deel API rate limits exceeded

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.

#### Identity provider API authentication fails

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 relationships not syncing

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

#### Department groups not assigned

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.

#### Sync process running too long

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

#### [SCIM API reference](/api/scim-api/overview)

Learn about Deel SCIM API capabilities, schemas, and endpoints

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

Set up webhooks to receive real-time notifications of user changes

#### [Authentication](/api/authentication)

Generate API tokens for secure Deel SCIM API access

#### [HRIS introduction](/api/hris/introduction)

Explore additional HRIS APIs for workforce data management