All you need to know about Exchange Online Admin API (or how to run cmdlets without PowerShell) – part 2

This is a continuation of the topic discussed in our previous article, make sure to go over it first.

REST cmdlets and additional methods

If you made it this far, you know how to run any Exchange Online cmdlet without the need to use the module itself. But there is more to the Exchange Online Admin API than the InvokeCommand method, as we know from the presence of the REST cmdlets. In fact, each REST cmdlet has its own endpoint under https://outlook.office365.com/adminapi. For example:

$uri = "https://outlook.office365.com/adminapi/beta/tenant.onmicrosoft.com/Mailbox('user@domain.com')"
$res = Invoke-WebRequest -Uri $uri -Headers $authHeader
($res.Content | ConvertFrom-Json)

corresponds to the Get-EXOMailbox cmdlet, and so on. But that’s not all! A plethora of other methods are also available for us to tap into, all nested under the same “parent” endpoint. While there isn’t any publicly documented list of endpoints, you can get one by simply running a GET query against https://outlook.office365.com/adminapi/beta/tenant.onmicrosoft.com:

$uri = "https://outlook.office365.com/adminapi/beta/tenant.onmicrosoft.com"
$res = Invoke-WebRequest -Uri $uri -Headers $authHeader
$ApiEndpoints = ($res.Content | ConvertFrom-Json).value

Currently, there are a total of 51 entries returned, along with their URLs, as shown below:

$ApiEndpoints | sort Name 

name                                  kind      url
----                                  ----      ---
ActiveSyncDeviceAccessRule            EntitySet ActiveSyncDeviceAccessRule
ActiveSyncDeviceClass                 EntitySet ActiveSyncDeviceClass
ActiveSyncOrganizationSettings        EntitySet ActiveSyncOrganizationSettings
AddressBookPolicy                     EntitySet AddressBookPolicy
AntiPhishPolicyPresentation           EntitySet AntiPhishPolicyPresentation
AntiPhishRule                         EntitySet AntiPhishRule
AtpPolicyForO365Presentation          EntitySet AtpPolicyForO365Presentation
BasicInfo                             EntitySet BasicInfo
CalendarProcessing                    EntitySet CalendarProcessing
CasMailbox                            EntitySet CasMailbox
CmdletInfo                            EntitySet CmdletInfo
ConfigAnalyzerPolicyRecommendation    EntitySet ConfigAnalyzerPolicyRecommendation
DirectMobileDevice                    EntitySet DirectMobileDevice
Divergence                            EntitySet Divergence
DynamicDistributionGroup              EntitySet DynamicDistributionGroup
EligibleDistributionGroup             EntitySet EligibleDistributionGroup
ExchangeManagementScope               EntitySet ExchangeManagementScope
ExchangeRoleGroup                     EntitySet ExchangeRoleGroup
ExchangeRoleGroupMember               EntitySet ExchangeRoleGroupMember
GraphConnectorGroup                   EntitySet GraphConnectorGroup
GraphConnectorGroupMember             EntitySet GraphConnectorGroupMember
HistoricalSearch                      EntitySet HistoricalSearch
HostedContentFilterPolicyPresentation EntitySet HostedContentFilterPolicyPresentation
HostedContentFilterRule               EntitySet HostedContentFilterRule
InboundConnector                      EntitySet InboundConnector
Mailbox                               EntitySet Mailbox
MailboxAutoReplyConfiguration         EntitySet MailboxAutoReplyConfiguration
MailboxPlan                           EntitySet MailboxPlan
MailboxRecoverableItem                EntitySet MailboxRecoverableItem
MalwareFilterPolicy                   EntitySet MalwareFilterPolicy
MalwareFilterRule                     EntitySet MalwareFilterRule
MobileDeviceMailboxPolicy             EntitySet MobileDeviceMailboxPolicy
OutboundConnector                     EntitySet OutboundConnector
Place                                 EntitySet Place
Recipient                             EntitySet Recipient
ReportSchedule                        EntitySet ReportSchedule
RetentionPolicy                       EntitySet RetentionPolicy
RoleAssignmentPolicy                  EntitySet RoleAssignmentPolicy
RoleAssignments                       EntitySet RoleAssignments
RoleDefinitions                       EntitySet RoleDefinitions
SafeAttachmentPolicy                  EntitySet SafeAttachmentPolicy
SafeAttachmentRule                    EntitySet SafeAttachmentRule
SafeLinksPolicyPresentation           EntitySet SafeLinksPolicyPresentation
SafeLinksRule                         EntitySet SafeLinksRule
SecurityPrincipal                     EntitySet SecurityPrincipal
SharingPolicy                         EntitySet SharingPolicy
UnifiedGroup                          EntitySet UnifiedGroup
UnifiedRbacManagementScope            EntitySet UnifiedRbacManagementScope
UnifiedRbacRoleAssignment             EntitySet UnifiedRbacRoleAssignment
UnifiedRbacRoleDefinition             EntitySet UnifiedRbacRoleDefinition
User                                  EntitySet User

