Reference: Recommendations
Savings and hygiene recommendations, realized savings, access optimization and Azure Advisor. This is the data behind the VM Sizing, Orphaned Resources, Access Optimization and Advisor pages.
11 Routes
vm-sizing-recommendationsdisk-sizing-recommendationsorphaned-resource-recommendationsservice-retirement-recommendationssubscription-quota-recommendationsrealized-savings-recommendationsaccess-optimization-summarygetAdvisorRecommendationsSummarygetAdvisorRecommendationDetailsgetAdvisorRecommendationDiffgetAdvisorRecommendationDiffDetails
Whole set, no UI filters, hidden items included
Apart from realized savings and the Advisor routes, these routes take no parameters. They return the whole set in your scope, including items hidden in the UI, without the UI's on-screen filters. Filter in your script. Sizing amounts are USD list prices.
Right-Sizing
GET /api/vm-sizing-recommendations
Permission: View Resources
Right-sizing for VMs and scale sets.
Response: { recommendations[], metadata, totalCount, lastUpdatedScanId, lastUpdatedTimestamp }. metadata is { totalResourcesAnalyzed, totalResourcesWithRecommendations, aggressiveness } (installation-wide). Each recommendation:
| Field | Meaning |
|---|---|
| Identity fields | The VM or scale set |
recommendationType | vm_sku_downsize | vmss_sku_downsize |
autoscaleEnabled? | Scale sets |
currentConfiguration | { sku, vCPUs, memoryGB, monthlyCost, pricingTier?, instanceCount? } |
originalDetectedSku?, originalDetectedMonthlyCost? | Before a partial change |
recommendationSlots[] | Up to 18 alternatives, each { slotType, excludeRestricted?, targetSku, vCPUs, memoryGB, monthlyCost, monthlySavings, annualSavings, savingsPercentage, rationale, restrictions?, allPricing?, confidence: { level: very_high | high | medium | low, score, factors[], warnings[], requiredActions? } } |
utilizationEvidence | { analysisWindowDays, cpuUtilization { average, p95, maximum }, memoryUtilization?, memoryDataAvailable } |
workloadProfile | { type, recommendedFamily, dominantResource, … } |
severity | high when default monthly savings are above $200, medium when above $50, else low |
defaultMonthlySavings, bestMonthlySavings | Savings for the default slot and the best slot |
Notes
- The UI's default recommendation is the slot with
slotType: "default"and noexcludeRestricted. - Other slot types (
crossFamily,olderGeneration,burstable, …) correspond to the UI's option toggles.
Errors
- 500The error text, or Failed to fetch VM sizing recommendations
Example
GET /api/vm-sizing-recommendationsGET /api/disk-sizing-recommendations
Permission: View Resources
Managed disk tier and SKU recommendations. Same wrapper as VM sizing. Each recommendation:
| Field | Meaning |
|---|---|
recommendationType | disk_sku_downsize |
currentConfiguration | { sku, tier?, sizeGiB, provisionedIops, provisionedMBps, redundancy, isOsDisk, attachedVmId?, vmPowerState?, monthlyCost } |
recommendationSlots[] | Up to 4 (tierDowngrade, skuConversion, skuConversionHdd, v2Migration), each with targetSku, targetTier?, monthlyCost, monthlySavings, annualSavings, savingsPercentage, requiresVmDeallocation, oneWayMigration?, prerequisites?, rationale, confidence, warnings? |
utilizationEvidence | { analysisWindowDays, metricsAvailable, iops?: { avgP95, peakP95, maximum }, throughputMBps?: { avgP95, peakP95, maximum }, vmDeallocatedDays?, vmDeallocatedSince? }. iops and throughputMBps are absent when metricsAvailable is false; the vmDeallocated* fields appear only for disks attached to a stopped VM |
severity, defaultMonthlySavings, bestMonthlySavings | As for VM sizing, with the same thresholds |
Errors
- 500The error text, or Failed to fetch disk sizing recommendations
Example
GET /api/disk-sizing-recommendationsOrphans, Retirements and Quotas
GET /api/orphaned-resource-recommendations
Permission: View Resources
Unattached, idle, stopped or empty resources.
Response: { recommendations[], totalCount, totalMonthlyCost, totalAnnualCost, availableFilters: { types[], statuses[] }, lastUpdatedScanId, lastUpdatedTimestamp }. Each recommendation:
| Field | Meaning |
|---|---|
Identity fields, entityType | resource or resourceGroup |
recommendationType | One of: orphaned_public_ip, orphaned_managed_disk, orphaned_nic, orphaned_private_endpoint, orphaned_nsg, orphaned_route_table, idle_vpn_gateway, unattached_nat_gateway, idle_nat_gateway, idle_bastion, idle_application_gateway, stopped_vm_not_deallocated, idle_vm, stale_snapshot, empty_app_service_plan, empty_load_balancer, empty_application_gateway, orphaned_expressroute_circuit, orphaned_ddos_protection_plan, empty_sql_elastic_pool, empty_traffic_manager_profile, idle_traffic_manager_profile, orphaned_private_dns_zone, orphaned_frontdoor_waf_policy, empty_virtual_network, orphaned_ip_group, orphaned_firewall_policy, idle_container_registry, idle_storage_account, orphaned_api_connection, empty_subnet, empty_resource_group (new types may be added) |
status | Unattached, Idle, Stopped (Not Deallocated), Stale, Empty |
orphanedSince, orphanedDays, lastMetricDate? | Timing |
previousAttachment? | What it was attached to |
indirectOrphan? | Orphaned because something in between is orphaned |
monthlyCost?, annualCost?, rolling30DayCost?, currency? | Cost |
detectionEvidence | { detectionMethod: inventory | metrics, reason, … } |
recommendation | { action: "review_and_remove", rationale } |
detectedAt | When detected |
Notes
- Totals add amounts in different currencies without conversion.
- The UI's metrics-timeframe filter is not applied.
Errors
- 500The error text, or Failed to fetch recommendations
Example
GET /api/orphaned-resource-recommendationsGET /api/service-retirement-recommendations
Permission: View Resources
Resources affected by upcoming Azure service retirements.
Response: { recommendations[], metadata, guidance, totalCount, lastUpdatedScanId, lastUpdatedTimestamp }. The last two are null when there is no data. Each recommendation has a tier:
| Field | Meaning |
|---|---|
tier | advisorResource: confirmed for this exact resource (adds problemDescription, solutionDescription). resourceType: matched by resource type, unconfirmed. serviceHealth: an Azure Service Health event (adds eventTrackingId, eventStatus, eventSummary, impactStartTime, impactedServices[], impactedResourceCount, impactedResources[] up to 200) |
| Common | subscriptionId, subscriptionName, title, retirementDate?, retirementFeatureName?, category?, impact?, activeInArg |
| Resource tiers | Also retirementTypeId, resourceId, resourceName, resourceType, resourceGroup |
guidance | Keyed by retirementTypeId, each { detailedDescription?, potentialBenefits?, learnMoreLink?, serviceName? } |
metadata | { catalogEntryCount, catalogLastUpdated, tier1Count, tier2Count, tier3Count, subscriptionsScanned, serviceHealthFailedSubscriptions } |
Notes
- The same resource can appear in more than one tier.
- The UI hides type-level and undated items by default; the API does not.
Errors
- 500The error text, or Failed to fetch service retirement recommendations
Example
GET /api/service-retirement-recommendationsGET /api/subscription-quota-recommendations
Permission: View Resources
Subscriptions near their vCPU quota limits. Only quotas at or above the threshold are listed.
Response: { recommendations[], metadata, totalCount, lastUpdatedScanId, lastUpdatedTimestamp }. Each recommendation is one subscription: { subscriptionId, subscriptionName, quotas[]: { location, quotaName, currentUsage, quotaLimit, usagePercent }, totalQuotaCount, highestUsagePercent, locationCount, locationSummaries[] }. metadata is { minimumUtilizationPercent, totalSubscriptionsScanned, subscriptionsWithQuotas }.
Errors
- 500The error text, or Failed to fetch subscription quota recommendations
Example
GET /api/subscription-quota-recommendationsRealized Savings
GET /api/realized-savings-recommendations
Permission: per type: commitment_underutilized and commitment_expiring need View Costs; all other types need View Resources; a request mixing both needs both
Recommendations that were acted on, and the savings they produced. Kept about 2 years; removed if the issue comes back.
Parameters
| Name | Required | Notes |
|---|---|---|
types | yes | CSV of orphaned_resource_cleanup, vm_sku_downsize, vmss_sku_downsize, disk_sku_downsize, access_optimization, commitment_underutilized, commitment_expiring. No spaces after commas |
Errors
- 400
Missing required query parameter: types - 400
Query parameter types must name at least one recommendation type - 400
Invalid recommendation type(s): <list> - 403
You do not have permission to read commitment realized savings recommendations. - 403
You do not have permission to read these realized savings recommendations. - 500The error text, or Failed to fetch realized savings recommendations
Response: { recommendations[], totalCount, totalMonthlySavings, totalAnnualSavings, availableResolutionTypes[] }. Each recommendation:
| Field | Meaning |
|---|---|
Identity fields, recommendationType | What was acted on |
resolutionType | resource_deleted, resource_reattached, vm_deallocated, sku_changed, sku_partially_downsized, assignment_removed, eligibility_removed, commitment_utilization_improved, commitment_renewed |
estimatedMonthlySavings, estimatedAnnualSavings | 0 for access optimization |
currency | Null for access optimization |
originalSnapshot | { label, recommendedTarget?, actualTarget?, originalMonthlyCost? } |
resolvedAt, resolvedByScanId | When and in which scan |
Example
GET /api/realized-savings-recommendations?types=orphaned_resource_cleanup,vm_sku_downsize,disk_sku_downsizeAccess Optimization
GET /api/access-optimization-summary
Permission: View Resources
Evidence for unused, stale, over-scoped, over-privileged, sprawling or redundant role assignments. The labels themselves are not stored; derive them with the rules below. Rows inherited through an Entra group are dropped when that group's membership lookup fails (logged server-side only).
Response: { recommendations[], scopeMetadata, totalCount, lastUpdatedScanId, lastUpdatedTimestamp }. scopeMetadata is { subscriptionResourceGroupCounts, managementGroupChildCounts, managementGroupHierarchy, totalManagementGroups, totalSubscriptions }, narrowed to your scope. Each recommendation is one principal + role + scope:
| Field | Meaning |
|---|---|
principalId, principalDisplayName, principalType | Who |
roleDefinitionName, roleDefinitionId, roleCategory | The role |
scope, scopeType, entityName?, subscriptionId, subscriptionName?, resourceGroupName? | subscriptionId is "N/A" at management-group scope |
createdOn | Assignment date |
grantedViaGroupId?, grantedViaGroupName?, grantedViaGroupChain? | Inherited rows |
pimEligible?, pimStartDateTime?, pimEndDateTime? | PIM |
lastActivityTimestamp, totalActivityCount | Over 365 days |
operationCounts, operationTypes[] | { create, update, delete, rbac, action } |
lastRbacTimestamp | Last RBAC operation |
scopeActivity[], activeScopes[], totalScopes, scopeEfficiency | Where the principal was active |
crossSubscriptionCount | Subscriptions the principal has access to |
redundancyReason?, redundancyDetails? | Set when the assignment is redundant |
Deriving labels (look-back window W days)
| Applies to | Label | Rule |
|---|---|---|
| PIM-eligible assignments | unusedEligibility | Only this label applies: not a group, role is critical or management, and no activity in W |
| Non-group principals with a critical or management role | unused | No activity in W |
stale | Otherwise, when days since last activity > W × 0.5 | |
overScoped | Active scopes within W ÷ totalScopes < 30%, with at least one active scope | |
| Non-group principals with a critical role | overPrivileged | No RBAC operation in W and the assignment is not already unused |
| Any principal, including groups | excessiveSprawl | crossSubscriptionCount ≥ 5 |
redundantAssignment | redundancyReason is present |
Errors
- 500
Failed to retrieve access optimization recommendations
Example
GET /api/access-optimization-summaryAzure Advisor
Shared Advisor values
- Single-scan routes accept
latest. - Category:
Cost,HighAvailability,Performance,Security,OperationalExcellence. Impact:High,Medium,Low. - Hidden and legacy items are included. Legacy means the target resource is no longer in the inventory.
GET /api/getAdvisorRecommendationsSummary
Permission: View Advisor Recommendationsaccepts scanId=latest
Advisor recommendation counts, one row per resource × recommendation type.
Parameters
| Name | Required |
|---|---|
scanId | yes |
Errors
- 400
scanId parameter is required - 500
Failed to retrieve advisor hierarchy summaries
Response: { compactSummaries[], availableFilters: { categories, impacts, resourceTypes }, totalCount }. Each row:
| Field | Meaning |
|---|---|
entityId, displayName, entityType | entityId is <originalResourceId>|<recommendationTypeId> (Azure casing; lower-case it before comparing with resourceId values from other routes) |
subscriptionId, subscriptionName, resourceGroupName?, resourceType? | Location |
recommendationTypeId?, problemDescription? | The recommendation type |
totalCount | Recommendations |
categoryCounts, impactCounts, hiddenCounts, legacyCounts | Dictionaries |
combinedCounts | Key category|impact|hidden|legacy |
Example
GET /api/getAdvisorRecommendationsSummary?scanId=latestGET /api/getAdvisorRecommendationDetails
Permission: View Advisor Recommendationsaccepts scanId=latest
Advisor recommendations for one entity, ordered by category, then impact.
Parameters
| Name | Required | Notes |
|---|---|---|
scanId, subscriptionId | yes | subscriptionId is required for every type |
entityType | no | resource (default), resourceGroup, subscription |
resourceGroupName, resourceName | per entityType |
Errors
- 400
entityType must be one of subscription, resourceGroup, resource - 400
subscriptionId and scanId parameters are required - 400
resourceGroupName parameter is required for resourceGroup and resource queries - 400
resourceName parameter is required for resource queries - 500
Failed to retrieve advisor recommendation details
Response: { recommendations[], totalCount }. Each recommendation:
| Field | Meaning |
|---|---|
recommendationId | Stable across scans |
recommendationName, recommendationTypeId? | Type |
category, impact | Classification |
problemDescription, solutionDescription | Text |
| Target and hierarchy fields | The resource |
impactedField?, impactedValue? | What Advisor looked at |
extendedProperties? | Cost recommendations include savingsAmount, savingsCurrency, annualSavingsAmount |
lastUpdated | Advisor's timestamp |
isHidden?, notes?, isLegacy? | Annotations and legacy flag |
Example
GET /api/getAdvisorRecommendationDetails?scanId=latest&subscriptionId=<guid>&resourceGroupName=rg-data&resourceName=sqlsrv01GET /api/getAdvisorRecommendationDiff
Permission: View Advisor Recommendations
Advisor recommendation change counts between two scans.
Parameters
| Name | Required |
|---|---|
baseScanId, compareScanId | yes |
Errors
- 400
Please provide both 'baseScanId' and 'compareScanId' query parameters. - 404
Base scan with ID '<id>' not found. - 404
Compare scan with ID '<id>' not found. - 400
Base scan '<base>' must be older than compare scan '<compare>'. - 500
An error occurred while processing the advisor recommendation comparison.
Response: { summary: { baseScanId, compareScanId, baseScanTime, compareScanTime, added, deleted, modified }, recommendations: [] (always empty), hierarchySummary[] }. hierarchySummary is as in the Defender diffs.
Example
GET /api/getAdvisorRecommendationDiff?baseScanId=…&compareScanId=…GET /api/getAdvisorRecommendationDiffDetails
Permission: View Advisor Recommendations
Paged changed Advisor recommendations for one entity. Order: added, modified, deleted.
Parameters
| Name | Required | Notes |
|---|---|---|
baseScanId, compareScanId, subscriptionId | yes | |
resourceGroupName, resourceName | no | Includes everything below that level |
changeType | no | One value |
page | no | Default 1 |
pageSize | no | 1-100, default 25 |
Errors
- 400Missing-pair message, scan-pair 404/400
- 400
Please provide 'subscriptionId' query parameter. - 400
Invalid 'changeType' parameter. Must be 'added', 'modified', or 'deleted'. - 400
page must be a positive integer - 400
pageSize must be between 1 and 100 - 500
An error occurred while processing the advisor recommendation diff details.
Response: { recommendations[], totalCount, totalPages, currentPage }. Each recommendation is a full one plus changeStatus and changes[].
Notes
- For
modifieditems,changescan be empty.
Example
GET /api/getAdvisorRecommendationDiffDetails?baseScanId=…&compareScanId=…&subscriptionId=<guid>&changeType=added&pageSize=50