Public/Remove-GkUserLicense.ps1

function Remove-GkUserLicense {
    <#
    .SYNOPSIS
        Remove one or more license SKUs from users, reclaiming the seats.

    .DESCRIPTION
        Calls POST /users/{id}/assignLicense with the SKU(s) in removeLicenses, releasing those
        licenses from the user. Typically used to reclaim licenses from disabled or stale accounts.

        State-changing: supports -WhatIf / -Confirm and prompts by default. Accepts users from the
        pipeline and yields a PSGraphKit.LicenseRemoveResult per user; a failure warns and continues.

        SKU GUIDs come from Get-GkLicenseOverview (the SkuId column). Note: a license assigned via
        group-based licensing cannot be removed per user — that returns a Graph error (reported as a
        Failed result); change the group assignment instead.

        Requires LicenseAssignment.ReadWrite.All (or Directory.ReadWrite.All / User.ReadWrite.All)
        plus a supporting Entra role (e.g. License Administrator or User Administrator).

    .PARAMETER UserId
        One or more user object IDs or userPrincipalNames. Accepts pipeline input (incl. by the
        UserPrincipalName / Id property).

    .PARAMETER SkuId
        One or more SKU GUIDs to remove (from Get-GkLicenseOverview's SkuId).

    .EXAMPLE
        Remove-GkUserLicense -UserId ada@contoso.com -SkuId 6fd2c87f-b296-42f0-b197-1e91e994b900

        Remove one SKU from one user (prompts for confirmation).

    .EXAMPLE
        $e3 = (Get-GkLicenseOverview | Where-Object SkuPartNumber -eq 'ENTERPRISEPACK').SkuId
        Get-GkStaleUser -InactiveDays 365 | Remove-GkUserLicense -SkuId $e3 -WhatIf

        Preview reclaiming Office 365 E3 from users stale 365+ days.

    .EXAMPLE
        'ada@contoso.com','bob@contoso.com' | Remove-GkUserLicense -SkuId $skuId -Confirm:$false |
            Where-Object Outcome -eq 'Failed'

        Remove a SKU from several users without prompting and inspect any failures.

    .OUTPUTS
        PSGraphKit.LicenseRemoveResult
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [OutputType('PSGraphKit.LicenseRemoveResult')]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('UserPrincipalName', 'Id')]
        [string[]] $UserId,

        [Parameter(Mandatory)]
        [Alias('RemoveSkuId')]
        [string[]] $SkuId
    )

    begin {
        Test-GkConnection -FunctionName 'Remove-GkUserLicense' | Out-Null
    }

    process {
        foreach ($uid in $UserId) {
            if ([string]::IsNullOrWhiteSpace($uid)) { continue }

            if (-not $PSCmdlet.ShouldProcess($uid, "Remove license(s): $($SkuId -join ', ')")) { continue }

            $enc = [uri]::EscapeDataString($uid)

            $outcome = 'Removed'
            $errMsg = $null
            try {
                Invoke-GkGraphRequest -Method POST -Uri "/users/$enc/assignLicense" `
                    -Body @{ addLicenses = @(); removeLicenses = @($SkuId) } `
                    -CallerFunction 'Remove-GkUserLicense' | Out-Null
            }
            catch {
                $outcome = 'Failed'
                $errMsg = $_.Exception.Message
                Write-Warning "Failed to remove license(s) from '$uid': $errMsg"
            }

            [pscustomobject]@{
                PSTypeName = 'PSGraphKit.LicenseRemoveResult'
                UserId     = $uid
                Action     = 'RemoveLicense'
                SkuId      = ($SkuId -join '; ')
                Outcome    = $outcome
                Error      = $errMsg
            }
        }
    }
}