Skip to content

Export-LicenseDetails

Exports Microsoft 365 license and subscription details to an Excel report.

Description

Connects to Microsoft Graph to retrieve subscription and license assignment data for the tenant. Resolves SKU part numbers to friendly display names using Microsoft's published mapping, calculates commitment and billing terms, and exports the results to a formatted Excel workbook.

Parameters

-GraphCloud

The Microsoft Graph cloud environment to connect to. Valid values: Global, USGov, USGovDoD, Germany, China. Defaults to Global.

Property Value
Type String
Required false
Default Global

-Tenant

The tenant ID to connect to. If omitted, the default tenant for the authenticated account is used.

Property Value
Type String
Required false

-Out

Output file path for the Excel report. Defaults to the user's Downloads folder with a timestamped filename. Automatically appends .xlsx if not present.

Property Value
Type String
Required false

Examples

EXAMPLE 1

.\Export-LicenseDetails.ps1

EXAMPLE 2

.\Export-LicenseDetails.ps1 -Tenant "contoso.onmicrosoft.com" -Out "C:\Reports\licenses.xlsx"

EXAMPLE 3

.\Export-LicenseDetails.ps1 -GraphCloud USGov

Requires -Modules Microsoft.Graph.Authentication, ImportExcel

Script

Download Export-LicenseDetails.ps1

<#
.SYNOPSIS
    Exports Microsoft 365 license and subscription details to an Excel report.

.DESCRIPTION
    Connects to Microsoft Graph to retrieve subscription and license assignment data for
    the tenant. Resolves SKU part numbers to friendly display names using Microsoft's
    published mapping, calculates commitment and billing terms, and exports the results
    to a formatted Excel workbook.

.PARAMETER GraphCloud
    The Microsoft Graph cloud environment to connect to.
    Valid values: Global, USGov, USGovDoD, Germany, China. Defaults to Global.

.PARAMETER Tenant
    The tenant ID to connect to. If omitted, the default tenant for the authenticated
    account is used.

.PARAMETER Out
    Output file path for the Excel report. Defaults to the user's Downloads folder
    with a timestamped filename. Automatically appends .xlsx if not present.

.EXAMPLE
    .\Export-LicenseDetails.ps1

.EXAMPLE
    .\Export-LicenseDetails.ps1 -Tenant "contoso.onmicrosoft.com" -Out "C:\Reports\licenses.xlsx"

.EXAMPLE
    .\Export-LicenseDetails.ps1 -GraphCloud USGov
#>
#Requires -Modules Microsoft.Graph.Authentication, ImportExcel

[CmdletBinding()]
param(
    [ValidateSet('Global', 'USGov', 'USGovDoD', 'Germany', 'China')]
    [string]$GraphCloud = 'Global',
    [string]$Tenant,
    [string]$Out
)

function Log { param([string]$Msg, [ConsoleColor]$Color = 'White') Write-Host "[$(Get-Date -Format 'HH:mm')] $Msg" -ForegroundColor $Color }

$scopes = @(
    'User.Read.All',
    "Directory.Read.All",
    'Organization.Read.All'
)
Log "Connecting to Graph..."
if ($Tenant) { Connect-MgGraph -Scopes $scopes -Environment $GraphCloud -NoWelcome -ErrorAction Stop -TenantId $Tenant }
else { Connect-MgGraph -Scopes $scopes -Environment $GraphCloud -NoWelcome -ErrorAction Stop }
$Org = Get-MgOrganization
Log "Connected to Graph: $($Org.DisplayName)" Green

if (!$Out) {
    $friendlyName = "LicenseDetails"
    $Out = Join-Path $env:USERPROFILE "Downloads\$($Org.DisplayName)-$friendlyName-$(Get-Date -Format 'yyyy-MM-dd_HH-mm').xlsx"
}
elseif ($Out -notlike '*.xlsx') { $Out += '.xlsx' }

