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.
site1.erralert.com