Skip to content
Development
Skill

/licensing

The M365 subscription → SKU → service-plan model, seat availability versus consumption, assigning and removing licenses through Graph, the audit workflow for finding unused or misallocated seats, common SKU GUIDs, and licensing error causes.

From plugin
msp-claude-plugins
46200 skills146 agents200 commands4 MCP
Install
$ npx -y skills add wyre-technology/msp-claude-plugins --skill licensing --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/licensing

Context preview

The summary Claude sees to decide when to auto-load this skill.

The M365 subscription → SKU → service-plan model, seat availability versus consumption, assigning and removing licenses through Graph, the audit workflow for finding unused or misallocated seats, common SKU GUIDs, and licensing error causes.

SKILL.md

licensing.SKILL.md
name: "Microsoft 365 Licensing"
description: >
  The M365 subscription → SKU → service-plan model, seat availability versus
  consumption, assigning and removing licenses through Graph, the audit workflow
  for finding unused or misallocated seats, common SKU GUIDs, and licensing error
  causes.
when_to_use: >-
  When checking, assigning, removing, or auditing Microsoft 365 licenses for a customer
  tenant. Use when: m365 license, microsoft 365 license,
  m365 seats, m365 sku, license audit, license usage m365, unused license m365, license assignment
  m365, m365 subscription, or license optimization.

Microsoft 365 Licensing

Overview

M365 licensing is a top billing concern for MSPs. Licenses are purchased as SKU subscriptions, each containing bundles of service plans (Exchange, Teams, SharePoint, etc.). Efficient license management — finding unused seats, rightsizing SKUs, ensuring all users have what they need — directly impacts both the MSP's margin and the customer's costs.

Anti-triggers

Assigning a seat and buying a seat are different systems, and the word "license" covers both:

  • **Buying, cancelling, or repricing subscriptions** — changing what

the tenant *owns* happens at the distributor, not in Graph. `subscribedSkus` only reports what was already purchased. Use `pax8` (`pax8-subscriptions`), `sherweb`, or `cipp` for CSP licences.

  • **License cost, margin, or invoice reconciliation** — commercial data

lives with the distributor and in the PSA contract, not in Entra; use `pax8` (`pax8-invoices`) or the `finance-pack`.

  • **A licence audit across every customer tenant** — use the `cipp`

plugin (`cipp-licenses`).

  • **Reading seat utilisation without changing assignments** — the

vetted query catalogue answers this without composing a `$filter`; use the `microsoft-graph` plugin's `microsoft-graph-querying` skill.

Core Concepts

Subscription → SKU → Service Plans

M365 Business Premium (subscription)
  └── GUID: cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46
       ├── Exchange Online (service plan)
       ├── Microsoft Teams (service plan)
       ├── SharePoint Online (service plan)
       ├── Intune (service plan)
       └── Entra ID P1 (service plan)

License States

| State | Meaning | |-------|---------| | `Enabled` | Service plan is active and usable | | `Disabled` | Plan is turned off for this user (license still assigned) | | `Error` | Assignment failed — usually missing `usageLocation` | | `LockedOut` | Tenant billing issue | | `PendingInput` | Waiting for additional configuration |

Graph API Patterns

Get All SKUs Available in Tenant

GET /v1.0/subscribedSkus?$select=skuPartNumber,skuId,consumedUnits,prepaidUnits,servicePlans

**Response:**

{
  "value": [
    {
      "skuPartNumber": "SPE_E3",
      "skuId": "05e9a617-0261-4cee-bb44-138d3ef5d965",
      "consumedUnits": 42,
      "prepaidUnits": {
        "enabled": 50,
        "suspended": 0,
        "warning": 0
      },
      "servicePlans": [...]
    }
  ]
}

**Available seats = `prepaidUnits.enabled` - `consumedUnits`**

Get All Users With Their Assigned Licenses

GET /v1.0/users?$select=id,displayName,userPrincipalName,accountEnabled,assignedLicenses,usageLocation&$top=999

Find Users With a Specific License

Filter by SKU GUID:

GET /v1.0/users?$filter=assignedLicenses/any(x:x/skuId eq cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46)&$select=id,displayName,userPrincipalName,accountEnabled

Find Unlicensed Users

GET /v1.0/users?$filter=assignedLicenses/$count eq 0&$count=true&$select=id,displayName,userPrincipalName,accountEnabled

> Requires `ConsistencyLevel: eventual` header and `$count=true`

Assign a License to a User

POST /v1.0/users/{userId}/assignLicense
Content-Type: application/json

{
  "addLicenses": [
    {
      "skuId": "cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46",
      "disabledPlans": []
    }
  ],
  "removeLicenses": []
}

> `usageLocation` must be set on the user before assigning. Use `PATCH /v1.0/users/{id}` with `"usageLocation": "US"` first.

Remove a License from a User

POST /v1.0/users/{userId}/assignLicense
Content-Type: application/json

{
  "addLicenses": [],
  "removeLicenses": ["cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46"]
}

Disable Specific Service Plans (Partial License)

Assign a license but disable specific plans (e.g., give E3 without Teams):

POST /v1.0/users/{userId}/assignLicense
Content-Type: application/json

{
  "addLicenses": [
    {
      "skuId": "05e9a617-0261-4cee-bb44-138d3ef5d965",
      "disabledPlans": ["57ff2da0-773e-42df-b2af-ffb7a2317929"]
    }
  ],
  "removeLicenses": []
}

License Audit Workflow

Step 1: Inventory Available SKUs

Pull `subscribedSkus` and calculate:

  • Total purchased per SKU
  • Consumed seats
  • Available seats
  • SKUs in `warning` state (near renewal, overallocated)

Step 2: Cross-Reference With Active Users

Find licenses assigned to disabled accounts — these are reclaim candidates:

GET /v1.0/users?$filter=accountEnabled eq false and assignedLicenses/$count ne 0&$count=true&$select=id,displayName,userPrincipalName,assignedLicenses

Step 3: Find Inactive Licensed Users

Users licensed but not signing in (90+ days):

GET /v1.0/users?$filter=accountEnabled eq true&$select=id,displayName,userPrincipalName,assignedLicenses,signInActivity

Filter results where `signInActivity.lastSignInDateTime < (today - 90 days)`.

Step 4: Produce Optimization Report

| Optimization | Estimated Saving | |--------------|----------------| | Remove licenses from disabled accounts | # disabled × monthly seat cost | | Downgrade inactive users to lighter SKU | SKU price delta × count | | Recover unused purchased seats | (purchased - consumed) seats available |

Common SKU GUIDs Reference

| SKU Part Number | GUID | Notes | |-----------------|------|-------| | `SPE_E3` | `05e9a617-

Read more
Ships withmsp-claude-plugins

One command to supercharge Claude Code for MSP workflows. Then restart Claude Code. That's it. Documentation: mcp.wyre.ai

Get the whole plugin

Other skills on msp-claude-plugins.