Managing Copilot Spending Policies with Microsoft Graph and PowerShell (Preview)

Microsoft has introduced new Microsoft Graph APIs that give organizations a programmatic way to manage how Copilot credits are spent and tracked. These APIs are currently in public preview and should let teams responsible for Copilot in their organization view credit balances and set up and adjust spending policies in a more automated way.

This update has no impact on users, and there is no change to how they work with Copilot day to day. An organization can now connect Copilot cost management with existing financial and administrative processes.

If a user requests additional Copilot credits, their organization may be able to review and respond to that request through these new tools.
For financial reporting, the current preview offers only limited options: you can read the tenant credit balance and the balances of specific users, but there is no way to get a full consumption report for all users. You must already know the user (see the chapter “Reading credit balances and consumption”).

The APIs are only relevant to organizations that use Copilot credits with their Microsoft 365 Copilot setup. This first release focuses on spending policies and credit balance information and does not include detailed usage reporting.

If you are testing these APIs, you will notice many billing services are missing. The current release supports the following billing services. Check those services again later.

  • Cowork
  • Code
  • Copilot in OneDrive
  • Copilot in SharePoint
image.png
These services are currently supported

Those are all the Microsoft Copilot services.

image.png
The Microsoft Copilot services are currently supported via Graph API

Listing all spending services

To find supported services, I manually created a new spending policy in the Microsoft 365 admin center, enabled all billing services (then disabled the policy), and listed all policies via the Graph API.

PowerShell
Import-Module Microsoft.Graph.Authentication
Connect-MgGraph -Scopes "CopilotCostManagement-Policy.Read.All"

# Get all spending policies ordered by creation date descending
$Url = "https://graph.microsoft.com/beta/copilot/costManagement/spendingPolicies?`$orderby=createdDateTime desc"
$AllSpendingPolicies = Invoke-MgGraphRequest -Method GET -Uri $Url
$AllSpendingPolicies.value | select  id,displayName,status,createdDateTime

Check the spending policy ID.

image.png
Check the spending policy ID

List the spending services for that policy.

PowerShell
# Get the service settings of a specific spending policy
$Url = "https://graph.microsoft.com/beta/copilot/costManagement/spendingPolicies/57c717d5-10f2-47a3-9730-5c68b8bbc8aa/serviceSettings"
$ServiceSettings = Invoke-MgGraphRequest -Method GET -Uri $Url
$ServiceSettings.value | Select-Object serviceId, state

The system returns all supported services and their internal service IDs. You need them to create a new policy.

image.png
You need the serviceId to create a new spending policy

The system returns all services, but the Graph API supports only the green-marked ones.

image.png
Just the green marked services are currently supported

Creating a new spending policy

Before you create a new spending policy via Graph:

  • The service must be supported via the Graph endpoint (cowork, workIQ, sharePointCopilot, or oneDriveCopilot). Pay close attention to the wording, as the Graph endpoint is case-sensitive.
  • The Graph API has limitations; for example, you cannot yet add a specific Azure subscription as a billing method.
  • You cannot update or delete a spending policy in the Microsoft 365 admin center if the policy was created via Graph APIs. The admin center will return an error. You must update or delete it via Graph again.
  • Creating a policy also requires the Billing Administrator or Global Administrator role.

