Sooner or later every Business Central project needs to talk to something else: a web shop, a POS, a warehouse app, a reporting service. If that something is written in C# / .NET, the cleanest route is the Business Central REST API with OAuth 2.0 service-to-service authentication. Basic authentication with web service access keys is no longer supported in Business Central online.
This guide walks through the whole setup: registering the app, granting access inside Business Central, getting a token with MSAL and building a typed, resilient HttpClient in ASP.NET Core.
How the pieces fit together
- Your .NET service authenticates as an application (no user) against Microsoft Entra ID using the client credentials flow.
- Entra ID issues an access token for the Business Central API.
- Your service calls
https://api.businesscentral.dynamics.com/v2.0/{tenant}/{environment}/api/v2.0/…with that token. - Business Central checks which permission sets the application has been given and runs the request with those permissions.
Step 1: Register an app in Microsoft Entra ID
- In the Azure portal, open Microsoft Entra ID → App registrations → New registration. Give it a clear name such as bc-integration-service.
- Under Certificates & secrets, create a client secret (or, better for production, upload a certificate).
- Under API permissions, add Dynamics 365 Business Central → Application permissions →
API.ReadWrite.All, then select Grant admin consent. - Note the Application (client) ID and Directory (tenant) ID.
Step 2: Allow the app inside Business Central
An Entra app can't do anything in Business Central until it's registered there too:
- In Business Central, search for Microsoft Entra Applications and create a new card.
- Enter the client ID and a description, and set the state to Enabled.
- Assign only the permission sets the integration actually needs. Avoid
SUPER. - Choose Grant Consent and sign in as an administrator.
Step 3: Get a token with MSAL
Install Microsoft.Identity.Client (MSAL) and Microsoft.Extensions.Http.Resilience from NuGet. Keep the settings in configuration. Use user secrets locally and Azure Key Vault in production:
{
"BusinessCentral": {
"TenantId": "00000000-0000-0000-0000-000000000000",
"Environment": "Production",
"ClientId": "00000000-0000-0000-0000-000000000000",
"ClientSecret": "stored-in-user-secrets-or-key-vault"
}
}
A small DelegatingHandler attaches the token to every request. MSAL caches app tokens in memory and renews them before they expire, so calling it on each request is cheap:
using System.Net.Http.Headers;
using Microsoft.Identity.Client;
public sealed class BcOptions
{
public string TenantId { get; set; } = "";
public string Environment { get; set; } = "Production";
public string ClientId { get; set; } = "";
public string ClientSecret { get; set; } = "";
}
public sealed class BcAuthHandler(IConfidentialClientApplication msal) : DelegatingHandler
{
private static readonly string[] Scopes = ["https://api.businesscentral.dynamics.com/.default"];
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
var token = await msal.AcquireTokenForClient(Scopes).ExecuteAsync(ct);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
return await base.SendAsync(request, ct);
}
}
Step 4: Register a typed HttpClient
Wire everything up in Program.cs. The standard resilience handler adds retries with back-off, timeouts and a circuit breaker:
using Microsoft.Extensions.Options;
using Microsoft.Identity.Client;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<BcOptions>(builder.Configuration.GetSection("BusinessCentral"));
builder.Services.AddSingleton<IConfidentialClientApplication>(sp =>
{
var o = sp.GetRequiredService<IOptions<BcOptions>>().Value;
return ConfidentialClientApplicationBuilder
.Create(o.ClientId)
.WithClientSecret(o.ClientSecret)
.WithAuthority($"https://login.microsoftonline.com/{o.TenantId}")
.Build();
});
builder.Services.AddTransient<BcAuthHandler>();
builder.Services.AddHttpClient<BusinessCentralClient>((sp, http) =>
{
var o = sp.GetRequiredService<IOptions<BcOptions>>().Value;
http.BaseAddress = new Uri(
$"https://api.businesscentral.dynamics.com/v2.0/{o.TenantId}/{o.Environment}/api/v2.0/");
})
.AddHttpMessageHandler<BcAuthHandler>()
.AddStandardResilienceHandler();
var app = builder.Build();
Step 5: Query data and follow paging
Business Central APIs follow OData v4 conventions: use $select to fetch only the fields you need, $filter to narrow results, and follow @odata.nextLink when a result spans several pages. An IAsyncEnumerable keeps that transparent to callers:
using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
public sealed record ODataPage<T>(
[property: JsonPropertyName("value")] List<T> Value,
[property: JsonPropertyName("@odata.nextLink")] string? NextLink);
public sealed record Company(Guid Id, string Name, string DisplayName);
public sealed record Customer(Guid Id, string Number, string DisplayName, string? Email, string? PhoneNumber);
public sealed class BusinessCentralClient(HttpClient http)
{
public async Task<IReadOnlyList<Company>> GetCompaniesAsync(CancellationToken ct = default)
{
var page = await http.GetFromJsonAsync<ODataPage<Company>>("companies", ct);
return page?.Value ?? [];
}
public async IAsyncEnumerable<Customer> GetCustomersAsync(
Guid companyId,
string? filter = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
string? url = $"companies({companyId})/customers?$select=id,number,displayName,email,phoneNumber";
if (!string.IsNullOrWhiteSpace(filter))
url += "&$filter=" + Uri.EscapeDataString(filter);
while (url is not null)
{
var page = await http.GetFromJsonAsync<ODataPage<Customer>>(url, ct)
?? throw new InvalidOperationException("Empty response from Business Central.");
foreach (var customer in page.Value)
yield return customer;
url = page.NextLink; // absolute URL – HttpClient accepts it as-is
}
}
}
A common pattern for incremental sync is to filter on lastModifiedDateTime and store the timestamp of your last successful run:
app.MapGet("/customers/changed", async (BusinessCentralClient bc, CancellationToken ct) =>
{
var company = (await bc.GetCompaniesAsync(ct))[0];
var since = DateTime.UtcNow.AddDays(-1).ToString("yyyy-MM-ddTHH:mm:ssZ");
var changed = new List<Customer>();
await foreach (var c in bc.GetCustomersAsync(company.Id, $"lastModifiedDateTime gt {since}", ct))
changed.Add(c);
return changed;
});
app.Run();
Step 6: Expose your own data with a custom API page
Standard APIs cover the common entities. For custom tables and fields, publish your own API page in AL:
page 50120 "DS Loyalty Customer API"
{
PageType = API;
APIPublisher = 'diwas';
APIGroup = 'loyalty';
APIVersion = 'v1.0';
EntityName = 'loyaltyCustomer';
EntitySetName = 'loyaltyCustomers';
SourceTable = Customer;
ODataKeyFields = SystemId;
DelayedInsert = true;
layout
{
area(Content)
{
repeater(Records)
{
field(id; Rec.SystemId) { Editable = false; }
field(number; Rec."No.") { }
field(displayName; Rec.Name) { }
field(loyaltyTier; Rec."DS Loyalty Tier") { }
}
}
}
}
It's then available at …/api/diwas/loyalty/v1.0/companies({id})/loyaltyCustomers, and the same typed-client pattern works unchanged with a different base path.
Production checklist
- Respect rate limits. Business Central online throttles API traffic and returns
HTTP 429. Let the resilience handler back off and honourRetry-After. - Batch writes. Use OData
$batchto send many small requests in one round trip. - Use webhooks instead of polling where possible. Business Central can notify your endpoint when entities change.
- Least privilege. Give the Entra application only the permission sets it needs, and rotate secrets, or better, use certificates or managed identities.
- Log correlation IDs and Business Central's error messages so failed syncs are easy to trace.
Wrapping up
With an Entra app registration, a small auth handler and a typed client, Business Central becomes just another well-behaved API in your .NET solution. You can then build sync workers, Azure Functions, portals or POS bridges on top of it.
Need help integrating Business Central with your .NET application? Let's talk.