Skip to content

PowerShell Examples

PowerShell Examples

Most examples here use a delegated token, so they run as you and can do exactly what you can do in the portal. The last one runs unattended, as an Azure Automation Account’s managed identity, with no secret anywhere. See Authentication for both, and the API reference for which endpoints each can reach.

All of these call supported endpoints only. If you are looking for the older CI/CD upload example, see What happened to the upload example at the bottom.

The examples below assume this is in scope.

Terminal window
function Get-AppStoreToken {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$ClientId, # the App Store app registration
[Parameter(Mandatory)][string]$TenantId
)
# MSAL.PS is one option; any MSAL-based library works. The important part is the scope:
# a delegated token for this API, not a client-credentials token.
$token = Get-MsalToken -ClientId $ClientId -TenantId $TenantId `
-Scopes "api://$ClientId/access_as_user" -Interactive
return $token.AccessToken
}
# Set these once in your profile.
$ClientId = $env:APPSTORE_CLIENT_ID
$TenantId = $env:APPSTORE_TENANT_ID
$AppStoreHost = $env:APPSTORE_HOST # https://appstore.contoso.com
$accessToken = Get-AppStoreToken -ClientId $ClientId -TenantId $TenantId
$headers = @{ Authorization = "Bearer $accessToken" }

There is no client secret here, and there is nothing to store in a secret manager. The token belongs to you and expires on its own.

Terminal window
$apps = Invoke-RestMethod -Uri "$AppStoreHost/api/apps" -Headers $headers
$apps | Select-Object id, name, publisher, version | Sort-Object name | Format-Table

For a large catalog, page through it instead:

Terminal window
$page = Invoke-RestMethod -Uri "$AppStoreHost/api/apps/paginated?page=1&pageSize=50" -Headers $headers

The read behind a vulnerability-driven workflow: find out whether a newer version exists before asking anyone to approve deploying it.

Terminal window
$updates = Invoke-RestMethod -Uri "$AppStoreHost/api/winget/updates/available" -Headers $headers
$updates |
Select-Object appName, currentVersion, availableVersion |
Format-Table

Pair it with your vulnerability feed by matching on the app name or publisher, then route the ones that matter to whoever decides.

Terminal window
$deployments = Invoke-RestMethod -Uri "$AppStoreHost/api/admin/update-deployments" -Headers $headers
$deployments |
Where-Object { $_.status -in @('InProgress', 'Paused') } |
Select-Object id, appName, fromVersion, toVersion, status |
Format-Table
# One rollout, including per-ring progress.
$id = $deployments[0].id
Invoke-RestMethod -Uri "$AppStoreHost/api/admin/update-deployments/$id" -Headers $headers
Terminal window
$mine = Invoke-RestMethod -Uri "$AppStoreHost/api/requests" -Headers $headers
$mine | Select-Object id, appName, status, requestedDate | Format-Table
# One request.
Invoke-RestMethod -Uri "$AppStoreHost/api/requests/<request-id>" -Headers $headers

Only meaningful if the signed-in person is entitled to decide the request. The API applies the same rules as the portal.

Terminal window
# What is waiting for your decision.
$pending = Invoke-RestMethod -Uri "$AppStoreHost/api/requests/pending" -Headers $headers
$pending | Select-Object id, appName, userDisplayName, requestedDate | Format-Table
# Approve one. A comment is optional.
$id = $pending[0].id
Invoke-RestMethod -Method Post -Uri "$AppStoreHost/api/requests/$id/approve" -Headers $headers `
-ContentType 'application/json' `
-Body (@{ comments = 'Approved: standard software for this team.' } | ConvertTo-Json)
# Reject one. A reason is required.
Invoke-RestMethod -Method Post -Uri "$AppStoreHost/api/requests/$id/reject" -Headers $headers `
-ContentType 'application/json' `
-Body (@{ comments = 'Not licensed for this department.' } | ConvertTo-Json)

Things that will refuse, and should. Approving your own request. Approving a request whose current stage does not name you or a group you belong to. Rejecting without a reason. Approving a request somebody else has just decided, which returns a message saying so rather than overwriting their decision.

Earlier versions of this page showed a CI/CD pipeline uploading a custom MSI through /api/admin/app-upload/…. That endpoint still exists and the portal uses it, but it is not part of the supported API: it may change shape or move in any release, and a pipeline built on it would break without warning.

The supported list is deliberately small, and reads dominate it, because a read that changes shape is a bug while a mutation that runs twice is an incident. If you need to publish apps from a pipeline, tell us: the list should grow from what customers actually automate.

The older examples also authenticated with a client secret, and that is still not supported and will not be. Unattended access now works without one: see Unattended: a managed identity and the Azure Automation example below.

No secret, no certificate, nothing to rotate. The runbook authenticates as the Automation Account’s own system-assigned managed identity, which your administrator has granted the AppStore.Automation.Read role.

Terminal window
# In an Azure Automation runbook. -Identity uses the Automation Account's
# system-assigned managed identity: there is no credential to supply.
Connect-AzAccount -Identity | Out-Null
# A token for the App Store workload API. Your administrator has the audience;
# it is the workload registration's Application ID URI.
$audience = Get-AutomationVariable -Name 'AppStoreApiAudience' # api://<workload-app-id>
$appStore = Get-AutomationVariable -Name 'AppStoreHost' # https://appstore.contoso.com
$token = (Get-AzAccessToken -ResourceUrl $audience).Token
$headers = @{ Authorization = "Bearer $token" }
# Which apps have a newer version upstream. This is the read the unattended
# surface exists for: find out what is true, then act on it outside App Store.
$updates = Invoke-RestMethod -Uri "$appStore/api/winget/updates/available" -Headers $headers
$updates |
Select-Object appName, currentVersion, availableVersion |
Format-Table
# For example: raise a ticket, or notify the people who decide. The managed
# identity cannot deploy or approve anything itself, by design.

Setting this up end to end, including granting the role: Call the API from Azure Automation.

What a managed identity cannot do here. Approve, reject, deploy, publish, upload, or read anything scoped to a person. Those return 403, and the approval endpoints refuse before any approval rule is consulted.

  • The full route list, including internal routes, is in the OpenAPI spec. See the API reference for how to get it and why being in the spec does not make a route supported.
  • Reporting and data export belong in BI for Intune, not here.