site1.erralert.com

← Services & processes

Microsoft 365 license usage (Graph API) powershell

Pulls per-SKU consumed vs available license counts from Microsoft Graph. One-time setup: register an Azure AD app, grant it `Organization.Read.All` (Application) with admin consent, generate a client secret, paste TenantId/ClientId/ClientSecret below (or pull them from a vault). The script then runs unattended.

Placeholders only. Before running, replace YOUR_URL with your capture endpoint's POST URL . (Open this page from your capture object to have these auto-filled.)
# m365-license-usage.ps1 — assigned vs total per M365 SKU via Microsoft Graph.
#
# One-time setup:
#   1. Register an Azure AD application (App registrations → New registration).
#   2. Under API permissions: Microsoft Graph → Application permissions
#      → Organization.Read.All. Grant admin consent.
#   3. Certificates & secrets → New client secret. Copy the VALUE (not the id).
#   4. Note your Directory (tenant) ID and Application (client) ID.
#   5. Fill in the three values below — or read them from a vault / env vars.

$Url          = "YOUR_URL/m365licenses"
$TenantId     = "YOUR_TENANT_ID"
$ClientId     = "YOUR_APP_ID"
$ClientSecret = "YOUR_APP_SECRET"

# --- exchange client credentials for a Graph token --------------------------
try {
  $Token = Invoke-RestMethod -Method Post `
    -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" `
    -Body @{
      client_id     = $ClientId
      client_secret = $ClientSecret
      scope         = "https://graph.microsoft.com/.default"
      grant_type    = "client_credentials"
    }
} catch {
  $Body = (@{ error = "token request failed: $($_.Exception.Message)" }) | ConvertTo-Json -Compress
  Invoke-RestMethod -Uri $Url -Method Post -Body $Body -ContentType "application/json" -TimeoutSec 15 | Out-Null
  exit 1
}

# --- enumerate subscribed SKUs ---------------------------------------------
$Subs = Invoke-RestMethod -Method Get `
  -Uri "https://graph.microsoft.com/v1.0/subscribedSkus" `
  -Headers @{ Authorization = "Bearer $($Token.access_token)" }

$Skus    = @{}
$AnyFull = $false
foreach ($s in $Subs.value) {
  $consumed  = [int]$s.consumedUnits
  $enabled   = [int]$s.prepaidUnits.enabled
  $available = $enabled - $consumed
  $Skus[$s.skuPartNumber] = @{
    consumed   = $consumed
    enabled    = $enabled
    available  = $available
    pct_used   = if ($enabled -gt 0) { [math]::Round(($consumed / $enabled) * 100, 1) } else { 0 }
  }
  if ($enabled -gt 0 -and $available -le 0) { $AnyFull = $true }
}

$Body = (@{
  tenant_id  = $TenantId
  sku_count  = $Skus.Count
  skus       = $Skus
  any_full   = $AnyFull
}) | ConvertTo-Json -Compress -Depth 5

Invoke-RestMethod -Uri $Url -Method Post -Body $Body `
  -ContentType "application/json" -TimeoutSec 15 | Out-Null

Recommended pairing

Add a capture.value check to this capture object.
json_path = any_full, op = ==, threshold = true (severity: warn — running out of seats). Or per-SKU: json_path = skus.ENTERPRISEPACK.available, op = <, threshold = 5.

What is the filename?

m365-license-usage.ps1 — this is the suggested name for the downloaded file. Rename freely if you prefer.