Most Microsoft 365 admins find out about an expired subscription the same way: someone opens a ticket because Outlook stopped working. That used to come with a cushion. Since February 9, 2026, license-based subscriptions bought directly through a Microsoft Customer Agreement no longer get the Expired grace state, so for a lot of tenants the end date on the bill is now the day users lose access. Renewal is also where the money leaks. A good Microsoft 365 license expiry report fixes both problems. It shows what ends when, what happens on that date given how you bought it, and how many seats you really need before you pay again.
Key takeaways
- An expiry report and a usage report answer different questions. Have both open before any renewal.
- Subscriptions move from Active to Expired to Disabled to Deleted. Once a subscription is deleted, it can’t be reactivated and the data is gone.
- MCA-direct subscriptions lost the Expired grace state on February 9, 2026.
- Grace periods depend on how you bought the license: 30 days for most direct terms, 90 for multi-year EA or Volume Licensing, and only 30 for some Copilot VL subscriptions.
- You can pull expiry dates from Billing > Your products, or with Get-MgDirectorySubscription. (Get-MgSubscribedSku won’t show them.)
- Start acting 90 days out. Cancellation windows close long before the end date does.
What is a Microsoft 365 license expiry report?
A Microsoft 365 license expiry report lists every subscription in your tenant with its end date, lifecycle status, and renewal setting, so you can decide what to renew, reduce, or let lapse before Microsoft decides for you.
The report works at the subscription level, not the user level. Each row is one SKU you’ve bought: Microsoft 365 E3, Business Premium, a Teams Phone add-on, the Copilot seats finance signed off on last spring. Trials and free SKUs belong in it too. Trials especially, because when a trial ends, Microsoft starts billing you automatically.
People confuse this with the license usage report all the time. They’re different reports.
A usage report tells you who holds a license and whether they actually use it. An expiry report tells you when the contract behind those licenses ends and what happens after that. At renewal, neither one is enough on its own. If you know the date but not the usage, you renew 500 seats when 380 would have done. If you know the usage but not the date, you find the waste a week after the cancellation window closed.
The data source trips people up as well. Get-MgSubscribedSku is where most license scripts start, and it gives you consumed and prepaid units but no expiry date. For dates you need Get-MgDirectorySubscription. (Both come up again in the how-to section below.)
A useful expiry report answers three questions:
- What ends, and when?
- What actually happens on that date, given whether you bought through MCA, an Enterprise Agreement, Volume Licensing, or a CSP?
- How many of those seats should you renew at all?
Most free scripts and admin tools stop at the first question. Turbo360 Tenant Manager answers all three in one view, with expiry dates, license consumption, and cost side by side.
What happens when a Microsoft 365 subscription expires?
When a Microsoft 365 subscription expires, it moves through four lifecycle states (Active, Expired, Disabled, and Deleted), and how long it stays in each one depends on how you bought it.
That second half is the part most guides skip. And since February 2026, it’s the part that bites.
The four lifecycle states
Active. This is the normal state. Users work, admins assign licenses, and nothing is wrong.
Expired. The subscription has passed its end date, but nobody notices. Users keep full access to apps, mail, and files. A Global or Billing admin can still reactivate it from the admin center. This is the grace period people mean when they say “we have 30 days.”
Disabled. Now people notice. Users can’t get into Microsoft 365 apps, and installed apps drop into a read-only, reduced-functionality mode with Unlicensed Product banners. Only admins can reach the data, so this is your backup window. Admins can still sign in to the admin center, but they can’t assign licenses from that subscription. Reactivation is still possible.
Deleted. It’s over at this point. Customer data is deleted, and Microsoft Entra ID is removed if no other service uses it. You can’t reactivate the subscription. Buying a new subscription of the same type later won’t bring the old data back either.
The February 2026 change for MCA subscriptions
As of February 9, 2026, license-based subscriptions bought directly from Microsoft under a Microsoft Customer Agreement no longer go through the Expired state.
Think about what that does to your report. For those subscriptions there’s no quiet 30 days where everything keeps working. The end date is the date that matters, full stop.
Microsoft’s answer for MCA customers who need more time is the Extended Service Term (EST). An EST keeps the subscription running month to month after the term ends, until you cancel or convert it to a standard subscription. It’s useful when a renewal decision is stuck in procurement, but you have to choose it before the end date. Your expiry report should flag MCA subscriptions early for exactly that reason.
How the purchase channel changes the timeline
The same SKU can follow a different clock depending on where you bought it. Your report needs a channel column, or the dates in it will mislead you.
| Purchase channel and term | Expired (grace) | Disabled | After that |
| Direct from Microsoft, most offers | 30 days | 90 days | Deleted |
| Direct under MCA, license-based (from Feb 9, 2026) | None | Disabled period applies | Deleted |
| EA, Volume Licensing, or Open Value, monthly or annual term | 30 days | 90 days (called Inactive) | Deleted |
| EA, Volume Licensing, or Open Value, multi-year term | 90 days | 90 days (called Inactive) | Deleted |
| Some Microsoft 365 Copilot subscriptions via VL | 30 days only | Per agreement | Deleted |
| Cloud Solution Provider (CSP) | Per Partner Center rules | Per Partner Center rules | Deleted |
Microsoft adds a caveat for agreement-based purchases: your actual end date may be tied to the agreement end date rather than the individual subscription term, and grace periods can vary by program. If you buy through EA or VL, confirm the numbers with your licensing partner instead of trusting a blog table. (Including this one.)
Volume Licensing has one more quirk worth knowing. A VL subscription only enters Expired once all its licenses are removed. If you renew with fewer seats than before, there’s no Expired state. Instead, you get 90 days to sort out users who are assigned more licenses than you now own, with no interruption for per-user subscriptions. Non-user subscriptions behave differently. Cut the quantity on something like Office 365 Extra File Storage, and the storage drops right away.
When the grace period doesn’t apply at all
There are three ways to skip the Expired state, and only one of them is a policy change.
- Cancelling a monthly subscription inside the cancellation window. It goes straight to Disabled. Users lose access immediately, and admins keep access to back up data.
- Explicitly deleting a subscription. This skips Expired and Disabled, and SharePoint Online and OneDrive data is deleted immediately. If someone on your team “cleans up” the billing page by deleting instead of letting a subscription lapse, there’s no undo.
- Being an MCA-direct customer after February 9, 2026. Covered above.
Even after a normal cancellation, the clock keeps running. Microsoft says data you leave behind may be deleted after 90 days, and will be deleted no later than 180 days after cancellation.
So the question your expiry report really has to answer isn’t only “when does this end?” It’s also “which of these rules applies to this row?”
How do you check Microsoft 365 license expiry dates?
You can check Microsoft 365 license expiry dates in the admin center under Billing > Your products, with the Get-MgDirectorySubscription PowerShell cmdlet, or through the Microsoft Graph /directory/subscriptions endpoint.
The admin center is fine for a quick look. Anything you want to run every month belongs in PowerShell or Graph.
In the Microsoft 365 admin center
Sign in with at least the Billing admin role. (Global admin works, but you don’t need it for this.) Go to Billing > Your products. Each subscription shows its status and expiry date, and you can open it to check whether recurring billing is on.
It’s quick, but it’s a screen, not a report. You can’t sort a 40-subscription tenant by end date, you can’t schedule it, and it tells you nothing about how many of those seats people actually use.
With PowerShell (Get-MgDirectorySubscription)
The Microsoft Graph PowerShell SDK gives you the same data in a form you can sort, filter, and export. Read-only access is all it needs.
Connect-MgGraph -Scopes "Organization.Read.All"
Get-MgDirectorySubscription |
Select-Object SkuPartNumber, Status, IsTrial, TotalLicenses,
CreatedDateTime, NextLifecycleDateTime |
Sort-Object NextLifecycleDateTime
The field that matters is NextLifecycleDateTime, but read it alongside Status. It’s the date the subscription moves to its next state, which isn’t always the date you’d call “expiry.” For an active subscription, it’s the term end. For one that’s already Expired, it’s the date it turns Disabled. A report that shows the date without the status will mislead whoever reads it.
Remember the trap from earlier: Get-MgSubscribedSku has no expiry dates. It does have something the subscription object lacks, though, which is how many units are assigned. Join the two and the report starts doing real work:
$assigned = @{}
Get-MgSubscribedSku | ForEach-Object { $assigned[$_.SkuId] = $_.ConsumedUnits }
Get-MgDirectorySubscription | ForEach-Object {
[pscustomobject]@{
Sku = $_.SkuPartNumber
Status = $_.Status
Trial = $_.IsTrial
Purchased = $_.TotalLicenses
Assigned = $assigned[$_.SkuId]
NextLifecycle = $_.NextLifecycleDateTime
}
} | Sort-Object NextLifecycle |
Export-Csv .\m365-expiry-report.csv -NoTypeInformation
One caveat. ConsumedUnits is counted per SKU, not per subscription. If you hold two subscriptions of the same SKU (say, an annual E3 plus a monthly top-up), both rows will show the same assigned count. Flag those rows and look at them by hand.
With the Microsoft Graph API
For automation, call Graph directly:
GET https://graph.microsoft.com/v1.0/directory/subscriptions
It returns the same companySubscription objects, as JSON: SKU, status, trial flag, total licenses, and nextLifecycleDateTime. The least-privileged application permission is Organization.Read.All. For delegated access, a reader role like Global Reader or Directory Readers is enough, so nobody needs Global admin to run the report.
This is the route if you want the report to land in someone’s inbox, feed a dashboard, or raise an alert at 90 days. Run it from an Azure Automation runbook with a managed identity and you don’t have to store any credentials. (That setup is covered in the automation section.)
Which method should you use?
| Method | Least privilege | What you get | Schedulable | Best for |
| Admin center | Billing admin | Status, end date, recurring billing, one subscription at a time | No | Spot checks in small tenants |
| PowerShell | Organization.Read.All | Status, NextLifecycleDateTime, trial flag, purchased and assigned units, CSV export | With a scheduler or runbook | Monthly reports for a single tenant |
| Graph API | Organization.Read.All (application) | The same fields as JSON, ready for other systems | Yes | Alerts, dashboards, multi-tenant setups |
Whichever method you pick, the hard part isn’t pulling the dates. It’s deciding which columns the report needs so the person reading it can act on it.
What should a license expiry report include?
A Microsoft 365 license expiry report should show each subscription’s end date and lifecycle status next to its purchase channel, seat counts, actual usage, cost, and owner, so that every row ends in a renewal decision.
Most reports stop at the date. That tells you when the problem arrives, not what to do about it.
Here’s the column set that holds up in a real renewal meeting. Some of it comes straight from Graph. The rest you’ll have to add yourself, and that’s normal: the parts Graph doesn’t give you are usually the parts finance asks about.
| Column | Where it comes from | Why it matters |
| SKU / subscription name | Graph (skuPartNumber) or admin center | The friendly name, so finance can read it. Map part numbers like SPE_E3 to “Microsoft 365 E3.” |
| Purchase channel | Admin center, invoice, or your partner | Decides which lifecycle rules apply: MCA direct, EA/VL, or CSP. |
| Term and billing frequency | Admin center or invoice | Monthly, annual, or multi-year changes both the grace period and how much room you have to reduce. |
| Status | Graph (status) | Tells you how to read the date column. |
| Next lifecycle date | Graph (nextLifecycleDateTime) | The date something happens. |
| Days remaining | Calculated | Lets you sort the report and band rows at 90, 60, and 30 days. |
| Recurring billing | Admin center | If it’s on, the subscription renews by itself. Sometimes that’s exactly what you don’t want. |
| Cancellation window | Purchase terms | Whether you can still cancel or reduce. Once it closes, you’re committed until the term ends. |
| Trial | Graph (isTrial) | Trials turn into paid subscriptions automatically. |
| Purchased units | Graph (totalLicenses) | What you’re paying for. |
| Assigned units | Graph (consumedUnits, via Get-MgSubscribedSku) | What you’ve handed out. |
| Active users | Microsoft 365 usage reports | What people actually use. This is the column that saves money. |
| Annual cost | Invoice or price sheet | Turns seat gaps into a number someone will act on. |
| Owner | You | Who signs off on the renewal. Rows without an owner tend to auto-renew. |
| Decision | You | Renew, reduce, switch SKU, move to EST, or let it lapse. |
A few of these deserve a word.
Purchased, assigned, and active are three different numbers. Purchased minus assigned is shelfware: seats nobody holds. Assigned minus active is quieter waste: licenses sitting on people who left, moved roles, or never opened the app. Most tenants have both kinds. The second kind is harder to see because the license looks “in use.”
The cancellation window column is the one people forget. For some subscriptions, you can only cancel during a short window after you buy or renew. After that, turning off recurring billing just stops the next renewal. Your report needs to show when that window closes, not only when the term ends. Otherwise the waste you find in March is waste you’re paying for until next year.
Owner and Decision turn this into a report rather than a list. A row reading “E3, 500 purchased, 412 assigned, 371 active, ends in 74 days, owner: IT Ops, decision: reduce to 400” is something a person can act on. The same row without the last two fields is something a person can only look at.
Should usage and cost live in the same file as the dates? Ideally, yes. Pulling usage from one script, dates from another, and prices from an invoice PDF is how these reports go stale, which brings up timing.
How far ahead should you act on an expiring license?
Start acting on an expiring Microsoft 365 license at least 90 days before its end date, with checkpoints at 90, 60, and 30 days, because cancellation windows and seat reductions have to be settled long before the subscription actually ends.
Thirty days is when most teams start. It’s also when most of the options are already gone.
Here’s why. By 30 days out, procurement has a quote in motion, recurring billing is still on, and nobody has checked usage. The renewal goes through at last year’s seat count because there’s no time to argue for a smaller one. Microsoft does send warning emails and admin center alerts as the end date gets closer, but they go to billing admins, who often aren’t the people who know whether 120 Visio seats are still needed.
So build the cadence backward from the end date.
90 days out: find the waste. Pull the report and compare purchased, assigned, and active seats for everything ending this quarter. Reclaim licenses from leavers and long-inactive accounts now, so the assigned count drops before anyone quotes a renewal. Look for trials too. Any trial that ends in this window will start billing unless someone turns it off.
60 days out: make the decision. Each row gets an owner and a call: renew as-is, reduce, change SKU, move to EST, or let it lapse. This is where finance comes in. The admin knows the usage and finance knows the budget, and the decision needs both of them in the same meeting (or at least the same spreadsheet).
30 days out: execute and check. Change seat counts, set recurring billing to match the decision, and confirm with your partner if you buy through one. Anything still unresolved at this point gets escalated. Don’t let it default.
Day 0 and after: confirm the status changed the way you expected. If a subscription was meant to lapse, check that it’s in the right state and that data from it has been backed up. Don’t delete it to tidy up the billing page. (We covered why.)
Who owns which checkpoint?
It’s split between roles, and that’s where expiries fall through.
The M365 admin owns the data: seat counts, usage, reclaiming licenses, and the technical risk of anything lapsing. Finance or the billing owner owns the money: budget, invoice, and whether a reduction actually shows up on the next bill. If you buy through an EA or a CSP, your licensing partner owns part of the timeline as well, and they’ll need notice before they can change anything.
Name a single owner for each subscription, even if three people are involved. “IT” isn’t an owner.
Adjust the cadence by channel
Ninety days is the default, not a rule.
EA and Volume Licensing renewals usually need more runway than that. They involve negotiation and a partner, and often a true-up, so start the 90-day review a quarter earlier for the big agreement. CSP subscriptions run on your partner’s terms, and their cancellation windows can be short, so ask the partner for exact dates rather than assuming.
MCA-direct subscriptions are the ones to watch closest now. There’s no Expired grace state behind them anymore. If your decision isn’t made by the end date, the main way to buy time is the Extended Service Term, and you need to choose it before the term ends, not after.
Get the timing right and the 90-day review stops being a clean-up chore. It becomes the point where you actually cut the bill.
How do you automate Microsoft 365 license expiry reporting?
To automate Microsoft 365 license expiry reporting, run a Microsoft Graph PowerShell script as an Azure Automation runbook with a managed identity, give it read-only Organization.Read.All access, and schedule it to flag anything ending within 90 days.
No stored passwords, no app secrets to rotate, no laptop that has to stay on.
If your FinOps team already works in Azure, this is the natural home for it. The runbook lives in a subscription you already govern, alongside the rest of your cost tooling. And M365 licensing gets the same treatment as your Azure spend instead of sitting in someone’s Downloads folder.
Step 1: Create the automation account and identity
Create an Azure Automation account with a system-assigned managed identity turned on. Then import two modules as resources on the account: Microsoft.Graph.Authentication and Microsoft.Graph.Identity.DirectoryManagement. The second one holds the subscription and SKU cmdlets. Skip it and the runbook fails on its first line of real work.
Step 2: Grant the Graph permission
This step catches people out. The Entra admin center will show the managed identity’s permissions, but it won’t let you assign Graph application permissions to it. You do that once, from your own machine, with PowerShell:
Connect-MgGraph -Scopes "AppRoleAssignment.ReadWrite.All","Application.Read.All"
$graph = Get-MgServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'"
$mi = Get-MgServicePrincipal -Filter "displayName eq 'your-automation-account'"
$role = $graph.AppRoles | Where-Object {
$_.Value -eq "Organization.Read.All" -and
$_.AllowedMemberTypes -contains "Application" }
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $mi.Id `
-PrincipalId $mi.Id -ResourceId $graph.Id -AppRoleId $role.Id
Organization.Read.All is enough. Don’t grant Directory.Read.All “just in case.”
Step 3: Write the runbook
Create a PowerShell 7 runbook. It’s the same logic as the export script earlier, plus a days-remaining calculation and a filter for the rows someone actually needs to see:
Connect-MgGraph -Identity -NoWelcome
$assigned = @{}
Get-MgSubscribedSku | ForEach-Object { $assigned[$_.SkuId] = $_.ConsumedUnits }
$today = Get-Date
$report = Get-MgDirectorySubscription | ForEach-Object {
$daysLeft = if ($_.NextLifecycleDateTime) {
[int]($_.NextLifecycleDateTime - $today).TotalDays } else { $null }
[pscustomobject]@{
Sku = $_.SkuPartNumber
Status = $_.Status
Trial = $_.IsTrial
Purchased = $_.TotalLicenses
Assigned = $assigned[$_.SkuId]
NextLifecycle = $_.NextLifecycleDateTime
DaysLeft = $daysLeft
}
}
$flagged = $report | Where-Object {
($_.DaysLeft -ne $null -and $_.DaysLeft -le 90) -or
$_.Trial -or $_.Status -ne "Enabled"
} | Sort-Object DaysLeft
$flagged | Format-Table -AutoSize
The filter catches three things: anything inside the 90-day window, every trial (because trials convert to paid), and anything whose status isn’t Enabled. That last one means a subscription is already past its end date and nobody noticed.
Step 4: Schedule it and send it somewhere
Link a schedule to the runbook. Monthly is enough for the full report. Weekly is better once anything enters the 90-day band.
Then send the output to wherever the owners will actually see it. Writing the CSV to a storage account works. Posting to a Teams channel works better, because people read Teams. If you’d rather email it, the identity needs Mail.Send. Scope that to a single sending mailbox rather than leaving it tenant-wide.
Where the script stops
Be honest about what this covers. The runbook gives you dates, status, trials, and purchased versus assigned seats, and that alone is more than most tenants track today.
It doesn’t know your purchase channel, cancellation windows, or what each seat costs. It doesn’t know who’s actually using their license, as opposed to just holding one. And if you manage more than one tenant, you’re now maintaining a runbook, an identity, and a permission grant for each of them.
That gap, between “which licenses are expiring” and “which licenses are worth renewing,” is what Turbo360 Tenant Manager is built for. It analyzes every SKU in the tenant (paid, free, and trial) for consumption and right-sizing, so the renewal conversation starts from usage data instead of last year’s seat count.