Invoice seats vs assigned seats
09 Sept 2026
Reconciling invoice seats against assigned seats
You bought 240 seats. 197 are assigned. Nobody noticed for eight months, because the two numbers live in two different places and nothing in Microsoft 365 puts them side by side. The invoice is in the admin center's billing section. The assignment count is in Graph. Neither one tells you the other exists.
This is a reconciliation, not an audit — the point isn't to prove waste, it's to make the gap a number that somebody owns before the renewal closes the door on doing anything about it. Here is the procedure, monthly, about forty minutes the first time and ten after that.
First, know which four numbers you're comparing
Most reconciliations fail because someone compares the wrong pair. There are four counts for the same SKU, they are all correct, and each gap between them means something different — and needs a different person to fix it.
Row one against row two is a procurement question — did the seats you paid for actually land in the tenant. Row two against row three is an assignment question. Row three against row four is a usage question, and it's the only one of the three where the fix involves talking to a human. Keep them separate or the meeting turns into an argument about whose fault it is.
The reconciliation, five steps
Seat-based products on a Microsoft invoice show a unit price and a quantity, itemised per product order from page two. Watch the billing period: for purchases and renewals it's a single day, and mid-term changes are billed the next day on a separate monthly invoice. In a renewal month you legitimately get two invoices for the same subscription covering different service periods. Take the quantity from the renewal invoice, not from whichever PDF arrived last.
Done when: every seat-based line has a SKU name, a quantity and a service period.
One GET /subscribedSkus gives you
skuPartNumber, prepaidUnits and
consumedUnits. The trap is that a
subscribedSku
carries a subscriptionIds collection — plural. Buy 200 seats
in January and 40 more in June and you have two subscriptions, two invoice
lines and one SKU row showing 240. Match on the sum, not line by
line, or you will "find" a discrepancy that is just addition.
Done when: invoice quantities summed per SKU equal prepaidUnits.enabled, or you have a written reason why not.
prepaidUnits has four counters, and
dashboards routinely read only the first. enabled is the active
subscription; warning is the grace period after expiry;
suspended means cancelled but still reactivatable; and
lockedOut means the customer cancelled outright. A SKU sitting at
warning: 45 is 45 seats that still appear to work and are on
a clock nobody set a reminder for.
Done when: every non-zero warning, suspended or lockedOut count has a renewal decision attached to it.
enabled − consumedUnits is the
unassigned gap. Two rows will look alarming and aren't: SKUs where
appliesTo
is Company rather than User are not assignable
at all, so a zero there is normal; and add-on SKUs bought as a pool are
supposed to run below capacity. Everything else is a real number.
Done when: each remaining gap is labelled deliberate buffer, hiring pipeline, or nobody knows.
Unassigned seats are the visible half. The other
half is assigned seats nobody touches.
getOffice365ActiveUserDetail
returns one row per user with Assigned Products, a licence
assign date and a last-activity date per workload, over D7, D30, D90 or D180.
Assigned six months ago, last activity blank, on your most expensive SKU — that
is the row that pays for the exercise.
Done when: the dormant list is with the manager who owns those people, with a date.
Reading the gap you found
| What you see | Usual cause | Where it gets fixed |
|---|---|---|
| Invoice > enabled | Seats bought on a second subscription, or one that has already expired into warning |
Procurement |
| Enabled > consumed, gap growing | Leavers offboarded, seats never returned at the renewal | Next renewal window |
| Enabled > consumed, gap constant | A deliberate hiring buffer nobody wrote down | Write it down |
| Consumed jumps overnight | A group-based assignment rule caught a bigger group than intended | Group membership |
| Consumed = enabled, exactly | You're full — the next hire fails to get a licence | Buy before onboarding day |
Because under new commerce you can add seats whenever you like and remove them only inside a seven-day window. A reconciliation run in the renewal week produces a number and no time to act on it. Run monthly, and the number is already agreed when the window opens — see annual vs monthly commitment for how often that door actually opens for you.
Making it survive month two
The reason this reconciliation gets done once and abandoned is that step 1
lives in a billing portal, steps 2–5 live in Graph, and gluing them together by
hand is nobody's favourite Tuesday. Half of it automates cleanly. The tenant side
is three calls, and the
companySubscription
resource adds totalLicenses, status,
isTrial and nextLifecycleDateTime per subscription — the
date the whole exercise is timed against. The invoice side has no tenant API and
stays a monthly download.
So build the tenant half as a standing report and paste one column into it. GraphPaaS keeps prepaid, consumed and the per-user activity dates in the same view per tenant, with subscription lifecycle dates alongside — which turns the forty-minute version into reading a page and deciding what to do about a row that is already highlighted.
- subscribedSku resource type —
consumedUnitsas "the number of licenses that have been assigned", thesubscriptionIdscollection behind a single SKU row, and that only SKUs withappliesToofUserare assignable. - licenseUnitsDetail resource type — the four prepaid counters and what each means:
enabled,warning(grace period after expiry),suspended(cancelled, still reactivatable) andlockedOut. - Understand your invoice for your Microsoft MCA billing account — that seat-based products show a unit price and quantity, the single-day billing period on purchases and renewals, and why a renewal month produces two invoices for the same subscription.
- reportRoot: getOffice365ActiveUserDetail — the CSV columns including
Assigned Products, per-workload licence assign dates and last activity dates, over D7/D30/D90/D180. - companySubscription resource type —
totalLicenses,status,isTrialandnextLifecycleDateTimeper subscription, and the absence of any price or invoice property.
Prepaid, assigned and actually used — per SKU, per tenant, on the same page as the renewal date.
See your seat gap →