If you examine the list above closely, you will notice that few methods that map to some of the existing REST cmdlets appear to be missing. One such example is the Get-EXOMailboxPermission cmdlet. So, what’s going on? Well, much like the Graph API, we have navigation properties which are not immediately visible from the above. We can however fetch the metadata document to expose them, as well as some additional methods and entities.

#Fetch the metadata document
$uri = 'https://outlook.office365.com/adminapi/beta/tenant.onmicrosoft.com/$metadata'
$res = Invoke-WebRequest -Uri $uri -Headers $authHeader
[xml]$metadata = $res.Content #cast to XML

#Explore the metadata
($metadata.Edmx.DataServices.Schema | ? {$_.Namespace -eq "Exchange"})
Namespace       : Exchange
xmlns           : http://docs.oasis-open.org/odata/ns/edm
ComplexType     : {ComplexEntry, ByteArrayType, StringFieldDeltaUpdateData, MailboxRecoverableItemsQuery…}
EntityType      : {BasicInfo, MailboxPermission, MailboxFolderPermission, MailboxFolder…}
EnumType        : {ElcFolderType, OofState, ExternalAudience, RecipientAccessRight…}
Function        : {GetMobileDeviceStatistics, GetMailboxStatistics, GetMailboxStatisticsV2, GetMailboxFolderStatistics…}
Action          : {UpdateMailboxArchive, GetRecoverableItems, RestoreRecoverableItems, ValidateOutboundConnector…}
EntityContainer : EntityContainer

Note that you still need to be authenticated to query the metadata, the request will fail if you don’t pass a valid access token! The set of EntityType objects from the metadata map to the endpoint list above, with their total count being 57. This count includes the set of navigation properties we alluded to above, and here is how to get them:

($metadata.Edmx.DataServices.Schema | ? {$_.Namespace -eq "Exchange"}).EntityType | ? {$_.Name -eq "Mailbox"} | select -ExpandProperty NavigationProperty

Name                         Type                                        ContainsTarget
----                         ----                                        --------------
MailboxPermission            Collection(Exchange.MailboxPermission)      true
MailboxFolder                Collection(Exchange.MailboxFolder)          true
MobileDevice                 Collection(Exchange.MobileDevice)           true
MailboxRecoverableItem       Collection(Exchange.MailboxRecoverableItem) true
MailboxRegionalConfiguration Exchange.MailboxRegionalConfiguration       true

Apart from the EntityType set, we also have a set of Action and Function to work with, both exposing some methods that are not accessible in cmdlet form. For example, we have the following set of Actions:

($metadata.Edmx.DataServices.Schema | ? {$_.Namespace -eq "Exchange"}).Action

Name                             IsBound Parameter
----                             ------- ---------
UpdateMailboxArchive             true    {mailbox, archive}
GetRecoverableItems              true    {bindingParameter, value}
RestoreRecoverableItems          true    {bindingParameter, value}
ValidateOutboundConnector        true    {outboundConnector, recipients}
DeleteGroup                      true    Parameter
DeleteGroupMember                true    {graphConnectorGroup, graphConnectorGroupMemberId}
UpgradeDistributionGroup         true    {EligibleDistributionGroup, DlIdentities}
ClearMobileDevice                true    {mobileId, cancel}
SearchMessageTrace                       Parameter
SearchMessageTraceDetail                 Parameter
GetMailDetailTransportRuleReport         Parameter
GetMailTrafficPolicyReport               Parameter
GetMailDetailDlpPolicyReport             Parameter
RbacQuery                                Parameter
Initialize
InitializeLiteFRC
InvokeCommand                            Parameter

