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:
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:
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
- Test in dev environment first
- Run dry-run - Review log file
- Verify preview - Confirm correct items selected
- Set
$dryRun = $false - Run live update - Type
YESto confirm - Verify in UI - Spot-check random items
- Deploy to production - After dev verification
Authentication
All scripts require HaloPSA API credentials:
- Configuration → Integrations → HaloPSA API → View Applications
- Create application - Select "Services" authentication
- Set permissions -
read:items,edit:items - Copy Client ID and Client Secret - Secret shown only once
- 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.comsuffix) - 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
- Always run dry-run first - Preview changes before execution
- Test in dev environment - Before production deployment
- Use small batch sizes - Avoid API rate limiting
- Add delays between batches - 500ms recommended
- Log everything - Timestamped logs for audit trail
- Verify in UI - Spot-check after bulk updates
- Keep backups - Export data before mass modifications