# Download Microsoft's SKU display names
Log "Downloading SKU display names..."
$csv = Invoke-RestMethod "https://download.microsoft.com/download/e/3/e/e3e9faf2-f28b-490a-9ada-c6089a1fc5b0/Product%20names%20and%20service%20plan%20identifiers%20for%20licensing.csv"
$productNames = $csv | ConvertFrom-Csv

# Create lookup table
$skuLookup = @{}
foreach ($product in $productNames | Sort-Object String_Id -Unique) {
    if ($product.String_Id) {
        $skuLookup[$product.String_Id] = $product.Product_Display_Name
    }
}

# Get subscription data
Log "Fetching subscription data..."
$subs = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/beta/directory/subscriptions"

# Get SKU data for assigned licenses
$skus = Get-MgSubscribedSku
$skuAssigned = @{}
foreach ($sku in $skus) {
    $skuAssigned[$sku.SkuId] = @{
        Consumed  = $sku.ConsumedUnits
        Total     = $sku.PrepaidUnits.Enabled
        Available = $sku.PrepaidUnits.Enabled - $sku.ConsumedUnits
    }
}

# Create results array
$results = @()

foreach ($sub in $subs.value) {
    # Handle null dates
    $createdDate = if ($sub.createdDateTime) { [DateTime]$sub.createdDateTime } else { $null }
    $renewalDate = if ($sub.nextLifecycleDateTime) { [DateTime]$sub.nextLifecycleDateTime } else { $null }

    # Calculate commitment term
    $commitmentTerm = "Unknown"
    $billingTerm = "Unknown"
    if ($createdDate -and $renewalDate) {
        $termDays = ($renewalDate - $createdDate).Days
        if ($termDays -le 31) { 
            $commitmentTerm = "Monthly"
            $billingTerm = "Monthly"
        }
        elseif ($termDays -le 366) { 
            $commitmentTerm = "1-Year"
            $billingTerm = "Annual"
        }
        elseif ($termDays -le 732) { 
            $commitmentTerm = "2-Year"
            $billingTerm = "Annual"
        }
        else { 
            $commitmentTerm = "3-Year"
            $billingTerm = "Annual"
        }
    }
    elseif ($sub.isTrial) {
        $commitmentTerm = "Trial"
        $billingTerm = "Trial"
    }

    # Get display name
    $displayName = if ($skuLookup[$sub.skuPartNumber]) { 
        $skuLookup[$sub.skuPartNumber] 
    }
    else { 
        $sub.skuPartNumber 
    }

    # Get assigned license info
    $assignedInfo = $skuAssigned[$sub.skuId]

    $results += [PSCustomObject]@{
        'Subscription Name'  = $displayName
        'Status'             = $sub.status
        'Total Licenses'     = $sub.totalLicenses
        'Assigned Licenses'  = if ($assignedInfo) { $assignedInfo.Consumed } else { 0 }
        'Available Licenses' = if ($assignedInfo) { $assignedInfo.Available } else { $sub.totalLicenses }
        'Created Date'       = if ($createdDate) { $createdDate.ToString("yyyy-MM-dd") } else { "N/A" }
        'Renewal Date'       = if ($renewalDate) { $renewalDate.ToString("yyyy-MM-dd") } else { "N/A" }
        'Commitment'         = $commitmentTerm
        'Billing'            = $billingTerm
        'Is Trial'           = $sub.isTrial
        'Subscription ID'    = $sub.commerceSubscriptionId
        'SKU Part Number'    = $sub.skuPartNumber
        'SKU ID'             = $sub.skuId
    }
}

# Export to Excel
$results | Export-Excel -Path $Out `
    -AutoSize `
    -AutoFilter `
    -TableName "Subscriptions" `
    -TableStyle Medium2 `
    -FreezeTopRow `
    -WorksheetName "Subscriptions"

Log "Exported report: $Out" Green

$answer = Read-Host "Open the report now? [Y/n]"
if ($answer -eq '' -or $answer -match '^y') {
    Start-Process $Out
}