as well as the following set of Functions:

($metadata.Edmx.DataServices.Schema | ? {$_.Namespace -eq "Exchange"}).Function

Name                                IsBound Parameter                                     ReturnType
----                                ------- ---------                                     ----------
GetMobileDeviceStatistics           true    Parameter                                     ReturnType
GetMailboxStatistics                true    Parameter                                     ReturnType
GetMailboxStatisticsV2              true    {bindingParameter, mailboxGuid, databaseGuid} ReturnType
GetMailboxFolderStatistics          true    {mailboxfolder, folderscope}                  ReturnType
BySmtpAddress                       true    {CasMailbox, SmtpAddress}                     ReturnType
BySmtpAddress                       true    {Recipient, SmtpAddress}                      ReturnType
GetRecipientPermissionByFilters     true    {Recipient, identity, trustee, accessRights}  ReturnType
GetMobileDeviceStatisticsByIdentity         Parameter                                     ReturnType

Unfortunately, the metadata document doesn’t reveal much detail on how exactly we can use any of the above. But if you are willing to spend some time on it, you can get some of these working. For example, GetMailDetailTransportRuleReport is the endpoint to use if you want to fetch the (detailed) Exchange Transport Rule report. What the metadata doesn’t tell us is how to construct the query itself, but I got you covered! Here’s a working example of fetching the Exchange Transport Rule report with most of the parameters it accepts:

#Prepare the request body. Must include QueryTable node
$body = @{
    QueryTable = @{
        Direction = @("Outbound","Inbound")
        Action = @("SetAuditSeverityHigh","SetAuditSeverityMedium","SetAuditSeverityLow")
        EventType = @("TransportRuleHits")
        StartDate = "2025-11-12T00:00:00.000Z"
        EndDate = "2025-11-18T23:59:59.000Z"
        Page = 1
        PageSize = 30
    }
}

#Fetch the report
$uri = "https://outlook.office365.com/adminapi/beta/tenant.onmicrosoft.com/GetMailDetailTransportRuleReport"
$res = Invoke-WebRequest -Uri $uri -Headers $authHeader -Method Post -Body ($body | ConvertTo-Json -Depth 5)
($res.Content | ConvertFrom-Json).value

The output should now include the relevant details from the Exchange Transport Rule report, as available within the EAC.

Some PATCH examples

If you made it thus far, it is time for the big reveal! While Microsoft has been telling us that it only plans to support read-only REST cmdlets, the underlying methods do support PATCH, POST and DELETE operations. One should probably think twice before attempting a non-read operation against an unsupported and undocumented endpoint, but such are indeed possible. For example, we can use the code below to update the custom attribute of a given mailbox user:

#Prepare the request body
$body = @{
    CustomAttribute1="TestValue"
} | ConvertTo-Json -Depth 5

#PATCH the mailbox object
$uri = "https://outlook.office365.com/adminapi/beta/tenant.onmicrosoft.com/Mailbox('user@domain.com')"
$res = Invoke-WebRequest -Uri $uri -Headers $authHeader -Method PATCH -Body $body

Successful execution of the above request will result in an 204 NoContent reply. You can use any of the available methods to confirm the change was indeed committed! Support for PATCH operations is not limited to the endpoints corresponding to the REST cmdlets either. Some of the other methods outlined above also support PATCH-ing. For example, we can leverage the CalendarProcessing method to change how booking requests for a given room mailbox are processed:

#Prepare the request body
$body = @{
    AllBookInPolicy=$false
} | ConvertTo-Json -Depth 5

#PATCH the mailbox object
$uri = "https://outlook.office365.com/adminapi/beta/tenant.onmicrosoft.com/CalendarProcessing('room@domain.com')"
$res = Invoke-WebRequest -Uri $uri -Headers $authHeader -Method Patch -Body $body
($res.Content | ConvertFrom-Json)

