NinjaOne API Scripts
NinjaOne API Examples

Get All Devices with a Piece of Software Installed

This script finds all devices managed by NinjaOne that have a specific piece of software installed (defaults to Google Chrome)

PowerShell
# --------------------------------------------------
# Author: Gavin Stone (NinjaOne)
# Attribution: Luke Whitelock (NinjaOne) for his work on the Authentication Functions
# Date: 2026-03-10
# Description: Find all devices managed by NinjaOne that have Google Chrome installed. Return the device name, OS, and the Chrome version found.

# Version: 1.0
# --------------------------------------------------

# User editable variables:
$NinjaOneInstance = 'eu.ninjarmm.com' # Please replace with the region instance you login to (app.ninjarmm.com, us2.ninjarmm.com, eu.ninjarmm.com, ca.ninjarmm.com, oc.ninjarmm.com)
$NinjaOneClientId = ''
$NinjaOneClientSecret = ''

# Regex pattern to match software names against (e.g. 'Google Chrome', 'Firefox')
$SoftwareNameFilter = 'Google Chrome'

# Functions for Authentication
function Get-NinjaOneToken {
    [CmdletBinding()]
    param()

    if ($Script:NinjaOneInstance -and $Script:NinjaOneClientID -and $Script:NinjaOneClientSecret ) {
        if ($Script:NinjaTokenExpiry -and (Get-Date) -lt $Script:NinjaTokenExpiry) {
            return $Script:NinjaToken
        }
        else {

            if ($Script:NinjaOneRefreshToken) {
                $Body = @{
                    'grant_type'    = 'refresh_token'
                    'client_id'     = $Script:NinjaOneClientID
                    'client_secret' = $Script:NinjaOneClientSecret
                    'refresh_token' = $Script:NinjaOneRefreshToken
                }
            }
            else {

                $body = @{
                    grant_type    = 'client_credentials'
                    client_id     = $Script:NinjaOneClientID
                    client_secret = $Script:NinjaOneClientSecret
                    scope         = 'monitoring management'
                }
            }

            $token = Invoke-RestMethod -Uri "https://$($Script:NinjaOneInstance -replace '/ws','')/ws/oauth/token" -Method Post -Body $body -ContentType 'application/x-www-form-urlencoded' -UseBasicParsing

            $Script:NinjaTokenExpiry = (Get-Date).AddSeconds($Token.expires_in)
            $Script:NinjaToken = $token

            Write-Host 'Fetched New Token'
            return $token
        }
    }
    else {
        Throw 'Please run Connect-NinjaOne first'
    }

}

function Connect-NinjaOne {
    [CmdletBinding()]
    param (
        [Parameter(mandatory = $true)]
        $NinjaOneInstance,
        [Parameter(mandatory = $true)]
        $NinjaOneClientID,
        [Parameter(mandatory = $true)]
        $NinjaOneClientSecret,
        $NinjaOneRefreshToken
    )

    $Script:NinjaOneInstance = $NinjaOneInstance
    $Script:NinjaOneClientID = $NinjaOneClientID
    $Script:NinjaOneClientSecret = $NinjaOneClientSecret
    $Script:NinjaOneRefreshToken = $NinjaOneRefreshToken


    try {
        $Null = Get-NinjaOneToken -ea Stop
    }
    catch {
        Throw "Failed to Connect to NinjaOne: $_"
    }

}

function Invoke-NinjaOneRequest {
    param(
        $Method,
        $Body,
        $InputObject,
        $Path,
        $QueryParams,
        [Switch]$Paginate,
        [Switch]$AsArray
    )

    $Token = Get-NinjaOneToken

    if ($InputObject) {
        if ($AsArray) {
            $Body = $InputObject | ConvertTo-Json -depth 100
            if (($InputObject | Measure-Object).count -eq 1 ) {
                $Body = '[' + $Body + ']'
            }
        }
        else {
            $Body = $InputObject | ConvertTo-Json -depth 100
        }
    }

    try {
        if ($Method -in @('GET', 'DELETE')) {
            if ($Paginate) {

                $After = 0
                $PageSize = 1000
                $NinjaResult = do {
                    $Result = Invoke-WebRequest -uri "https://$($Script:NinjaOneInstance)/api/v2/$($Path)?pageSize=$PageSize&after=$After$(if ($QueryParams){"&$QueryParams"})" -Method $Method -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json' -UseBasicParsing
                    $Result
                    $ResultCount = ($Result.id | Measure-Object -Maximum)
                    $After = $ResultCount.maximum

                } while ($ResultCount.count -eq $PageSize)
            }
            else {
                $NinjaResult = Invoke-WebRequest -uri "https://$($Script:NinjaOneInstance)/api/v2/$($Path)$(if ($QueryParams){"?$QueryParams"})" -Method $Method -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -UseBasicParsing
            }

        }
        elseif ($Method -in @('PATCH', 'PUT', 'POST')) {
            $NinjaResult = Invoke-WebRequest -uri "https://$($Script:NinjaOneInstance)/api/v2/$($Path)$(if ($QueryParams){"?$QueryParams"})" -Method $Method -Headers @{Authorization = "Bearer $($token.access_token)" } -Body $Body -ContentType 'application/json; charset=utf-8' -UseBasicParsing
        }
        else {
            Throw 'Unknown Method'
        }
    }
    catch {
        Throw "Error Occured: $_"
    }

    try {
        return $NinjaResult.content | ConvertFrom-Json -ea stop
    }
    catch {
        return $NinjaResult.content
    }

}

# Connect to NinjaOne API
try {
    Connect-NinjaOne -NinjaOneInstance $NinjaOneInstance -NinjaOneClientID $NinjaOneClientId -NinjaOneClientSecret $NinjaOneClientSecret
}
catch {
    Write-Output "Failed to connect to NinjaOne API: $_"
    exit 1
}

# ─── Script Logic ─────────────────────────────────────────────────────────────

# Query all software inventory in bulk using cursor pagination
Write-Host "Fetching software inventory..."
$AllSoftware = [System.Collections.Generic.List[PSCustomObject]]::new()
$CursorName = $null
$PageSize = 1000

do {
    $QueryParams = "pageSize=$PageSize"
    if ($CursorName) {
        $QueryParams += "&cursor=$CursorName"
    }

    $Response = Invoke-NinjaOneRequest -Method GET -Path 'queries/software' -QueryParams $QueryParams

    if ($Response.results) {
        foreach ($Item in $Response.results) {
            $AllSoftware.Add($Item)
        }
    }

    $CursorName = $Response.cursor.name
    $PageCount = if ($Response.results) { $Response.results.Count } else { 0 }
    Write-Host "  Retrieved $($AllSoftware.Count) records so far..."
} while ($CursorName -and $PageCount -eq $PageSize)

Write-Host "Retrieved $($AllSoftware.Count) total software record(s)"

# Filter to matching software
$MatchingSoftware = $AllSoftware | Where-Object { $_.name -match $SoftwareNameFilter }

if ($MatchingSoftware.Count -eq 0) {
    Write-Host "No installations found matching '$SoftwareNameFilter'."
    exit 0
}

# Get unique device IDs that have the software
$DeviceIds = $MatchingSoftware | Select-Object -ExpandProperty deviceId -Unique
Write-Host "Found $($MatchingSoftware.Count) installation(s) across $($DeviceIds.Count) device(s). Fetching device details..."

# Fetch all devices to resolve names (single paginated call)
$AllDevices = Invoke-NinjaOneRequest -Method GET -Path 'devices' -Paginate
$DeviceLookup = @{}
foreach ($Device in $AllDevices) {
    $DeviceLookup[$Device.id] = $Device
}

# Build results
$Results = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($App in $MatchingSoftware) {
    $Device = $DeviceLookup[$App.deviceId]
    $Results.Add([PSCustomObject]@{
        DeviceName   = if ($Device) { $Device.systemName } else { "Unknown (ID: $($App.deviceId))" }
        DeviceId     = $App.deviceId
        DeviceClass  = if ($Device) { $Device.nodeClass } else { 'N/A' }
        SoftwareName = $App.name
        Version      = $App.version
        Publisher    = $App.publisher
        InstallDate  = $App.installDate
    })
}

Write-Host "`nFound $($Results.Count) installation(s) across $($DeviceIds.Count) device(s)."
$Results | Format-Table -AutoSize

# Uncomment the next line to export to a CSV
$Results | Export-Csv -Path ('C:\Temp\NinjaOneSoftwareReport_' + (Get-Date -f "yyyyMMdd_HHmm") + '.csv') -NoTypeInformation

Get-DevicesWithSoftware

Overview

This script finds all devices managed by NinjaOne that have a specific piece of software installed (defaults to Google Chrome). It uses the bulk software inventory query endpoint for efficient retrieval across the entire estate, then resolves device names from the devices endpoint.

Attribution

  • Author: Gavin Stone (NinjaOne)

Requirements / Prerequisites

  • NinjaOne API Credentials:
    • NinjaOneClientId
    • NinjaOneClientSecret
  • NinjaOne Instance URL: e.g., eu.ninjarmm.com

How It Works

  1. Authentication: The script authenticates with the NinjaOne API using OAuth2 Client Credentials, obtaining an access token via Connect-NinjaOne.
  2. Bulk Software Query: Retrieves the entire software inventory via queries/software using cursor-based pagination, collecting all records in a single bulk operation rather than querying each device individually.
  3. Filter Matches: Filters the software list using the $SoftwareNameFilter regex pattern (default: Google Chrome).
  4. Resolve Device Names: Fetches all devices via devices to build a lookup table, mapping device IDs from the software results to device names and classes.
  5. Output Results: Displays a summary count and a formatted table of all matching installations with device name, version, publisher, and install date.

Usage

  1. Set Variables:

    • Open Get-DevicesWithSoftware.ps1 in an editor (e.g., VS Code, PowerShell ISE).

    • Fill in your NinjaOne credentials and any script-specific variables at the top:

      $NinjaOneInstance     = "eu.ninjarmm.com"
      $NinjaOneClientId     = "your_client_id"
      $NinjaOneClientSecret = "your_client_secret"
      $SoftwareNameFilter = 'Google Chrome'
  2. Run the Script:

    .\Get-DevicesWithSoftware.ps1

Expected Output

Fetching software inventory... Retrieved 1000 records so far... Retrieved 2000 records so far... Retrieved 2420 records so far... Retrieved 2420 total software record(s) Found 12 installation(s) across 12 device(s). Fetching device details... Found 12 installation(s) across 12 device(s). DeviceName DeviceId DeviceClass SoftwareName Version Publisher InstallDate ---------- -------- ----------- ------------ ------- --------- ----------- DESK-001 142 WINDOWS_WORKSTATION Google Chrome 133.0.6943.142 Google LLC 2025-01-15 DESK-002 287 WINDOWS_WORKSTATION Google Chrome 133.0.6943.142 Google LLC 2025-02-01 SRV-WEB-01 403 WINDOWS_SERVER Google Chrome 133.0.6943.142 Google LLC 2024-11-20 LAPTOP-JS 891 WINDOWS_WORKSTATION Google Chrome 132.0.6834.83 Google LLC 2025-01-10

Troubleshooting

  • Issue: Authentication fails with an error message.

    • Solution: Verify that $NinjaOneClientId and $NinjaOneClientSecret are correct and that the API client has the required scopes (monitoring, management).
  • Issue: The script cannot connect to the NinjaOne API.

    • Solution: Ensure that $NinjaOneInstance is correct (e.g., eu.ninjarmm.com, app.ninjarmm.com, oc.ninjarmm.com) and accessible from your network.
  • Issue: No results returned but the software is known to be installed.

    • Solution: Check that $SoftwareNameFilter matches the display name shown in NinjaOne. The filter uses regex — special characters like + or . may need escaping (e.g. \.NET instead of .NET).

Notes

  • Ensure that your NinjaOne API credentials are kept secure and not shared.
  • This script uses the bulk queries/software endpoint which retrieves the entire software inventory in just 2 paginated API calls (software + devices) — much faster than the per-device approach which requires N+1 calls.
  • The queries/software endpoint uses named cursor pagination (different from the after-based pagination used by devices), which is handled directly in the script.
  • Change $SoftwareNameFilter to search for any software — e.g. 'Firefox', '7-Zip', 'Microsoft Teams'.
  • Uncomment the last line of the script to export results to a CSV file.