Skip to content

Licensing

Seven examples, in the order the questions are asked: what the tenant owns, what one person holds, whether a call may rely on it, who has what, how to change it, and what the change did to the plans. Two more carry that state as a list: export_licences.py writes it, and import_licences.py reads it back into assignLicense calls, sent as one batch.


Prerequisites

Permission Description Reference
Organization.Read.All Read subscribed SKUs and their service plans Organization permissions
User.Read.All Read users and their license assignments User permissions
User.ReadWrite.All Assign and remove licenses User permissions
Group.ReadWrite.All Assign a license to a group Group permissions
Example Writes Question
inventory.py — What does the tenant own — seats, state, how many are left? --plans lists the plans inside each SKU, --csv writes one row per SKU
matrix.py — Who holds which SKU, and which service plans inside it are enabled or disabledPlans. --out licences.csv, --sku SPE_E5
whose.py — Who holds this SKU (--sku), or what does this person hold (--user) with each plan's provisioning status?
check.py — Is the seat — or the capability inside it — actually held? --plan RECORDS_MANAGEMENT
assign.py ✓ Add or --remove one licence, refusing the calls Graph would refuse: no usageLocation, no seat left, already assigned. --dry-run stops before the write
group_license.py ✓ Licence a group once and let Graph keep its members licensed
plans.py ✓ List the service plans of a SKU, or --plan-off EXCHANGE_S_ENTERPRISE for one user
export_licences.py — Every user's SKUs and disabled plans as CSV — the input the next script reads
import_licences.py ✓ Give each user the licences a row asks for — one assignLicense per row, in one $batch. --dry-run prints the rows

Every script is runnable and documents itself:

python inventory.py --help
python assign.py --user ada@contoso.com --sku SPE_E5 --dry-run

Two questions, not one

subscribedSkus says what the tenant buys; licenseDetails says what one seat grants. A SKU part number is packaging, a service plan is capability — and a seat may carry a plan no retail SKU name suggests:

from office365.graph_client import GraphClient

client = GraphClient(tenant="contoso.onmicrosoft.com").with_client_secret("client_id", "client_secret")

held = client.licences.get_all().execute_query().value
print(sorted(held.names))  # SKU part numbers and provisioned plan names
print(held.holds("RECORDS_MANAGEMENT"))  # True for DEVELOPERPACK_E5
print([sku.sku_part_number for sku in held.carrying("RECORDS_MANAGEMENT")])
print(held.to_rows())  # one row per SKU: seats bought, handed out, left

mine = client.licences.get_user(client.me).execute_query().value  # a user's licenseDetails

client.licences is a manager and sends nothing; get_all() / get_user(user) return a ClientResult that execute_query() submits. Only a plan whose provisioningStatus is Success counts, and nothing is cached: a grant changes the answer, and the library is not the level that knows when.


A list, both ways

export_licences.py > licences.csv writes the state; import_licences.py licences.csv gives each user what a row asks for, one assignLicense per row in one $batch. Nothing is read first — the row addresses the user by UPN or id — and Graph's own preconditions are what refuses a row, each refusal returning as its own sub-request's error. A licence a group grants cannot be removed: the group owns it. For a population, group-based licensing is the way round; a per-user fan-out is for a one-off list.

Official docs