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.

240
Seats on the invoice
197
consumedUnits in Graph
43
Paid for, assigned to nobody

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.

ONE SKU, FOUR COUNTS, THREE DIFFERENT PROBLEMS Invoice quantity two subscription lines 200 40 prepaidUnits.enabled one subscribedSku row 240 consumedUnits licences assigned 197 43 Active in 30 days usage report 168 29 Rows 1→2: procurement. Rows 2→3: someone stopped assigning. Rows 3→4: someone stopped working here.

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

1
Take the seat count off the invoice, not off memory.

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.

2
Pull the tenant's side and expect a many-to-one.

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.

3
Check the other three buckets before you celebrate a match.

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.

4
Subtract, then discard the false positives.

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.

5
Do the second subtraction — the one that finds more money.

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
Why monthly, and not at renewal

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.

Sources
  • subscribedSku resource type — consumedUnits as "the number of licenses that have been assigned", the subscriptionIds collection behind a single SKU row, and that only SKUs with appliesTo of User are assignable.
  • licenseUnitsDetail resource type — the four prepaid counters and what each means: enabled, warning (grace period after expiry), suspended (cancelled, still reactivatable) and lockedOut.
  • 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, isTrial and nextLifecycleDateTime per subscription, and the absence of any price or invoice property.
GraphPaaS

Prepaid, assigned and actually used — per SKU, per tenant, on the same page as the renewal date.

See your seat gap →

Newsletter

Get the next article by email

Practical M365 & Azure cost pieces like this one. No spam, unsubscribe anytime.