HomeUser provisioningCSV to SCIM example

CSV to SCIM example

A sample PowerShell script that creates and updates Interact users via the SCIM API from CSV data, including manager assignment.

Before you start

  • You need a SCIM profile source and authentication token in Interact. See SCIM v2.0.
  • You need PowerShell and a CSV file of user data.

Overview

This PowerShell script automates creating and updating user accounts via the SCIM API from CSV data. It includes performance options, error handling and manager assignment.

Features:

  • Bulk user processing — creates new users and updates existing users from CSV data.
  • Performance options — configurable delays between API calls.
  • Manager assignment — assigns managers based on CSV data.
  • Error handling — error reporting and validation.
  • Progress tracking — real-time progress reporting and performance metrics.

The script

The full script follows. Substitute the parameters (CSV path, base URL and authentication token) when you run it.

Warning: The authentication token is visible in the script parameters. Consider secure storage and never expose your token in shared or client-side code.

# Create and update Interact users via the SCIM API from a CSV file.
#
# This is an illustrative example. It shows a two-phase flow — create or update
# each user, then assign managers in a second pass once all users exist — using a
# trimmed set of profile fields. See SCIM field mapping and SCIM package examples
# for the full set of supported fields and payloads.

param(
    [string]$CsvPath = "",
    [string]$BaseUrl = "",
    [string]$BearerToken = "",
    [int]$DelayMs = 50            # Delay between API calls, in milliseconds
)

$headers = @{
    "Authorization" = "Bearer $BearerToken"
    "Content-Type"  = "application/scim+json"
}

# --- JSON templates (trimmed to core fields; add more as needed) ---
$PostTemplate = @'
{
    "schemas": [
        "urn:ietf:params:scim:schemas:core:2.0:User",
        "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
    ],
    "externalId": "{ExternalId}",
    "userName": "{Username}",
    "active": {Active},
    "password": "{Password}",
    "title": "{JobTitle}",
    "userType": "{ProfileType}",
    "emails": [
        { "primary": true, "type": "work", "value": "{Email}" }
    ],
    "name": { "familyName": "{FamilyName}", "givenName": "{GivenName}" },
    "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
        "department": "{Department}",
        "organization": "{Company}"
    },
    "urn:ietf:params:scim:schemas:extension:interactsoftware:2.0:User": {
        "location": "{Location}",
        "loginType": "{AuthenticationType}",
        "jobStartDate": "{JobStartDate}"
    }
}
'@

$PatchTemplate = @'
{
    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
    "Operations": [
        { "op": "Replace", "path": "active", "value": {Active} },
        { "op": "Replace", "path": "title", "value": "{JobTitle}" },
        { "op": "Replace", "path": "userType", "value": "{ProfileType}" },
        { "op": "Replace", "path": "emails[type eq \"work\"].value", "value": "{Email}" },
        { "op": "Replace", "path": "name.familyName", "value": "{FamilyName}" },
        { "op": "Replace", "path": "name.givenName", "value": "{GivenName}" },
        { "op": "Replace", "path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department", "value": "{Department}" },
        { "op": "Replace", "path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:organization", "value": "{Company}" },
        { "op": "Replace", "path": "urn:ietf:params:scim:schemas:extension:interactsoftware:2.0:User:location", "value": "{Location}" },
        { "op": "Replace", "path": "urn:ietf:params:scim:schemas:extension:interactsoftware:2.0:User:loginType", "value": "{AuthenticationType}" }
    ]
}
'@

$ManagerTemplate = @'
{
    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
    "Operations": [
        {
            "op": "Replace",
            "path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager",
            "value": "{ManagerId}"
        }
    ]
}
'@