SCC Endpoints

As you are probably aware, the Security and Compliance PowerShell cmdlets also use the same proxy behavior, and therefore most of what we examined above applies to them, too. The permission requirements on the API side are also the same, you still need the Exchange.Manage or Exchange.ManageAsApp scopes, which you need to complement with the required Purview admin roles for the operations you plan to execute. There are however few differences in the process.

First, the resource (audience) for which you need to obtain the access token is different, as represented by the following URI: https://ps.compliance.protection.outlook.com. Once you have a valid access token for said resource, you can prepare the authentication header and the body payload just like we described above. The request URL must accordingly be adjusted to match the URI as well. Here’s a full example:

#Get an access token
$app = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx").WithRedirectUri("http://localhost").Build()

$Scopes = New-Object System.Collections.Generic.List[string]
$Scope = "https://ps.compliance.protection.outlook.com/.default"
$Scopes.Add($Scope)

$token = $app.AcquireTokenInteractive($Scopes).ExecuteAsync().Result

#Set the auth header and routing hint
$authHeader = @{
    'Content-Type'='application/json'
    'Authorization'="Bearer $($token.AccessToken)"
    'X-ResponseFormat'= "json"
    'X-AnchorMailbox' = "UPN:user@domain.com"
}

#Cmdlet payload
$body = @{
    CmdletInput = @{
        CmdletName="Get-RetentionCompliancePolicy"
    }
} | ConvertTo-Json -Depth 5

#Sample Request
$uri = "https://eur02b.ps.compliance.protection.outlook.com/adminapi/beta/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/InvokeCommand"
$res = Invoke-WebRequest -Method POST -Uri $uri -Headers $authHeader -Body $body
($res.Content | ConvertFrom-Json).Value

You might have noticed that we use the region-specific URI in the above request, i.e. “eur02b”, but the global one also works.

Closing remarks and summary

One thing we haven’t mentioned thus far is that you can interchangeably use https://outlook.office.com/adminapi (without the “365” suffix) instead of the https://outlook.office365.com/adminapi across all the examples we covered. We’re following the notation used by the built-in cmdlets for our examples, but both should works equally fine.

On a similar note, you might have noticed that all examples leverage the /beta endpoint. This is again due to the behavior of the built-in cmdlets, and shouldn’t make a difference as there is no officially supported version of the API anyway. There is a /v1.0 endpoint as well, with some notable differences in the set of methods exposed therein. Stick to the /beta endpoint for the “full” experience. On the other hand, the recently announced additions to the Exchange Admin API are homed under a /v2.0 version of the endpoint. This one however has nothing to do with anything we’ve discussed here. TL;DR – best use the /beta endpoint and forget about the rest.

Another question that regularly pops up is restricting permissions, especially with the non-interactive scenario. Now that the RBAC for applications feature is available, you can treat service principals just like any other security principal in Exchange and manage role assignments for them as needed. The only thing you need to do is make sure the service principal object is “known” to Exchange, which unfortunately is still a manual task (i.e. you have to use New-ServicePrincipal to provision it). It is worth reiterating that the API scopes do not grant any access on their own, Exchange’s RBAC is responsible for that.

In summary, we explored the hidden depth behind the Exchange Online Admin API. On the “officially supported” front, said API powers the set of REST-based cmdlets and plays the role of a proxy service for all the classic cmdlets as means to bypass the WinRM dependency. Multiple other methods are available under the endpoint, enabling all sorts of useful operations, in a non-supported and non-documented manner. The high risk that comes with using such methods can be offset by the high reward of being able to perform operations via a RESTful interface, without any need to use PowerShell.

Sadly, to this day Microsoft has not communicated any plans to move all these to the Graph API, or expose them publicly in a supported manner. So while we can make some parallels with the Graph, for example the support for pagination or $select, many of the QoL improvements we have grown accustomed to with the Graph are not available, and likely never will be. Still, there are many scenarios where the existing admin API endpoints and methods can be useful, so hopefully this guide will get you started on your journey with them.

1 thought on “All you need to know about Exchange Online Admin API (or how to run cmdlets without PowerShell) – part 2

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Discover more from Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading