Skip to main content

Items Field Inspection & Bulk Updates

PowerShell utilities for inspecting Items API fields and performing bulk updates via HaloPSA API.

Overview

These scripts provide field-level inspection and bulk update capabilities for the Items (Inventory) module. Useful for troubleshooting, data cleanup, and mass modifications.

⚠️ CRITICAL: Pagination Requirement

The HaloPSA Items API requires pageinate=true parameter for pagination to function.

Without this parameter, the API will:

  • Ignore page_no and page_size parameters
  • Return only the first ~1,000 items regardless of page number
  • Return identical results on all page requests
  • Provide no error or warning

Required Query Format

# CORRECT - Pagination enabled
$itemsUrl = "$apiUrl/Item?pageinate=true&page_no=$page&page_size=$pageSize"

# WRONG - Pagination disabled (parameters ignored)
$itemsUrl = "$apiUrl/Item?page_no=$page&page_size=$pageSize"

Impact

If you omit pageinate=true:

  • Scripts will only process the first ~1,000 items
  • Remaining items will never be retrieved or updated
  • API provides no indication of missing data
  • Updates will appear successful but be incomplete

Always verify total item counts match expected values after bulk operations.

Verification Test

# Test pagination is working
$testUrl = "$apiUrl/Item?pageinate=true&page_no=2&page_size=100"
$response = Invoke-RestMethod -Uri $testUrl -Headers $headers -Method Get

# Page 2 should have different items than page 1
Write-Host "Page 2 items: $($response.items.Count)"
Write-Host "First item ID: $($response.items[0].id)"

If first item ID on page 2 is 101 (not 1), pagination is working correctly.

Field Inspection Script

Query all fields available on an Item record to identify field names, types, and current values.

Use Cases

  • Discover field names before writing update scripts
  • Troubleshoot missing or unexpected data
  • Verify field types for API payload construction
  • Document available fields for integration work

Script

<#
.SYNOPSIS
    HaloPSA Items - Field Inspector
.DESCRIPTION
    Queries Items API and displays all available fields with types and values
#>

# Configuration
$tenant = 'your_tenant'
$clientId = 'your_client_id'
$clientSecret = 'your_client_secret'

$authUrl = "https://$tenant.halopsa.com/auth/token"
$apiUrl = "https://$tenant.halopsa.com/api"

# Authenticate
$tokenBody = @{
    grant_type = 'client_credentials'
    client_id = $clientId
    client_secret = $clientSecret
    scope = 'read:items'
}

$token = Invoke-RestMethod -Uri $authUrl -Method Post -Body $tokenBody
$headers = @{
    Authorization = "Bearer $($token.access_token)"
    'Content-Type' = 'application/json'
}

# Query Items with detailed fields
$itemsUrl = "$apiUrl/Item?page_size=5&includedetails=true"
$response = Invoke-RestMethod -Uri $itemsUrl -Headers $headers -Method Get

# Display field information
foreach ($item in $response.items) {
    Write-Host "`n=== Item ID: $($item.id) - $($item.name) ===" -ForegroundColor Cyan
    
    $item.PSObject.Properties | ForEach-Object {
        $fieldName = $_.Name
        $fieldValue = $_.Value
        $fieldType = $_.TypeNameOfValue
        
        Write-Host "$fieldName" -NoNewline -ForegroundColor Yellow
        Write-Host " [$fieldType]" -NoNewline -ForegroundColor Gray
        Write-Host " = $fieldValue" -ForegroundColor White
    }
}

# Save raw JSON for detailed inspection
$response | ConvertTo-Json -Depth 10 | Out-File "items_fields_raw.json"
Write-Host "`nRaw JSON saved to: items_fields_raw.json" -ForegroundColor Green

Key Parameters

Parameter Description
includedetails=true Returns all fields including custom fields
page_size Number of items to return (use small number for inspection)

Example Output

=== Item ID: 363 - MNT-135 ===
id [Int32] = 363
name [String] = MNT-135
description [String] = Tilting Wall Mount
template_id [Int32] = 88
item_type_id [Int32] = 2
supplier_id [Int32] = 15
...

Bulk Update Script

Perform mass updates on Items records with dry-run mode, batch processing, and comprehensive logging.

Use Cases

  • Clear template_id from all items
  • Update pricing across inventory
  • Reassign supplier relationships
  • Bulk field modifications

Features

  • Dry-run mode by default - Preview changes before execution
  • Batch processing - Configurable batch sizes with rate limiting
  • Progress tracking - Real-time percentage complete
  • Error handling - Continues on failures, reports all errors
  • Comprehensive logging - Full audit trail with timestamps

Script

<#
.SYNOPSIS
    HaloPSA Items - Bulk Update Template
.DESCRIPTION
    Bulk update Items with dry-run mode and comprehensive logging
    Modify the $updatePayload section for your specific use case
#>

$ErrorActionPreference = 'Continue'

# ============================================
# CONFIGURATION
# ============================================
$tenant = 'your_tenant'
$clientId = 'your_client_id'
$clientSecret = 'your_client_secret'

$batchSize = 10
$delayBetweenBatches = 500  # milliseconds
$dryRun = $true  # Set to $false to actually update

$authUrl = "https://$tenant.halopsa.com/auth/token"
$apiUrl = "https://$tenant.halopsa.com/api"

# ============================================
# LOGGING
# ============================================
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$logFile = "BulkItemUpdate_$timestamp.log"

function Write-Log {
    param(
        [Parameter(Mandatory=$false)]
        [AllowEmptyString()]
        [string]$Message = '',
        
        [Parameter(Mandatory=$false)]
        [ValidateSet('Black','DarkBlue','DarkGreen','DarkCyan','DarkRed','DarkMagenta','DarkYellow','Gray','DarkGray','Blue','Green','Cyan','Red','Magenta','Yellow','White')]
        [string]$Color = 'White'
    )
    
    if ($Message -eq '') {
        Write-Host ''
        Add-Content -Path $logFile -Value ''
    } else {
        $logMessage = "$(Get-Date -Format 'HH:mm:ss') $Message"
        Write-Host $logMessage -ForegroundColor $Color
        Add-Content -Path $logFile -Value $logMessage
    }
}

Write-Log -Message "=== HaloPSA Bulk Item Update - Started ===" -Color Cyan
Write-Log -Message "Log file: $logFile" -Color Cyan
Write-Log

# ============================================
# AUTHENTICATE
# ============================================
Write-Log -Message 'Authenticating...' -Color Cyan
try {
    $tokenBody = @{
        grant_type = 'client_credentials'
        client_id = $clientId
        client_secret = $clientSecret
        scope = 'edit:items read:items'
    }
    
    $token = Invoke-RestMethod -Uri $authUrl -Method Post -Body $tokenBody -ErrorAction Stop
    $headers = @{
        Authorization = "Bearer $($token.access_token)"
        'Content-Type' = 'application/json'
    }
    Write-Log -Message '✓ Authentication successful' -Color Green
    Write-Log
} catch {
    Write-Log -Message '✗ Authentication failed' -Color Red
    Write-Log -Message "Error: $($_.Exception.Message)" -Color Red
    exit 1
}

# ============================================
# RETRIEVE ALL ITEMS
# ============================================
Write-Log -Message 'Retrieving items...' -Color Cyan
$allItems = @()
$page = 1
$pageSize = 100

try {
    do {
        $itemsUrl = "$apiUrl/Item?page_size=$pageSize&page_no=$page"
        $response = Invoke-RestMethod -Uri $itemsUrl -Headers $headers -Method Get -ErrorAction Stop
        
        if ($response.items) {
            $allItems += $response.items
        }
        
        $page++
        Start-Sleep -Milliseconds 200
        
    } while ($response.items -and $response.items.Count -eq $pageSize)
    
    Write-Log -Message "✓ Retrieved $($allItems.Count) total items" -Color Green
    Write-Log
} catch {
    Write-Log -Message '✗ Failed to retrieve items' -Color Red
    Write-Log -Message "Error: $($_.Exception.Message)" -Color Red
    exit 1
}

# ============================================
# FILTER ITEMS TO UPDATE
# ============================================
Write-Log -Message 'Filtering items...' -Color Cyan

# MODIFY THIS FILTER FOR YOUR USE CASE
$itemsToUpdate = $allItems | Where-Object { 
    $_.template_id -ne $null -and $_.template_id -ne 0 
}

if ($itemsToUpdate.Count -eq 0) {
    Write-Log -Message '✓ No items match filter criteria' -Color Green
    exit 0
}

Write-Log -Message "✓ Found $($itemsToUpdate.Count) items to update" -Color Yellow
Write-Log

# ============================================
# PREVIEW
# ============================================
Write-Log -Message "Preview - First 10 items:" -Color Cyan
for ($i = 0; $i -lt [Math]::Min(10, $itemsToUpdate.Count); $i++) {
    $item = $itemsToUpdate[$i]
    Write-Log -Message "  ID: $($item.id) | Name: $($item.name) | template_id: $($item.template_id)"
}
Write-Log

# ============================================
# CONFIRMATION
# ============================================
if ($dryRun) {
    Write-Log -Message '*** DRY RUN MODE ***' -Color Magenta
    $confirm = Read-Host 'Continue with dry run? (yes/no)'
} else {
    Write-Log -Message '*** LIVE MODE - WILL UPDATE ITEMS ***' -Color Red
    $confirm = Read-Host 'Type YES in capitals to confirm'
}

if ($confirm -ne 'yes' -and $confirm -ne 'YES') {
    Write-Log -Message 'Operation cancelled' -Color Yellow
    exit 0
}

# ============================================
# PROCESS UPDATES
# ============================================
Write-Log
Write-Log -Message "Processing updates..." -Color Cyan

$successCount = 0
$failCount = 0
$processedCount = 0
$totalCount = $itemsToUpdate.Count

for ($i = 0; $i -lt $totalCount; $i += $batchSize) {
    $batch = $itemsToUpdate[$i..[Math]::Min($i + $batchSize - 1, $totalCount - 1)]
    
    foreach ($item in $batch) {
        $processedCount++
        $percentComplete = [Math]::Round(($processedCount / $totalCount) * 100, 1)
        
        if ($dryRun) {
            Write-Log -Message "  [$processedCount/$totalCount ($percentComplete%)] DRY RUN: Would update ID $($item.id)" -Color Gray
            $successCount++
        } else {
            try {
                # MODIFY THIS PAYLOAD FOR YOUR USE CASE
                $updatePayload = @{
                    id = $item.id
                    template_id = 0
                }
                
                # Manual array wrapper - HaloPSA API requires array
                $updateBody = "[$($updatePayload | ConvertTo-Json -Depth 5 -Compress)]"
                
                $updateResult = Invoke-RestMethod -Uri "$apiUrl/Item" -Headers $headers -Method Post -Body $updateBody -ContentType 'application/json' -ErrorAction Stop
                
                Write-Log -Message "  [$processedCount/$totalCount ($percentComplete%)] ✓ Updated ID $($item.id)" -Color Green
                $successCount++
                
            } catch {
                Write-Log -Message "  [$processedCount/$totalCount ($percentComplete%)] ✗ FAILED ID $($item.id)" -Color Red
                $failCount++
            }
            
            Start-Sleep -Milliseconds 100
        }
    }
    
    if ($i + $batchSize -lt $totalCount) {
        Start-Sleep -Milliseconds $delayBetweenBatches
    }
}

# ============================================
# SUMMARY
# ============================================
Write-Log
Write-Log -Message "=== SUMMARY ===" -Color Cyan
if ($dryRun) {
    Write-Log -Message "DRY RUN: Items that would be updated: $successCount" -Color Yellow
} else {
    Write-Log -Message "Successfully updated: $successCount" -Color Green
    Write-Log -Message "Failed: $failCount" -Color $(if ($failCount -gt 0) { 'Red' } else { 'White' })
}
Write-Log -Message "Log saved to: $logFile" -Color Cyan

Critical API Requirements

Array Format: The Items API requires an array payload even for single updates:

# CORRECT - Array format
$updateBody = "[{\"id\":363,\"template_id\":0}]"

# WRONG - Single object
$updateBody = "{\"id\":363,\"template_id\":0}"

PowerShell's ConvertTo-Json strips array wrappers for single elements, so manual wrapping is required:

$updatePayload = @{ id = 363; template_id = 0 }
$updateBody = "[$($updatePayload | ConvertTo-Json -Compress)]"

Workflow

  1. Test in dev environment first
  2. Run dry-run - Review log file
  3. Verify preview - Confirm correct items selected
  4. Set $dryRun = $false
  5. Run live update - Type YES to confirm
  6. Verify in UI - Spot-check random items
  7. Deploy to production - After dev verification

Authentication

All scripts require HaloPSA API credentials:

  1. ConfigurationIntegrationsHaloPSA APIView Applications
  2. Create application - Select "Services" authentication
  3. Set permissions - read:items, edit:items
  4. Copy Client ID and Client Secret - Secret shown only once
  5. Update script configuration:
    • $tenant = Your HaloPSA subdomain (e.g., 'dtcdev')
    • $clientId = Application Client ID
    • $clientSecret = Application Client Secret

API Endpoints

Endpoint Method Purpose
/Item GET Retrieve items (supports pagination)
/Item/{id} GET Retrieve single item by ID
/Item POST Create or update items (array payload)

Query Parameters

Parameter Description Example
page_size Items per page (max 100) ?page_size=50
page_no Page number ?page_no=2
includedetails Include all fields ?includedetails=true

Common Use Cases

Clear Project Template from All Items

$itemsToUpdate = $allItems | Where-Object { 
    $_.template_id -ne $null -and $_.template_id -ne 0 
}

$updatePayload = @{
    id = $item.id
    template_id = 0
}

Update Supplier for Specific Item Type

$itemsToUpdate = $allItems | Where-Object { 
    $_.item_type_id -eq 5  # Hardware
}

$updatePayload = @{
    id = $item.id
    supplier_id = 42
}

Bulk Price Update

$itemsToUpdate = $allItems | Where-Object {
    $_.name -like "MNT-*"  # All monitor mounts
}

$updatePayload = @{
    id = $item.id
    sell_price = $item.sell_price * 1.10  # 10% increase
}

Troubleshooting

Error: "Cannot deserialize...into type 'Item[]'"

Cause: JSON payload is not an array
Fix: Manually wrap payload in brackets: "[$json]"

Error: "Cannot bind parameter 'ForegroundColor'"

Cause: Write-Log function parameter parsing issue
Fix: Use named parameters: Write-Log -Message "text" -Color Green

Error: "Authentication failed"

Check:

  • Client ID and Secret are correct
  • API application has required permissions (read:items, edit:items)
  • Tenant name is correct (no .halopsa.com suffix)
  • API application is active in HaloPSA

Items not updating despite success message

Check:

  • Verify in HaloPSA UI
  • Check API response for errors
  • Ensure field name is correct (use field inspector)
  • Verify field type matches payload type

Best Practices

  1. Always run dry-run first - Preview changes before execution
  2. Test in dev environment - Before production deployment
  3. Use small batch sizes - Avoid API rate limiting
  4. Add delays between batches - 500ms recommended
  5. Log everything - Timestamped logs for audit trail
  6. Verify in UI - Spot-check after bulk updates
  7. Keep backups - Export data before mass modifications