Mailbox access via Microsoft Graph
Business Central's own email module covers sending mail from a flow – that's what the Send Email step uses. When a flow needs to do more than send, for example read the invoices that land in a shared mailbox, download their attachments, or move processed messages to a subfolder, it calls Microsoft Graph through the HTTP Request step instead.
Graph won't let a flow into a mailbox on its own. You first register an app in Microsoft Entra ID, give it access to exactly one mailbox, and hand its client ID and secret to an AutoFlow Credential. This page walks through that setup end to end.
Graph mail permissions are granted tenant-wide by default: an app with Mail.Read can read every mailbox in your organisation. Nothing in AutoFlow can narrow that down after the fact – the restriction has to be configured in Exchange Online. Step 4 is the part you must not skip.
What you need
- A Microsoft 365 tenant with the target mailbox hosted in Exchange Online (a shared mailbox works and needs no licence of its own).
- Someone with the Application Administrator (or Global Administrator) role in Entra ID to register the app and consent.
- Someone with the Exchange Administrator role, or membership in the Organization Management role group, to scope the app to the one mailbox.
- The Exchange Online PowerShell module on an admin machine for step 4 (
Install-Module ExchangeOnlineManagement).
You'll collect three values along the way. Keep them somewhere safe until the credential is created:
| Value | Where it comes from |
|---|---|
| Directory (tenant) ID | App registration overview |
| Application (client) ID | App registration overview |
| Client secret value | Created in step 2, shown once |
Step 1 – Register the app
- Open the Microsoft Entra admin center and go to Identity → Applications → App registrations → New registration.
- Give it a name that says what it is, for example
AutoFlow – Invoice Mailbox. - Under Supported account types, choose Accounts in this organizational directory only (single tenant).
- Leave Redirect URI empty. AutoFlow authenticates machine-to-machine; no user ever signs in to this app.
- Select Register.
On the Overview page that opens, copy the Application (client) ID and the Directory (tenant) ID.
Step 2 – Create a client secret
- In the app, open Certificates & secrets → Client secrets → New client secret.
- Describe it (
AutoFlow credential) and pick an expiry. Entra caps this at 24 months. - Select Add, then copy the Value column immediately – it is displayed only while you stay on the page. The Secret ID is not the secret; AutoFlow needs the Value.
Note the expiry date in whatever calendar your team actually reads. When the secret expires, every flow using the credential starts failing at the token request until you create a new secret and paste it into the credential.
AutoFlow Credentials authenticate to the token endpoint with a client secret. Certificate credentials aren't supported, so create a secret even if your tenant policy generally prefers certificates.
Step 3 – Decide how the app gets its permissions
There are two ways to give an Entra app access to mail, and they behave very differently.
| RBAC for Applications (recommended) | Entra API permissions | |
|---|---|---|
| Configured in | Exchange Online | Entra admin center |
| Scope | The mailboxes you name | Every mailbox in the tenant |
| Status | Microsoft's current model | Fine on its own only if the app really should see everything |
Because the two systems are additive, a tenant-wide grant in Entra can't be narrowed by an Exchange scope – the app ends up with access to everything. So for single-mailbox access, grant the permission only in Exchange Online and leave API permissions in the app registration empty.
If your app registration already carries Graph application permissions such as Mail.Read or Mail.Send from an earlier attempt, remove them under API permissions before continuing.
The Exchange roles you'll assign in the next step map one-to-one onto the Graph permissions:
| Exchange role | Grants | Use it for |
|---|---|---|
Application Mail.Read | Mail.Read | Reading messages and attachments. |
Application Mail.ReadWrite | Mail.ReadWrite | Reading plus moving, flagging, or deleting messages. |
Application Mail.Send | Mail.Send | Sending as the mailbox. |
Application Mail Full Access | Mail.ReadWrite + Mail.Send | Read, write and send in one assignment. |
Application MailboxFolder.Read | MailboxFolder.Read.All | Enumerating folders. |
Assign the narrowest role that covers what your flow does. A flow that only picks up incoming invoices needs Application Mail.Read and nothing more.
Step 4 – Scope the app to one mailbox (in PowerShell)
Everything in this step runs in PowerShell on an admin machine. There is no equivalent screen in the Exchange admin center or the Entra portal – the commands below can't be typed into a web page anywhere, and the only thing you go back to the browser for is looking up two IDs to paste into them.
Open a PowerShell window and install the Exchange Online module if it isn't there yet:
Install-Module ExchangeOnlineManagement -Scope CurrentUser
Then connect as an admin. A browser window opens for the sign-in; afterwards you stay in PowerShell for the rest of this step:
Import-Module ExchangeOnlineManagement
Connect-ExchangeOnline -UserPrincipalName admin@contoso.com
4a. Point Exchange at the app
Exchange needs its own pointer to the app's service principal, and it takes two IDs you have to look up in the browser: Entra admin center → Enterprise applications → your app. Copy the Application ID and the Object ID from there – not from App registrations, whose Object ID is a different value.
Back in PowerShell:
New-ServicePrincipal `
-AppId 71487acd-ec93-476d-bd0e-6c8b31831053 `
-ObjectId 6233fba6-0198-4277-892f-9275bf728bcc `
-DisplayName "AutoFlow Invoice Mailbox"
If the app doesn't appear under Enterprise applications at all, it has no service principal yet. Open the app registration's API permissions page and select Grant admin consent once (with the permission list empty) to create it.
4b. Define the scope
A management scope is a filter over mailbox properties. For a single mailbox, filter on its address:
New-ManagementScope `
-Name "AutoFlow Invoice Mailbox" `
-RecipientRestrictionFilter "PrimarySmtpAddress -eq 'invoices@contoso.com'"
If you expect the set of mailboxes to grow later, point the scope at a mail-enabled security group instead and add mailboxes to the group:
$Group = Get-Group -Identity "AutoFlow Mailboxes"
New-ManagementScope `
-Name "AutoFlow Mailboxes" `
-RecipientRestrictionFilter "MemberOfGroup -eq '$($Group.DistinguishedName)'"
Only direct members of the group count. Mailboxes that are members through a nested group stay out of scope.
4c. Assign the role
New-ManagementRoleAssignment `
-App 6233fba6-0198-4277-892f-9275bf728bcc `
-Role "Application Mail.Read" `
-CustomResourceScope "AutoFlow Invoice Mailbox"
Repeat for each role the flow needs – one assignment per role, all pointing at the same scope.
4d. Verify
Test-ServicePrincipalAuthorization `
-Identity 6233fba6-0198-4277-892f-9275bf728bcc `
-Resource invoices@contoso.com | Format-Table
InScope must be True for the roles you assigned. Run the same command against a mailbox the app should not reach and confirm it comes back False – that check is the whole point of this step.
Exchange caches an app's permissions for 30 minutes to 2 hours. Test-ServicePrincipalAuthorization bypasses the cache and shows the truth immediately, but a live Graph call may keep behaving the old way for a while after you change a role or scope.
Step 5 – Create the AutoFlow Credential
Open AutoFlow Credentials from Tell-Me, choose New, and fill in the card:
| Field | Value |
|---|---|
| Code | For example GRAPH-INVOICES. |
| Description | Graph access to invoices@contoso.com. |
| Test URL | https://graph.microsoft.com/v1.0/users/invoices@contoso.com/mailFolders/inbox |
| Authentication type | OAuth 2.0 client credentials |
| OAuth token URL | https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token |
| OAuth client ID | The Application (client) ID from step 1. |
| Client secret | The secret Value from step 2. |
| OAuth scope | https://graph.microsoft.com/.default |
| OAuth grant type | Client credentials |
| Client-credential transport | Request body (form-encoded) |
The scope is literally https://graph.microsoft.com/.default – not the individual permission names. Entra reads it as "everything this app is entitled to".
Lock the credential to the mailbox
Under Base URLs, add one entry:
https://graph.microsoft.com/v1.0/users/invoices@contoso.com
This is a second, independent safety net. AutoFlow refuses to send the credential to any URL that doesn't start with that prefix, so a mistyped or copy-pasted step URL pointing at another mailbox is rejected before the request leaves Business Central. It also lets your steps use short relative URLs like /messages.
Save the card and select Test. A 200 means the token was issued and Exchange let the app into the mailbox. Anything else is covered under Troubleshooting.
Step 6 – Call the mailbox from a flow
Add an HTTP Request step, select the credential under Credential, and use relative URLs.
Read the ten newest unread messages:
| Field | Value |
|---|---|
| Request Method | GET |
| Request URL | /messages?$filter=isRead eq false&$top=10&$orderby=receivedDateTime desc |
Feed the response into a Parser step to pull out sender, subject, and message IDs, then loop over them with a For Each loop.
Download an attachment:
| Field | Value |
|---|---|
| Request Method | GET |
| Request URL | /messages/{{message.id}}/attachments |
The contentBytes field of each attachment is base64 – the same shape the Send Email step's inline attachments expect.
Mark a message as read (needs Application Mail.ReadWrite):
| Field | Value |
|---|---|
| Request Method | PATCH |
| Request URL | /messages/{{message.id}} |
| Request Body | { "isRead": true } |
Send as the mailbox (needs Application Mail.Send):
| Field | Value |
|---|---|
| Request Method | POST |
| Request URL | /sendMail |
| Request Body | { "message": { "subject": "{{doc.no}} processed", "body": { "contentType": "HTML", "content": "<p>Done.</p>" }, "toRecipients": [ { "emailAddress": { "address": "{{customer.email}}" } } ] } } |
Set Content-Type: application/json on every request that carries a body. Graph replies to sendMail with 202 Accepted and an empty body.
Maintenance
- Rotate the secret before it expires: create a new secret in the app registration, paste the Value into the credential, save. Flows pick it up on their next run, and you can then delete the old secret in Entra.
- Add a mailbox when you scoped by group: add it to the group. Nothing changes in Entra or in AutoFlow – but widen the credential's Base URL, or create a second credential, so the allow-list doesn't block the new address.
- Retire the app: delete the app registration. Exchange removes the service principal pointer and its role assignments automatically; the management scope stays behind and can be deleted with
Remove-ManagementScope.
Troubleshooting
| What you see | What it usually means |
|---|---|
Test fails at the token endpoint with invalid_client | The client secret is wrong, expired, or you copied the Secret ID instead of the Value. |
Test fails with unauthorized_client or AADSTS700016 | The client ID or tenant ID in the token URL doesn't match the app. |
403 with ErrorAccessDenied | The token is fine, but Exchange doesn't let the app into this mailbox. Run Test-ServicePrincipalAuthorization against the address – usually the scope filter doesn't match, or the change hasn't left the cache yet. |
403 with Authorization_RequestDenied | The app has no permission for this call at all. Check that the role you assigned covers the operation – reading is not writing, and writing is not sending. |
404 with ResourceNotFound or MailboxNotEnabledForRESTAPI | The address in the URL doesn't resolve to an Exchange Online mailbox: a typo, a mail contact rather than a mailbox, or a mailbox still hosted on-premises. |
| The request is rejected before it fires, with no HTTP status | The step's URL doesn't match the credential's Base URL. Compare them character by character – a trailing / or a different mailbox address is enough. |
| The app can reach mailboxes it shouldn't | Tenant-wide Graph permissions are still consented on the app registration. Remove them under API permissions; the Exchange scope can't override them. |
See also
- AutoFlow Credentials – the credential store, base-URL allow-list, and secret handling.
- HTTP Request step – the step that makes the call.
- Send Email step – sending through Business Central's own email accounts, when Graph isn't needed.