Part 2: Extension Management
In Part 1, we walked through authentication and the fundamentals of the Business Central Administration API. In this second installment, we focus on practical extension management tasks that you can automate to streamline your development workflow. We will cover deploying a .app file, monitoring installation progress, handling failures, and uninstalling extensions through the API.
Before calling any Administration API endpoints, we need an authentication header containing a valid access token. The following script retrieves a token using client credentials and stores it in the $headers variable for later use:
$tenantId = "00000000-1111-2222-3333-444444444444"
$clientId = "55555555-6666-7777-8888-999999999999"
$clientSecret = "TheSecretFromAzureAppRegistration"
$baseUri = "https://api.businesscentral.dynamics.com"
$url = $baseUri+"/admin/v2.6"
$body = @{
grant_type = "client_credentials"
client_id = $clientId
client_secret = $clientSecret
scope = "https://api.businesscentral.dynamics.com/.default"
}
$token = Invoke-RestMethod -Method Post `
-Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
-Body $body
$headers = @{ Authorization = "Bearer $($token.access_token)" }
Once authenticated, we can begin interacting with the extension management endpoints.
Installing an APP Extension
The script below prompts the user for an environment name and a .app file (Line 1-18), then prepares a multipart form‑data request required by the pteInstall endpoint. This format is necessary because the API must receive both metadata and the binary extension file in a single request.
# Prompt for environment name
$envName = Read-Host "Enter the environment name (e.g., Production)"
Add-Type -AssemblyName System.Windows.Forms
$dialog = New-Object System.Windows.Forms.OpenFileDialog
$dialog.Filter = "Business Central App (*.app)|*.app"
$dialog.Title = "Select a Business Central .app file"
$null = $dialog.ShowDialog()
if ($dialog.FileName -eq "") {
Write-Host "❌ No file selected. Exiting."
return
}
$appFilePath = $dialog.FileName
Write-Host "Selected APP file: $appFilePath"
# JSON body required by pteInstall
$jsonBody = @{
deploymentSchedule = "Immediate"
syncMode = "Add"
languageId = "en-US"
acceptIsvEula = $true
installOrUpdateNeededDependencies = $false
} | ConvertTo-Json -Depth 5
$boundary = [System.Guid]::NewGuid().ToString()
$lf = "`r`n"
$ms = New-Object System.IO.MemoryStream
$writer = New-Object System.IO.StreamWriter($ms)
# extensionFile (binary)
$writer.Write("--$boundary$lf")
$writer.Write("Content-Disposition: form-data; name=`"extensionFile`"; filename=`"$(Split-Path $appFilePath -Leaf)`"$lf")
$writer.Write("Content-Type: application/octet-stream$lf$lf")
$writer.Flush()
$ms.Write([System.IO.File]::ReadAllBytes($appFilePath), 0, (Get-Item $appFilePath).Length)
$writer.Write($lf)
# deploymentSchedule
$writer.Write("--$boundary$lf")
$writer.Write("Content-Disposition: form-data; name=`"deploymentSchedule`"$lf$lf")
$writer.Write("Immediate$lf")
# syncMode
$writer.Write("--$boundary$lf")
$writer.Write("Content-Disposition: form-data; name=`"syncMode`"$lf$lf")
$writer.Write("Add$lf")
# languageId
$writer.Write("--$boundary$lf")
$writer.Write("Content-Disposition: form-data; name=`"languageId`"$lf$lf")
$writer.Write("en-US$lf")
# acceptIsvEula
$writer.Write("--$boundary$lf")
$writer.Write("Content-Disposition: form-data; name=`"acceptIsvEula`"$lf$lf")
$writer.Write("true$lf")
# installOrUpdateNeededDependencies
$writer.Write("--$boundary$lf")
$writer.Write("Content-Disposition: form-data; name=`"installOrUpdateNeededDependencies`"$lf$lf")
$writer.Write("false$lf")
# closing boundary
$writer.Write("--$boundary--$lf")
$writer.Flush()
$ms.Position = 0
$Results = Invoke-RestMethod `
-Uri "$url/applications/BusinessCentral/environments/$envName/apps/pteInstall" `
-Method Post `
-Headers $headers `
-ContentType "multipart/form-data; boundary=$boundary" `
-Body $ms
$AppID = $Results.appId
while ($true) {
$status = Invoke-RestMethod -Method Get `
-Uri "$url/applications/BusinessCentral/environments/$envName/apps/$AppId/operations" `
-Headers $headers
# Get the most recent operation
$op = $status.value | Sort-Object createdOn -Descending | Select-Object -First 1
Write-Host "Operation $($op.id): $($op.status)"
if ($op.status -eq "Succeeded") {
Write-Host "✔ App install/update succeeded."
break # <-- breaks the WHILE
}
if ($op.status -eq "Failed") {
Write-Host "❌ App install/update failed."
Write-Host "Error: $($op.errorMessage)"
break # <-- breaks the WHILE
}
if ($op.status -eq "Canceled") {
Write-Host "⚠ Operation was canceled."
break # <-- breaks the WHILE
}
Start-Sleep -Seconds 15
}
After selecting the file, we construct the deployment parameters on Lines 22 – 26.
- deploymentSchedule: This is when the deployment takes place
- Immediate: right now.
- UpdateWindow: Based on the configured update window
- NextMinorUpdate: with the next minor update
- NextMajorUpdate: with the next major update
- syncMode: The sync mode applied during installation
- Add: Default proper way to deploy an extension
- ForceSync: When you change schema and must force the deployment.
- languageId: the Microsoft language code
- acceptIsvEula: true if you accept the Eula.
- Leaving the value out, or false will fail the deployment.
- installOrUpdateNeededDependencies: true/false if the install should update any dependencies associated with the APP file
From lines 29-72 we are just packaging everything up for deployment. The multipart packaging is verbose, but it follows the structure required by the endpoint. Note that this method has a 50 MB file size limit.
Lines 74-79 sends the request to Business Central. When running we can see this takes a moment to upload, so be patient. We get a results data packet back, and we pull the APP ID out of that packet.
Using that APP ID we can use another endpoint to query how the installation is going. Lines 83-111 gets the current operations and gives us a status on the deployment.
Here is a fail:
Operation 144cc8a3-f4fe-425c-90f0-a7f47c00314c: running
Operation 144cc8a3-f4fe-425c-90f0-a7f47c00314c: failed
X App install/update failed.
Error: Package validation failed due to the following error(s): Error AVS0109: The per-tenant extension (or one of its dependencies) cannot be deployed as it has missing dependencies or the dependencies are conflicting with currently installed apps. Check if these dependencies are installed.
Success!
Operation 92d60b56-8e0c-4949-91da-21c2ae490c1a: running
Operation 92d60b56-8e0c-4949-91da-21c2ae490c1a: running
Operation 92d60b56-8e0c-4949-91da-21c2ae490c1a: running
Operation 92d60b56-8e0c-4949-91da-21c2ae490c1a: running
Operation 92d60b56-8e0c-4949-91da-21c2ae490c1a: succeeded
✔ App install/update succeeded.
Uninstalling an APP Extension
To uninstall an extension, we first check whether any dependencies must be removed. If the uninstallRequirements endpoint returns an empty list, we can proceed. If there are dependencies, we will need to set the uninstallDependents to true in our settings.
$uninstallReqs = Invoke-RestMethod -Method Get `
-Uri "$url/applications/BusinessCentral/environments/$envName/apps/$AppId/uninstallRequirements" `
-Headers $headers
$uninstallReqs.requirements
There are no requirements, so I get an empty list.
requirements
------------
{}
We can now uninstall the APP. We need to set some parameters in the body. Lines 1-5 handle those details.
- useEnvironmentUpdateWindow: True/False use the configured update window for the uninstall.
- uninstallDependents: true/false if it should uninstall the dependents. If this is false, and there are dependents, it will return an error with a list of dependents.
- deleteData: true/false, should we delete the data on uninstall.
$body = @{
useEnvironmentUpdateWindow = $false
uninstallDependents = $false
deleteData = $true
} | ConvertTo-Json
$operation = Invoke-RestMethod -Method Post `
-Uri "$url/applications/BusinessCentral/environments/$envName/apps/$appId/uninstall" `
-Headers $headers `
-ContentType "application/json" `
-Body $body
Write-Host "Uninstall started. Operation ID: $($operation.id)"
# --- 3️⃣ Poll until uninstall completes ---
while ($true) {
$status = Invoke-RestMethod -Method Get `
-Uri "$url/applications/BusinessCentral/environments/$envName/apps/$appId/operations" `
-Headers $headers
if (-not $status.value) {
Write-Host "Waiting for operation to appear..."
Start-Sleep -Seconds 5
continue
}
$op = $status.value | Sort-Object createdOn -Descending | Select-Object -First 1
Write-Host "Operation $($op.id): $($op.status)"
if ($op.status -eq "Succeeded") {
Write-Host "✔ App uninstall succeeded."
break
}
if ($op.status -eq "Failed") {
Write-Host "❌ App uninstall failed."
Write-Host "Error: $($op.errorMessage)"
break
}
if ($op.status -eq "Canceled") {
Write-Host "⚠ Uninstall was canceled."
break
}
Start-Sleep -Seconds 5
}
Line 7 fires off the operation and triggers the uninstall. Lines 17-50 monitor the status and report on the results. It should look something like this:
Uninstall started. Operation ID: 57f846f7-676e-4ca7-a02a-ac099f03e603
Operation 57f846f7-676e-4ca7-a02a-ac099f03e603: scheduled
Operation 57f846f7-676e-4ca7-a02a-ac099f03e603: scheduled
Operation 57f846f7-676e-4ca7-a02a-ac099f03e603: scheduled
Operation 57f846f7-676e-4ca7-a02a-ac099f03e603: scheduled
Operation 57f846f7-676e-4ca7-a02a-ac099f03e603: scheduled
Operation 57f846f7-676e-4ca7-a02a-ac099f03e603: scheduled
Operation 57f846f7-676e-4ca7-a02a-ac099f03e603: running
Operation 57f846f7-676e-4ca7-a02a-ac099f03e603: succeeded
✔ App uninstall succeeded.
The App Management group contains several other useful endpoints for querying installed apps, retrieving dependency information, and managing updates. You can explore the full list in the official documentation: Business Central Admin Center API – App Management – Business Central | Microsoft Learn. These examples demonstrate the structure and workflow needed to interact with any of the extension management endpoints.
If you have other Administration API processes you’d like to explore, feel free to reach out in the comments. I’m always happy to dive deeper into Business Central automation.





Leave a comment