You need the permission CopilotCostManagement-Policy.ReadWrite.All to prepare a new policy, and CopilotCostManagement-Assignment.ReadWrite.All if you want to assign a group to that policy.

  1. Prepare a new policy for Cowork.
    In my sample, I will restrict the policy to an Entra ID group (see the #microsoft.graph.selectedGroupsSpendingPolicy type). If you want to enable the policy for all users, use the #microsoft.graph.allUsersSpendingPolicy type instead.
PowerShell
Connect-MgGraph -Scopes "CopilotCostManagement-Policy.ReadWrite.All, CopilotCostManagement-Assignment.ReadWrite.All"

$Body = @{
    "@odata.type"  = "#microsoft.graph.selectedGroupsSpendingPolicy"
    displayName    = "Cowork by MSGraph"
    periodLimit    = @{
        "@odata.type" = "#microsoft.graph.limitedCostManagementQuantity"
        value         = 1000
        unit          = "copilotCredit"
    }
    perUserLimit   = @{
        "@odata.type" = "#microsoft.graph.unlimitedCostManagementQuantity"
        unit          = "copilotCredit"
    }
    billingMethods = "payAsYouGo"
    serviceSettings = @(
        # Valid serviceIds (case-sensitive): cowork, workIQ, sharePointCopilot, oneDriveCopilot
        @{ serviceId = "cowork"; state = "enabled" }
    )
} | ConvertTo-Json -Depth 5

$Url = "https://graph.microsoft.com/beta/copilot/costManagement/spendingPolicies"
$NewPolicy = Invoke-MgGraphRequest -Method POST -Uri $Url -Body $Body -ContentType "application/json"
  1. Add the group assignment to limit the usage to members of that group.
    Note: Updates to a policy require current ETag information. The sample uses a small helper function to fetch the latest ETag.
PowerShell
$GroupId  = "<GroupObjectID>"
$PolicyId = $NewPolicy.id
$PolicyUrl = "https://graph.microsoft.com/beta/copilot/costManagement/spendingPolicies/$PolicyId"

# Function to get the current ETag of the policy (it changes after every mutation)
function Get-PolicyEtag { (Invoke-MgGraphRequest -Method GET -Uri $PolicyUrl).'@odata.etag' }

# Assign the group
$Ref = @{ "@odata.id" = "https://graph.microsoft.com/beta/groups/$GroupId" } | ConvertTo-Json
Invoke-MgGraphRequest -Method POST -Uri "$PolicyUrl/microsoft.graph.selectedGroupsSpendingPolicy/assignedGroups/`$ref" -Body $Ref -ContentType "application/json" -Headers @{ "If-Match" = (Get-PolicyEtag) }

The spending policy has been created; alerts are off by default.
Pay attention to the billing method: The Graph API offers no property to select the Azure billing account, so the policy's billing method configuration is not finished. You must update the method manually until Microsoft provides an API option.

image.png
You must update the billing method manually

Adding or removing cost alerts

Alerts are off by default. I first prepare the alert; in the second step, I enable it.
You can add multiple users, but you must run the cmdlets for each user (or a loop), as the endpoint does not support multiple users with one update. You must add the user object ID. A UserPrincipalName is not supported.

PowerShell
# 1. Create the alert configuration disabled, with the 80 % threshold
$Body = @{
    isEnabled  = $false
    thresholds = @( @{ "@odata.type" = "#microsoft.graph.percentageCostManagementAlertThreshold"; value = 80 } )
} | ConvertTo-Json -Depth 5
Invoke-MgGraphRequest -Method PATCH -Uri "$PolicyUrl/emailThresholdSettings" -Body $Body -ContentType "application/json" -Headers @{ "If-Match" = (Get-PolicyEtag) }

# 2. Link the recipient
$UserId    = "<UserObjectID>"
$Ref = @{ "@odata.id" = "https://graph.microsoft.com/beta/users/$UserId" } | ConvertTo-Json
Invoke-MgGraphRequest -Method POST -Uri "$PolicyUrl/emailThresholdSettings/recipients/`$ref" -Body $Ref -ContentType "application/json" -Headers @{ "If-Match" = (Get-PolicyEtag) }

Enable (or disable) the alerts.

PowerShell
# 3. Enable (or disable) alerting
Invoke-MgGraphRequest -Method PATCH -Uri "$PolicyUrl/emailThresholdSettings" -Body (@{ isEnabled = $true } | ConvertTo-Json) -ContentType "application/json" -Headers @{ "If-Match" = (Get-PolicyEtag) }

Configured as expected.

image.png
Configured as expected

Deleting a spending policy

As mentioned before, you cannot update or delete a spending policy in the Microsoft 365 admin center if you created it via Microsoft Graph.

image.png
You cannot update or delete a spending policy if you created it via Microsoft Graph

Just delete it via Microsoft Graph, and it’s gone.

PowerShell
Invoke-MgGraphRequest -Method DELETE -Uri $PolicyUrl -Headers @{ "If-Match" = (Get-PolicyEtag) }

Reading credit balances and consumption

The reporting options are limited and not yet ready for a billing or financial team. There is no endpoint that lists the consumption of all users or of a spending policy. You can read the tenant credit balance and the balance of a specific user, but you must know the user object ID. Without it, you cannot get a full consumption list.

Read the tenant credit balance. You need the permission CopilotCostManagement.Read.All.

PowerShell
Connect-MgGraph -Scopes "CopilotCostManagement.Read.All, CopilotCostManagement-UserData.Read.All"
$Url = "https://graph.microsoft.com/beta/copilot/costManagement/getTenantCreditBalance"
$TenantCreditBalance = Invoke-MgGraphRequest -Method GET -Uri $Url
$TenantCreditBalance | ConvertTo-Json -Depth 5

The result lists the consumed credits per billing method (prepaidCredits and payAsYouGo).

image.png
Consumed credits per billing method

Read the balance of a specific user, including the balance per service. You need the permission CopilotCostManagement-UserData.Read.All. You must add the user object ID. A UserPrincipalName is not supported.

PowerShell
$UserId = "<UserObjectID>"
$Url = "https://graph.microsoft.com/beta/copilot/costManagement/userBalances/$UserId?`$expand=serviceBalances"
$UserBalance = Invoke-MgGraphRequest -Method GET -Uri $Url
$UserBalance | ConvertTo-Json -Depth 5

The result contains one row per service, with the consumed credits (consumedQuantity), the configured limit, the remaining credits, and the spendingPolicyId that applies to this user.

The result was a surprise. Below is a copy of my result; pay attention to the spendingPolicyId.
Some values are redacted for privacy reasons.

JSON
{
  "serviceBalances@odata.context": "https://graph.microsoft.com/beta/$metadata#copilot/costManagement/userBalances('00000000-0000-0000-0000-000000000000')/serviceBalances",
  "id": "00000000-0000-0000-0000-000000000000",
  "serviceBalances": [
    {
      "spendingPolicyId": "5a520702-2e18-4742-b2ae-3b0212c8b4e6",
      "accessDecisionReason": "allowed",
      "serviceId": "cowork",
      "consumedQuantity": 0.0,
      "effectiveCapacityQuantity": {
        "value": 1000.0,
        "unit": "copilotCredit",
        "@odata.type": "#microsoft.graph.limitedCostManagementQuantity"
      },
      "isConsumptionAllowed": true,
      "configuredPeriodLimit": {
        "value": 1000.0,
        "unit": "copilotCredit",
        "@odata.type": "#microsoft.graph.limitedCostManagementQuantity"
      },
      "dataStatus": {
        "lastModifiedDateTime": "2026-11-07T09:48:32.9577895Z"
      },
      "unit": "copilotCredit",
      "configuredPerUserLimit": {
        "@odata.type": "#microsoft.graph.unlimitedCostManagementQuantity",
        "unit": "copilotCredit"
      },
      "remainingQuantity": {
        "value": 1000.0,
        "unit": "copilotCredit",
        "@odata.type": "#microsoft.graph.limitedCostManagementQuantity"
      }
    }
  ],
  "unevaluatedServices": [
    {
      "reason": "noApplicablePolicy",
      "serviceId": "workIQ"
    }
  ],
  "@odata.context": "https://graph.microsoft.com/beta/$metadata#copilot/costManagement/userBalances(serviceBalances())/$entity",
  "selectionDataStatus": {
    "policyConfigurationAsOfDateTime": "2026-11-07T09:47:45.589555Z",
    "evaluatedDateTime": "2026-11-07T09:48:32.9577895Z",
    "groupMembershipAsOfDateTime": "2026-11-07T09:47:45.6463535Z",
    "usageAsOfDateTime": "2026-11-07T09:48:32.9577895Z"
  }
}

The spendingPolicyId is the policy I previously created via Microsoft Graph. It seems the system ignores spending policies created in the Microsoft 365 admin center.

image.png
The system ignores spending policies created in the Microsoft 365 admin center

For my test, I used credits to see whether the API returns the spent credits. The consumption summary confirms the API didn’t return the latest information.

image.png
No, the API hasn’t returned the latest information

It’s an early preview. You may want to test it, but don’t trust it yet, it’s clearly not ready.

Share this post:

About the Author

Tobias Asböck

Tobias Asböck

Tobias is a Senior System Engineer with more than 10 years of professional experience with Microsoft 365 products such as SharePoint Online, OneDrive for Business, Teams Collaboration, Entra ID, Information Protection, Universal Print, and Microsoft 365 Licensing. He also has 15+ years of experience planning, administering, and operating SharePoint Server environments. In his spare time, Tobias is busy with updates in the Microsoft 365 world or on the road with his road bike and other sports activities. If you have additional questions, please contact me via LinkedIn or support@topedia.com.

No comments yet