# Convert a date to the ISO 8601 format SCIM expects (yyyy-MM-ddTHH:mm:ssZ).
# This example handles date-only and ISO datetime input; extend it for other formats.
function Convert-DateToScimFormat {
    param([string]$DateString)
    if ([string]::IsNullOrWhiteSpace($DateString)) { return "" }
    if ($DateString -match '^\d{4}-\d{2}-\d{2}$') { return $DateString + "T00:00:00Z" }
    try   { return ([datetime]$DateString).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") }
    catch { Write-Host "Warning: could not parse date '$DateString', leaving empty"; return "" }
}

# Replace {Placeholder} tokens in a JSON template with escaped values.
function Set-JsonPlaceholders {
    param([string]$JsonTemplate, [hashtable]$ReplacementValues)
    $result = $JsonTemplate
    foreach ($placeholder in $ReplacementValues.Keys) {
        $value = $ReplacementValues[$placeholder]
        if ([string]::IsNullOrWhiteSpace($value)) {
            $escaped = ""
        } elseif ($placeholder -eq "Active" -and ($value -eq "true" -or $value -eq "false")) {
            $escaped = $value   # boolean, no quotes
        } else {
            $escaped = $value -replace '\\', '\\' -replace '"', '\"' -replace "`n", '\n' -replace "`r", '\r' -replace "`t", '\t'
        }
        $result = $result -replace [regex]::Escape("{$placeholder}"), $escaped
    }
    return $result
}

# Drop PATCH operations whose value is empty (keeps booleans and non-empty strings).
function Remove-NullPatchOperations {
    param([string]$PatchJson)
    try {
        $patch = $PatchJson | ConvertFrom-Json
        $patch.Operations = @($patch.Operations | Where-Object {
            $null -ne $_.value -and ($_.value -is [bool] -or -not [string]::IsNullOrWhiteSpace($_.value.ToString()))
        })
        return $patch | ConvertTo-Json -Depth 10
    }
    catch { return $PatchJson }
}

# Look up a user by userName; returns the SCIM id if found.
function Test-UserExists {
    param([string]$UserName)
    try {
        $resp = Invoke-RestMethod -Uri ("$BaseUrl" + "?filter=userName eq `"$UserName`"") -Method GET -Headers $headers
        if ($resp.Resources -and $resp.Resources.Count -gt 0) {
            return @{ Exists = $true; UserId = $resp.Resources[0].id }
        }
    }
    catch { Write-Host "Error checking user '$UserName': $($_.Exception.Message)" }
    return @{ Exists = $false; UserId = $null }
}

# --- Phase 1: create or update each user ---
if (-not (Test-Path $CsvPath)) { throw "CSV file not found at path: $CsvPath" }
$csvData = Import-Csv -Path $CsvPath
Write-Host "Found $($csvData.Count) users to process"

$createdCount = 0; $updatedCount = 0; $failureCount = 0
$failedUsers = @()

foreach ($row in $csvData) {
    if ([string]::IsNullOrWhiteSpace($row.UserName)) {
        Write-Host "Skipping row with missing username"
        $failureCount++; continue
    }

    # Map CSV columns to template placeholders
    $replacements = @{
        "ExternalId"         = $row.ExternalId
        "Username"           = $row.UserName
        "Active"             = if ($row.Active -in @("true", "1")) { "true" } else { "false" }
        "Password"           = $row.Password
        "FamilyName"         = $row.FamilyName
        "GivenName"          = $row.GivenName
        "Email"              = $row.EmailAddress
        "JobTitle"           = $row.JobTitle
        "ProfileType"        = $row.ProfileType
        "AuthenticationType" = $row.AuthenticationType
        "Department"         = $row.PrimaryDepartment
        "Company"            = $row.PrimaryCompany
        "Location"           = $row.PrimaryLocation
        "JobStartDate"       = Convert-DateToScimFormat -DateString $row.JobStartDate
    }

    $user = Test-UserExists -UserName $row.UserName
    try {
        if ($user.Exists) {
            $payload = Set-JsonPlaceholders -JsonTemplate $PatchTemplate -ReplacementValues $replacements
            $payload = Remove-NullPatchOperations -PatchJson $payload
            Invoke-RestMethod -Uri "$BaseUrl/$($user.UserId)" -Method PATCH -Body $payload -Headers $headers | Out-Null
            Write-Host "Updated $($row.UserName)"
            $updatedCount++
        }
        else {
            $payload = Set-JsonPlaceholders -JsonTemplate $PostTemplate -ReplacementValues $replacements
            Invoke-RestMethod -Uri $BaseUrl -Method POST -Body $payload -Headers $headers | Out-Null
            Write-Host "Created $($row.UserName)"
            $createdCount++
        }
    }
    catch {
        Write-Host "Failed for $($row.UserName): $($_.Exception.Message)"
        $failureCount++; $failedUsers += $row.UserName
    }

    if ($DelayMs -gt 0) { Start-Sleep -Milliseconds $DelayMs }
}

Write-Host ""
Write-Host "Created: $createdCount  Updated: $updatedCount  Failed: $failureCount"
if ($failedUsers.Count -gt 0) { Write-Host "Failed users: $($failedUsers -join ', ')" }

# --- Phase 2: assign managers (run after all users exist) ---
Write-Host ""
Write-Host "Assigning managers..."
$managerSuccess = 0; $managerMissing = 0; $managerFailed = 0

foreach ($row in ($csvData | Where-Object { -not [string]::IsNullOrWhiteSpace($_."Manager Email") })) {
    try {
        $user = Test-UserExists -UserName $row.UserName
        if (-not $user.Exists) { Write-Host "User not found: $($row.UserName)"; $managerFailed++; continue }

        $manager = Test-UserExists -UserName $row."Manager Email"
        if (-not $manager.Exists) { Write-Host "Manager not found: $($row.'Manager Email')"; $managerMissing++; continue }

        $payload = $ManagerTemplate -replace '\{ManagerId\}', $manager.UserId
        Invoke-RestMethod -Uri "$BaseUrl/$($user.UserId)" -Method PATCH -Body $payload -Headers $headers | Out-Null
        Write-Host "Assigned manager to $($row.UserName)"
        $managerSuccess++
    }
    catch {
        Write-Host "Failed manager assignment for $($row.UserName): $($_.Exception.Message)"
        $managerFailed++
    }

    if ($DelayMs -gt 0) { Start-Sleep -Milliseconds $DelayMs }
}

Write-Host ""
Write-Host "Managers assigned: $managerSuccess  Not found: $managerMissing  Failed: $managerFailed"

Parameters

Parameter Type Description
CsvPath String Path to the CSV file of user data
BaseUrl String The SCIM Users endpoint, for example https://{{intranet_url}}/api/v2/scim/v2/Users
BearerToken String The SCIM authentication token
DelayMs Integer Delay between API calls in milliseconds (default 50)

Usage examples

Run the script with your CSV path, SCIM endpoint and authentication token:

.\Load-Users.ps1 -CsvPath ".\users.csv" -BaseUrl "https://{{intranet_url}}/api/v2/scim/v2/Users" -BearerToken "{{auth_token}}"

Increase the delay between calls to respect API rate limits on large imports:

.\Load-Users.ps1 -CsvPath ".\users.csv" -BaseUrl "https://{{intranet_url}}/api/v2/scim/v2/Users" -BearerToken "{{auth_token}}" -DelayMs 200

CSV file format

The example above reads a core subset of these columns. The full set of recognised profile columns is listed below — extend the templates and the column mapping in the script to use more of them.

Required fields

  • UserName — primary identifier for the user
  • EmailAddress — primary email address
  • ExternalId — external system identifier
  • FamilyName — last name
  • GivenName — first name
  • Active — account status (true/false)
  • Password — initial password (can be blank if AuthenticationType is SAML)
  • AuthenticationType — authentication method
  • ProfileType — user profile type (can be blank and defaults to Intranet User)
  • ForcePasswordReset — force password reset on first login (true/false)
  • PrimaryDepartment — department name
  • PrimaryLocation — work location
  • PrimaryCompany — company name

Contact information

  • AlternativeEmailAddress — secondary email
  • HomePhoneNumber — home phone
  • WorkPhoneNumber — work phone
  • MobilePhoneNumber — mobile phone
  • Address — physical address

Localisation

  • Locale — user locale (default: en-US)
  • PreferredLanguage — preferred language
  • Timezone — user timezone

Personal information

  • Title — name title (Mr., Ms., Dr., etc.)
  • Initials — name initials
  • Pronouns — preferred pronouns
  • DateOfBirth — birth date (ISO 8601)
  • Bio — user biography

Employment information

  • JobTitle — job title
  • JobStartDate — employment start date
  • JobEndDate — employment end date
  • PrimaryDepartment — department name
  • PrimaryLocation — work location
  • PrimaryCompany — company name

Manager information

  • Manager Email — manager's email address for relationship assignment

Social media

  • LinkedInId — LinkedIn profile ID
  • FacebookId — Facebook profile ID
  • InstagramTag — Instagram handle
  • TwitterTag — Twitter handle

How the script works

The script runs in two phases.

Phase 1: user creation and update

  1. Reads and validates the CSV file.
  2. Queries the SCIM API to determine whether each user exists.
  3. Uses the POST template for new users and the PATCH template for updates.
  4. Maps CSV columns to SCIM schema fields.
  5. Validates required fields and JSON syntax.
  6. Makes the appropriate REST call (POST or PATCH).
  7. Updates counters and displays progress.

Phase 2: manager assignment

  1. Identifies users with manager assignments.
  2. Resolves user and manager IDs via SCIM search.
  3. Updates the user record with the manager relationship.
  4. Handles missing users or managers gracefully.

SCIM schema support

The script supports the core Interact fields and can be amended to include additional fields. It uses the following schemas.

  • Core SCIM 2.0 schema: urn:ietf:params:scim:schemas:core:2.0:User
  • Enterprise extension (urn:ietf:params:scim:schemas:extension:enterprise:2.0:User): department, organization, manager relationship.
  • Custom extension (urn:ietf:params:scim:schemas:extension:interactsoftware:2.0:User): location, authentication type, job start date and more. See SCIM field mapping for the full set.

Functions reference

The script uses several functions.

  • Convert-DateToScimFormat — converts date-only or ISO 8601 datetime input to the SCIM format (yyyy-MM-ddTHH:mm:ssZ); extend it if your CSV uses other date formats. It returns an empty string for unparseable dates and emits a warning.
  • Set-JsonPlaceholders — replaces placeholder tokens such as {Username} in a JSON template with actual values. It handles null and empty values, treats Active as a boolean (no quotes), and escapes special characters (\, ", newlines, tabs).
  • Remove-NullPatchOperations — filters out PATCH operations whose value is null, empty or whitespace, while keeping boolean values. Returns the original JSON if filtering fails.
  • Test-UserExists — checks whether a user exists by filtering on userName, returning the user's SCIM ID if found.
  • Invoke-UserUpdate — sends a SCIM PATCH request to {BaseUrl}/{UserId} with Content-Type: application/scim+json.
  • Invoke-UserCreate — sends a SCIM POST request to create a user.

Error handling

This script is offered as an example. It provides the following error handling.

  • Validation errors: rows with a missing username are skipped with a warning, and unparseable date fields fall back to empty with a warning.
  • API errors: authentication failures (token validation and clear messages), network connectivity (timeout and connection handling), SCIM specification violations (detailed API response parsing), and rate limiting (configurable delays).
  • Recovery: individual user failures do not stop batch processing, errors are logged with specific messages, and optional features such as manager assignment are handled separately.

Performance

The script makes one existence-check call plus one create or update call per user, then a further call per manager assignment, with a configurable delay (-DelayMs, default 50ms) between calls. For large imports, increase the delay to respect API rate limits and process in batches.

Approximate timings at the default delay:

User count Approximate time
50 users ~18 seconds
100 users ~36 seconds
500 users ~3 minutes

Security considerations

  • Authentication: bearer authentication token-based API authentication. The token is visible in the script parameters, so consider secure storage. Tokens are never logged or exposed in output.
  • Data handling: passwords are transmitted over HTTPS only, no sensitive information is written to error logs, and error messages are sanitised.
  • Network: all API communication is over HTTPS, with correct SCIM content types and authentication headers, and safe parsing of API responses.

Troubleshooting

  • "CSV file not found" — verify the file path (use absolute paths if needed), check file permissions, and ensure the file exists at the specified location.
  • "User creation/update failed" — verify the SCIM API endpoint URL format, check the authentication token's validity and expiry, validate that required CSV fields are populated, and review the API response details in the error output.
  • "Date parsing warnings" — use ISO 8601 format (2024-01-15T00:00:00Z), check the supported formats in Convert-DateToScimFormat, leave date fields empty if the format is unknown, and check for invalid characters or malformed date strings.
  • "Manager assignment failed" — the manager user may not exist yet, the manager email may not match the username field, or the manager lookup may fail due to an incorrect address. Ensure manager users are processed before employees, verify manager addresses exist, and check that the manager email matches the SCIM userName field.

For performance tuning, start with the default 50ms delay and increase it if you hit rate limiting. You can reduce it for faster processing on high-throughput APIs, monitoring for errors. Test with small batches first, and consider off-peak processing for large imports.

Section: User provisioning