There is no easy way to offboard a set of devices from the Microsoft 365 Defender portal. You should be able to mark a set of devices and hit a button called offboard, but no such thing exists. Luckily, we have Powershell and Microsoft API to solve this.

We need to perform a few tasks to set this up:

  • Create an application service principal
  • Give service principal necessary permissions
  • Change values in script
  • Run script

Create an application service principal

Go to Azure AD –> App Registrations –> New registration.
Give the application a suitable name that explains what it will do, leave default choices and click on Register to create the application. Now we need to collect some basic information from the application.
In the application overview you will find Application ID and Tenant ID, copy these values, you will need them later.

Go to Certificates & secrets, and create a new client secret with expiration you need. Copy the value from this secret, this will only be visible once!

Give service principal necessary permissions

Go to API permission and add the following permissions, if you’re not an admin you may have to ask an admin to approve these permission after you have added them.

Change values in script

Add you tenant ID, Application ID and Application secret to the variables in the script on line 2, 3 and 4.
In my example I create the query to only get inactive machines where lastseen is prior to 1st February. Change this value if you want to use another date.

Run script

Some considerations

Microsoft’s API have some limitations, these are:

  • 100 calls per minute
  • 1500 calls per hour

Because of these limitation I’ve added a 3 seconds sleep to keep calls under the limit. One call every 3 seconds will be 20 calls per minute and 1200 per hour.

Timeout for token is 1 hour. To prevent getting unauthorized responses from API in the middle of the script, token will renew every 50 minutes.

I’ve also added a $NextlLink which will get remaining devices over 10000, since that is maximum response from Microsoft’s API.

Some OS platforms are not supported, so the query only includes windows 10 and windows 11.

Client agent version must be greater than 10.2930.0.0, this is included in the query as well.

#Connecting with our service principal
$tenantId = '' ### Paste your tenant ID here
$appId = '' ### Paste your Application ID here
$appSecret = '' ### Paste your Application key here
$resourceAppIdUri = ''
$oAuthUri = "$TenantId/oauth2/token"
$authBody = [Ordered] @{
    resource = "$resourceAppIdUri"
    client_id = "$appId"
    client_secret = "$appSecret"
    grant_type = 'client_credentials'

$authResponse = Invoke-RestMethod -Method Post -Uri $oAuthUri -Body $authBody -ErrorAction Stop
$token = $authResponse.access_token
$headers = @{
    'Content-Type' = 'application/json'
    'Accept' = 'application/json'
    'Authorization' = "Bearer $token"

$JSON = @'
{"Comment":"Offboard machine by powershell automation"

$MachineURI = ""
$Response = Invoke-RestMethod -Method Get -Headers $headers -Uri $MachineURI -ErrorAction Stop
$Machines = $Response.Value
$NextLink = $Response."@odata.nextLink"
while ($NextLink -ne $null) {
    $Response = (Invoke-RestMethod -Uri $NextLink -Headers $headers -Method Get -Verbose)
    $NextLink = $Response."@odata.nextLink"
    $Machines += $Response.Value

#Lastseen format is year-month-day
$inactiveDevices = $Machines | where-object { ($_.HealthStatus -eq "Inactive") -and ($_.lastSeen -lt '2022-02-01') -and ($_.osPlatform -like "Windows1*") -and ($_.agentVersion -gt "10.2930.0.0") }

$timer = [Diagnostics.Stopwatch]::StartNew()

Foreach ($inactivedevice in $inactiveDevices) {
    $DeviceUri = "$($"
    $Response = Invoke-RestMethod -Uri $DeviceUri -Headers $headers -Method Post -Body $JSON -ContentType 'application/json' -Verbose
    sleep -Seconds 3

    #Renew credentials after 50 minutes to avoid disconnecting while running the script
    if($timer.Elapsed.minutes -eq 50) {
        $authResponse = Invoke-RestMethod -Method Post -Uri $oAuthUri -Body $authBody -ErrorAction Stop
        $token = $authResponse.access_token
        $headers = @{
            'Content-Type' = 'application/json'
            'Accept' = 'application/json'
            'Authorization' = "Bearer $token"


It can take up to 24 hours to remove the devices. If you rerun the script you will receive an error explaining that the action is already in progress.

Tips to take it a step further

The good thing about using a service principal is that the script can be added as a runbook in Azure without changing the script. From there you can run it on interval, daily, weekly, whatever your preference is.