M365 Licensing Groups
How PSI assigns Microsoft licences by group membership instead of per user, what the
LIC -naming convention means, and the Microsoft constraint that forces licence groups to be bundle-shaped rather than one-per-SKU.
The convention: LIC - <bundle>
A group whose name starts with LIC - spends money. Adding a member consumes a paid seat; removing
one frees it. That is the entire reason the prefix exists — licence groups sort together, and a reader
can tell at a glance that LIC - M365 BP + Teams Voice costs something while BC - PS Supervisor
merely grants permissions inside Business Central.
Established 2026-08-10. Before that, PSI had one licence-bearing group out of 378 (Co-Pilot users,
no prefix) and assigned everything else per user.
| Group | SKUs | Who it is for |
|---|---|---|
LIC - M365 Business Premium | SPB | anyone needing only the base licence |
LIC - M365 BP + Teams Voice | SPB + Teams Phone w/ PAYG Calling (US zone 1) + Teams Audio Conferencing select dial-out | anyone who needs a phone number — e.g. Process Services supervisors |
Put a person in exactly one LIC - group — the bundle matching their role. Membership in two
groups that both carry SPB still consumes one seat (Microsoft de-duplicates by SKU), but it makes
“which group owns this seat” unanswerable, and removal from one silently leaves the licence in place.
Do not confuse these with:
BC - <Role>(21 groups) — Business Central permission groups. No licences attached.DG - <dept> - <name>— distribution groups.- On-prem groups (
Everyone Group,B1_PlantDoors_24_7, …) — AD security/door groups written by the post-task worker.
⚠️ The constraint: dependencies are validated on the GROUP, not the member
An add-on SKU cannot live in a licence group by itself. Group-based licensing checks service-plan dependencies against the group’s own licence set, so a group containing only an add-on is rejected outright — even if every member already holds the base licence directly.
Proved three ways on 2026-08-10 while building LIC - M365 BP + Teams Voice:
| Attempt | Result |
|---|---|
| Teams Phone PAYG alone | ❌ License assignment failed because service plan 4828c8ec… depends on 57ff2da0…, 0feaeb32… |
| Teams Phone PAYG + Communications Credits | ❌ same error |
| SPB + Teams Phone PAYG (+ dial-out) | ✅ accepted |
The PAYG SKU carries the Communications Credits service plan (4828c8ec…), which depends on
Teams (57ff2da0…) and Skype for Business Online (0feaeb32…) — both of which arrive with
Business Premium. So the base SKU has to be in the same group as the add-on.
Consequence: licence groups are bundles, one per role-shaped licence set, not one per SKU. Name them for what they contain, so nobody is surprised that the “voice” group also grants Business Premium.
Doing it with Graph
Group licence assignment is beta-only — POST /v1.0/groups/{id}/assignLicense returns
The request URI is not valid. Use /beta:
$tok = az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv
$h = @{ Authorization = "Bearer $tok"; 'Content-Type' = 'application/json' }
# Literal JSON, not ConvertTo-Json: PowerShell collapses a single-element array into an object, and
# Graph then answers "No update in the group licenses" while changing nothing.
$json = '{"addLicenses":[{"disabledPlans":[],"skuId":"<sku-guid>"}],"removeLicenses":[]}'
Invoke-RestMethod -Method POST -Headers $h -Body $json `
-Uri "https://graph.microsoft.com/beta/groups/<group-id>/assignLicense"Two traps worth remembering:
assignedLicensesis omitted from a default group GET. Ask for it explicitly (?$select=assignedLicenses,licenseProcessingState) or a successful assignment looks like it failed.az restmangles Graph URLs on Windows — cmd eats$and splits on&. Use a token plusInvoke-RestMethod, or percent-encode (%24select).
Check licenseProcessingState — QueuedForProcessing becomes ProcessingComplete within seconds.
Migrating from direct to group assignment
Safe and reversible, in this order:
- Add the user to the
LIC -group. - Confirm the licence shows as
assignedByGroupon the user (GET /users/{id}?$select=licenseAssignmentStates). - Then remove the direct assignment.
Overlap costs nothing — a SKU held both directly and by group consumes one seat — so there is never a reason to remove the direct assignment first and leave someone unlicensed in between.
As of 2026-08-10 all 274 SPB assignments are still direct, including the three Process Services
supervisors.
Seat pools are tight — check before adding
| SKU | Position (2026-08-10) |
|---|---|
SPB (Business Premium) | 275 / 275 — full once Kurt Leatherman’s seat lands |
| Teams Audio Conferencing select dial-out | 248 / 248 — full |
| Teams Phone w/ PAYG Calling (US z1) | 79 / 84 — 5 free |
DYN365_BUSCENTRAL_PREMIUM | 24 / 42 — 18 free |
Adding someone to a bundle group needs a free seat in every SKU it carries. Group-based licensing does not refuse the membership — it records an error state on the group, so the person silently ends up without part of their licence.
A seat is worth hunting before it is worth buying. On 2026-08-10 exactly one disabled account still held a Business Premium seat; reclaiming it unblocked a hire the same afternoon. Disabled accounts holding paid licences are the first place to look.
⚠️ “Never signed in” is NOT evidence of a wasted seat
An audit on 2026-08-10 found 59 Business Premium seats on accounts that have never had a sign-in of any
kind (Entra signInActivity, interactive and non-interactive), 55 of them in department 125. The
obvious-looking conclusion — reclaim them, or drop those people to F1 — is wrong, and it is worth
knowing why before anyone reruns the query and reaches it again.
The shop floor works through a shared account. Under Microsoft’s multiplexing rules the licence follows the person who accesses the service, not the account they type in, so each of those individuals still needs their own Business Premium for access licensing even though their personal account is never used. F1 does not carry the entitlements they need. The seats are legitimately consumed.
What that leaves:
- Never-signed-in + enabled + on the current staffing board → correctly licensed. Not a finding.
- Never-signed-in + no longer on the board → worth checking as a leaver. On 2026-08-10 the department manager’s board named one explicitly (“replaced by Crystal Orduna”) whose account was still enabled and licensed. That is the reclaim pattern, and reading the board is what found it — not the sign-in data.
- Disabled + licensed → always a reclaim.
So the usable signal is directory state reconciled against the current staffing board, not usage. Sign-in activity is still worth surfacing (it answers “has this account ever been used?”), but it must be labelled as usage, never as cost.
Why the 125 F1 rules are not simply waiting for a signature
The licensing matrix carries M365 F1 (PROPOSED — badge-only sign-in, no personal mailbox) rules for
department 125, unsigned. It is tempting to read the never-signed-in data above and conclude F1 is
obviously correct for those people. It is not that simple: the shared-account access-licensing rule means
each shop-floor person needs a Business Premium entitlement regardless of whether they personally sign in.
The F1 proposal has to be resolved against what the shared account gives them access to, not against
whether their own account gets used. Treat those rules as an open licensing question, not a pending
rubber-stamp.
Known gap: the provisioning seat guard only checks the base SKU
IT Helper’s provisioning gate maps the matrix licence string (e.g.
“M365 Business Premium”) to one SKU and checks that pool before uploading a hire. It knows nothing about
the other SKUs in a bundle. So a supervisor hire can pass the guard on SPB and still fail to receive a
phone licence, silently. Closing that means teaching the guard to check every SKU in the bundle the
matrix assigns — a code change, tracked on psi-notify-bot#42.
See also
- IT Helper — the provisioning chain that assigns these groups
- PSI Portal → User Provisioning Module — where the policy lives