<# .SYNOPSIS Read-only inventory of a UniFi Network application through the official UniFi Network Integration API (sites, devices, clients, networks and, optionally, firewall zones and policies). Exports one CSV per object type. .DESCRIPTION The script only sends HTTP GET requests to the documented endpoints of the UniFi Network API (developer.ui.com, Network API v10.6.106): GET /v1/info GET /v1/sites GET /v1/sites/{siteId}/devices GET /v1/sites/{siteId}/clients GET /v1/sites/{siteId}/networks GET /v1/sites/{siteId}/firewall/zones (with -IncludeFirewall) GET /v1/sites/{siteId}/firewall/policies (with -IncludeFirewall) It never creates, updates or deletes anything on the console. Two connection modes are supported, matching the two server URLs published in the official OpenAPI specification: - Local : https:///proxy/network/integration - Remote : https://api.ui.com/v1/connector/consoles//proxy/network/integration Authentication uses an API key sent in the X-API-Key header. For the local mode, create the key in the Integrations section of the local UniFi Network application; for the remote mode, use a key created in UniFi Site Manager. Results are paginated by the API (offset/limit, limit max 200); the script follows totalCount until every item has been read. .PARAMETER ConsoleHost IP address or FQDN of the UniFi console for the local mode, optionally with a port (for example 192.168.1.1 or unifi.example.local:443). .PARAMETER ConsoleId Console ID for the remote mode through the Site Manager Cloud Connector. .PARAMETER ApiKey API key as a SecureString. If omitted, the script reads the environment variable UNIFI_API_KEY, then prompts for the key. .PARAMETER OutputDirectory Folder for the CSV files. Created if missing. Defaults to the current folder. .PARAMETER PageSize Number of items requested per page (1 to 200). Defaults to 200. .PARAMETER IncludeFirewall Also exports firewall zones and firewall policies (read-only GET calls). .PARAMETER SkipCertificateCheck Accepts the self-signed certificate of a local console. Use only on a trusted management network. On PowerShell 7 this maps to the -SkipCertificateCheck parameter of Invoke-RestMethod; on Windows PowerShell 5.1 a temporary certificate validation callback is used and the previous callback is restored when the script ends. .EXAMPLE $key = Read-Host -AsSecureString -Prompt 'UniFi API key' .\Get-UniFiInventory.ps1 -ConsoleHost 192.168.1.1 -ApiKey $key -SkipCertificateCheck -OutputDirectory C:\Audit\UniFi .EXAMPLE $env:UNIFI_API_KEY = '' # session only, do not store in scripts .\Get-UniFiInventory.ps1 -ConsoleId '' -IncludeFirewall .NOTES Version : 1.0 (2026-10-26) Target : UniFi Network API as documented for UniFi Network 10.6.106 on developer.ui.com Runtime : Windows PowerShell 5.1 and PowerShell 7.x Status : parsed and checked with PSScriptAnalyzer; not executed against a UniFi console by the author. Test it on a lab site first. Read-only: GET requests only. Source: ANYSYS ICT, https://www.anysys.be/articles/ #> [CmdletBinding(DefaultParameterSetName = 'Local')] param( [Parameter(Mandatory = $true, ParameterSetName = 'Local')] [ValidateNotNullOrEmpty()] [string]$ConsoleHost, [Parameter(Mandatory = $true, ParameterSetName = 'Remote')] [ValidateNotNullOrEmpty()] [string]$ConsoleId, [System.Security.SecureString]$ApiKey, [string]$OutputDirectory = (Get-Location).Path, [ValidateRange(1, 200)] [int]$PageSize = 200, [switch]$IncludeFirewall, [switch]$SkipCertificateCheck ) Set-StrictMode -Version 2.0 $ErrorActionPreference = 'Stop' # --------------------------------------------------------------------------- # Resolve the API key (parameter, then environment variable, then prompt) # --------------------------------------------------------------------------- if (-not $ApiKey) { if ($env:UNIFI_API_KEY) { $ApiKey = New-Object System.Security.SecureString foreach ($ch in $env:UNIFI_API_KEY.ToCharArray()) { $ApiKey.AppendChar($ch) } $ApiKey.MakeReadOnly() } else { $ApiKey = Read-Host -AsSecureString -Prompt 'UniFi API key' } } function ConvertTo-PlainText { param([Parameter(Mandatory = $true)][System.Security.SecureString]$Secure) $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secure) try { return [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } # --------------------------------------------------------------------------- # Base URL (both forms come from the "servers" section of the OpenAPI spec) # --------------------------------------------------------------------------- if ($PSCmdlet.ParameterSetName -eq 'Local') { $baseUrl = 'https://{0}/proxy/network/integration' -f $ConsoleHost.TrimEnd('/') $target = $ConsoleHost } else { $baseUrl = 'https://api.ui.com/v1/connector/consoles/{0}/proxy/network/integration' -f $ConsoleId $target = "console $ConsoleId (Cloud Connector)" } $isCore = $PSVersionTable.PSEdition -eq 'Core' $pageLimit = $PageSize $previousCallback = $null $previousProtocol = $null if (-not $isCore) { # Windows PowerShell 5.1: make sure TLS 1.2 is offered $previousProtocol = [System.Net.ServicePointManager]::SecurityProtocol [System.Net.ServicePointManager]::SecurityProtocol = $previousProtocol -bor [System.Net.SecurityProtocolType]::Tls12 if ($SkipCertificateCheck) { $previousCallback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } } } function Invoke-UniFiGet { <# Sends one GET request with the API key header; retries on HTTP 429. #> param( [Parameter(Mandatory = $true)][string]$Uri, [Parameter(Mandatory = $true)][hashtable]$Headers ) $params = @{ Uri = $Uri; Method = 'Get'; Headers = $Headers; ErrorAction = 'Stop' } if ($isCore -and $SkipCertificateCheck) { $params['SkipCertificateCheck'] = $true } for ($attempt = 1; $attempt -le 3; $attempt++) { try { return Invoke-RestMethod @params } catch { $status = $null $retryAfter = 5 $response = Get-PropertyValue $_.Exception 'Response' if ($response) { $status = [int]$response.StatusCode try { if ($isCore) { # HttpResponseMessage (PowerShell 7) $delta = $response.Headers.RetryAfter.Delta if ($delta) { $retryAfter = [int][Math]::Ceiling($delta.TotalSeconds) } } else { # HttpWebResponse (Windows PowerShell 5.1) $ra = $response.Headers['Retry-After'] if ($ra) { $retryAfter = [int]("$ra".Trim()) } } } catch { Write-Verbose 'No usable Retry-After header.' } } if ($status -eq 429 -and $attempt -lt 3) { Write-Warning "HTTP 429 on $Uri, waiting $retryAfter s (attempt $attempt of 3)." Start-Sleep -Seconds $retryAfter continue } $hint = switch ($status) { 401 { 'API key missing or invalid.' } 403 { 'API key valid but not allowed for this resource.' } 404 { 'Endpoint not found: check the console address, the connection mode and the Network version.' } default { 'See the error message below.' } } throw "GET $Uri failed (HTTP $status). $hint $($_.Exception.Message)" } } } function Get-UniFiPagedItem { <# Reads every page of a list endpoint (offset/limit/totalCount). #> param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][hashtable]$Headers ) $items = New-Object System.Collections.Generic.List[object] $offset = 0 do { $uri = '{0}{1}?offset={2}&limit={3}' -f $baseUrl, $Path, $offset, $pageLimit $page = Invoke-UniFiGet -Uri $uri -Headers $Headers foreach ($item in @($page.data)) { $items.Add($item) } $count = [int]$page.count $total = [int64]$page.totalCount $offset += $count Write-Verbose ("{0}: {1}/{2}" -f $Path, $offset, $total) } while ($count -gt 0 -and $offset -lt $total) return , $items.ToArray() } function Get-PropertyValue { <# Returns a property value or $null when the property is absent (StrictMode safe). #> param($InputObject, [Parameter(Mandatory = $true)][string]$Name) if ($null -eq $InputObject) { return $null } $prop = $InputObject.PSObject.Properties[$Name] if ($prop) { return $prop.Value } return $null } function ConvertTo-IsoText { <# PowerShell 7 turns ISO 8601 strings into DateTime; keep a stable ISO text in the CSV. #> param($Value) if ($Value -is [datetime]) { return $Value.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') } return $Value } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- $plainKey = ConvertTo-PlainText -Secure $ApiKey $headers = @{ 'X-API-Key' = $plainKey; 'Accept' = 'application/json' } try { if (-not (Test-Path -LiteralPath $OutputDirectory)) { New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null } $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' $info = Invoke-UniFiGet -Uri ($baseUrl + '/v1/info') -Headers $headers $appVersion = Get-PropertyValue $info 'applicationVersion' Write-Host ("UniFi Network application {0} on {1}" -f $appVersion, $target) $sites = Get-UniFiPagedItem -Path '/v1/sites' -Headers $headers $siteRows = New-Object System.Collections.Generic.List[object] $deviceRows = New-Object System.Collections.Generic.List[object] $clientRows = New-Object System.Collections.Generic.List[object] $networkRows = New-Object System.Collections.Generic.List[object] $zoneRows = New-Object System.Collections.Generic.List[object] $policyRows = New-Object System.Collections.Generic.List[object] foreach ($site in $sites) { $siteId = $site.id $siteName = $site.name Write-Host "Site: $siteName ($siteId)" $devices = Get-UniFiPagedItem -Path "/v1/sites/$siteId/devices" -Headers $headers $deviceNames = @{} foreach ($d in $devices) { $deviceNames[[string]$d.id] = $d.name $deviceRows.Add([pscustomobject]@{ Site = $siteName Name = $d.name Model = $d.model State = $d.state IPAddress = Get-PropertyValue $d 'ipAddress' MACAddress = Get-PropertyValue $d 'macAddress' FirmwareVersion = Get-PropertyValue $d 'firmwareVersion' FirmwareUpdatable = Get-PropertyValue $d 'firmwareUpdatable' Features = (@(Get-PropertyValue $d 'features') -join ';') Supported = Get-PropertyValue $d 'supported' DeviceId = $d.id }) } $clients = Get-UniFiPagedItem -Path "/v1/sites/$siteId/clients" -Headers $headers foreach ($c in $clients) { $uplinkId = [string](Get-PropertyValue $c 'uplinkDeviceId') $uplinkName = $null if ($uplinkId -and $deviceNames.ContainsKey($uplinkId)) { $uplinkName = $deviceNames[$uplinkId] } $clientRows.Add([pscustomobject]@{ Site = $siteName Name = Get-PropertyValue $c 'name' Type = Get-PropertyValue $c 'type' IPAddress = Get-PropertyValue $c 'ipAddress' MACAddress = Get-PropertyValue $c 'macAddress' ConnectedAt = ConvertTo-IsoText (Get-PropertyValue $c 'connectedAt') UplinkName = $uplinkName UplinkId = $uplinkId ClientId = $c.id }) } $networks = Get-UniFiPagedItem -Path "/v1/sites/$siteId/networks" -Headers $headers foreach ($n in $networks) { $networkRows.Add([pscustomobject]@{ Site = $siteName Name = $n.name VlanId = Get-PropertyValue $n 'vlanId' Management = Get-PropertyValue $n 'management' Enabled = Get-PropertyValue $n 'enabled' Default = Get-PropertyValue $n 'default' ZoneId = Get-PropertyValue $n 'zoneId' NetworkId = $n.id }) } if ($IncludeFirewall) { $zones = Get-UniFiPagedItem -Path "/v1/sites/$siteId/firewall/zones" -Headers $headers $zoneNames = @{} foreach ($z in $zones) { $zoneNames[[string]$z.id] = $z.name $zoneRows.Add([pscustomobject]@{ Site = $siteName Zone = $z.name Origin = Get-PropertyValue (Get-PropertyValue $z 'metadata') 'origin' NetworkIds = (@(Get-PropertyValue $z 'networkIds') -join ';') ZoneId = $z.id }) } $policies = Get-UniFiPagedItem -Path "/v1/sites/$siteId/firewall/policies" -Headers $headers foreach ($p in $policies) { $srcZone = [string](Get-PropertyValue (Get-PropertyValue $p 'source') 'zoneId') $dstZone = [string](Get-PropertyValue (Get-PropertyValue $p 'destination') 'zoneId') $policyRows.Add([pscustomobject]@{ Site = $siteName Index = Get-PropertyValue $p 'index' Name = Get-PropertyValue $p 'name' Enabled = Get-PropertyValue $p 'enabled' Action = Get-PropertyValue (Get-PropertyValue $p 'action') 'type' SourceZone = $(if ($zoneNames.ContainsKey($srcZone)) { $zoneNames[$srcZone] } else { $srcZone }) DestinationZone = $(if ($zoneNames.ContainsKey($dstZone)) { $zoneNames[$dstZone] } else { $dstZone }) LoggingEnabled = Get-PropertyValue $p 'loggingEnabled' Origin = Get-PropertyValue (Get-PropertyValue $p 'metadata') 'origin' PolicyId = $p.id }) } } $siteRows.Add([pscustomobject]@{ Site = $siteName InternalReference = Get-PropertyValue $site 'internalReference' Devices = @($devices).Count DevicesOffline = @($devices | Where-Object { $_.state -ne 'ONLINE' }).Count FirmwareUpdatable = @($devices | Where-Object { (Get-PropertyValue $_ 'firmwareUpdatable') -eq $true }).Count ConnectedClients = @($clients).Count Networks = @($networks).Count ApplicationVersion = $appVersion SiteId = $siteId }) } $exports = [ordered]@{ 'sites' = $siteRows 'devices' = $deviceRows 'clients' = $clientRows 'networks' = $networkRows } if ($IncludeFirewall) { $exports['firewall-zones'] = $zoneRows $exports['firewall-policies'] = $policyRows } foreach ($name in $exports.Keys) { $file = Join-Path $OutputDirectory ("unifi-{0}-{1}.csv" -f $name, $stamp) $rows = $exports[$name].ToArray() $rows | Export-Csv -LiteralPath $file -NoTypeInformation -Encoding UTF8 Write-Host ("{0,-18} {1,6} rows -> {2}" -f $name, $rows.Length, $file) } $siteRows | Format-Table Site, Devices, DevicesOffline, FirmwareUpdatable, ConnectedClients, Networks -AutoSize } finally { $plainKey = $null $headers = $null if (-not $isCore) { if ($SkipCertificateCheck) { [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $previousCallback } if ($null -ne $previousProtocol) { [System.Net.ServicePointManager]::SecurityProtocol = $previousProtocol } } }