Back to Blog

    OData and Business Central API: The Complete Integration Guide for Intranets & Web Apps (2026)

    June 8, 202613 min readAkila Dhanapala65 views
    OData and Business Central API: The Complete Integration Guide for Intranets & Web Apps (2026)

    Every business that runs Microsoft Dynamics 365 Business Central eventually faces the same question: how do we get our ERP data out - and into every other system that needs it? Whether that is your company intranet, a supplier portal, a Power BI dashboard, a mobile warehouse app, or a custom internal web application, the answer starts with the same two technologies: OData and the Business Central API.

    This guide is written for developers, IT managers, and solution architects who need to connect Business Central to something else. We cover the complete picture - from understanding the protocols, to writing your first authenticated API call, to designing integration patterns that hold up in production.

    What is OData and Why Does Business Central Use It?

    OData (Open Data Protocol) is an OASIS-ratified open standard that defines a universal grammar for building and consuming RESTful APIs. Instead of every API inventing its own way to express filtering, sorting, or field selection, OData defines it once and every compliant client and server understands it natively.

    Business Central implements OData v4, the current generation. When BC exposes a page or query as a web service, the following query capabilities become available automatically without any server-side coding:

    • $filter - server-side filtering with rich comparison and logical operators
    • $select - field projection, return only the fields your app needs
    • $orderby - sort results by one or more fields
    • $top and $skip - paginate large result sets
    • $expand - embed related entity data inline in a single request
    • $count - return the total number of matching records

    Because OData is a published standard, dozens of client libraries already support it: Power BI, Excel Power Query, .NET, JavaScript, Python, Java, and many more. You rarely need to write low-level HTTP plumbing - pick a library, point it at the endpoint, and start querying.

    Two Interfaces: REST API vs OData Web Services

    Business Central exposes data through two related but distinct interfaces. Understanding the difference is the single most important decision every integrator makes before writing a line of code.

    Dimension BC REST API (v2.0) OData Web Services
    URL prefix /api/v2.0/ /ODataV4/
    Schema Fixed, Microsoft-defined Dynamic - mirrors the published page or query
    Versioning Strict (v1.0, v2.0, custom API) None - schema reflects the live page
    Custom tables Via custom API pages in AL Publish any page or query directly
    Upgrade stability High - versioned contracts Lower - page changes can affect consumers
    Best for Standard entities, stable contracts, marketplace apps Custom data, internal tools, Power BI, intranets

    Rule of thumb: Use the REST API when Microsoft already defines the entity (customers, items, sales orders, purchase invoices). Use OData web services when you need custom tables, extended fields, or joined queries that do not exist in the standard API.

    Authentication: OAuth 2.0 with Microsoft Entra ID

    Basic authentication for Business Central SaaS has been deprecated. All production integrations must use OAuth 2.0 via Microsoft Entra ID (formerly Azure Active Directory). There are two flows you need to know:

    Client Credentials Flow (Service-to-Service)

    Used for background jobs, scheduled syncs, and machine-to-machine integrations. There is no human in the loop. The application authenticates with a client ID and secret and acts as itself. This is the workhorse of intranet and web app integrations.

    Authorization Code Flow (Delegated)

    Used when the integration acts on behalf of a signed-in user - for example, an intranet page that shows the current user's pending BC approvals.

    Registering Your App in Entra ID

    1. In the Azure portal go to Microsoft Entra ID → App registrations → New registration.
    2. Give the app a meaningful name, for example Intranet-BC-Connector.
    3. Under Certificates & secrets create a client secret and copy it immediately - it will not be shown again.
    4. Under API permissions add Dynamics 365 Business Central with the API.ReadWrite.All application permission and click Grant admin consent.
    5. In Business Central open the Microsoft Entra Applications page, create a new record with the same Application (Client) ID, and assign an appropriate permission set.

    Acquiring a Token in Node.js

    const { ConfidentialClientApplication } = require("@azure/msal-node");
    
    const cca = new ConfidentialClientApplication({
      auth: {
        clientId:     process.env.BC_CLIENT_ID,
        clientSecret: process.env.BC_CLIENT_SECRET,
        authority:    `https://login.microsoftonline.com/${process.env.BC_TENANT_ID}`
      }
    });
    
    async function getBCToken() {
      const result = await cca.acquireTokenByClientCredential({
        scopes: ["https://api.businesscentral.dynamics.com/.default"]
      });
      return result.accessToken; // Valid ~60 minutes - cache this
    }

    That token goes into the Authorization: Bearer <token> header of every API request. Cache tokens aggressively and refresh on receiving a 401 response.

    Security note: Never embed client secrets in front-end JavaScript or commit them to source control. Token acquisition must always happen on a trusted backend server. Store secrets in Azure Key Vault or your platform's secrets manager.

    Endpoint Anatomy

    Business Central API URLs follow a consistent structure. Once you understand each segment you can build any URL from memory.

    Standard REST API

    https://api.businesscentral.dynamics.com/v2.0/{tenantId}/{environment}/api/v2.0/companies({companyId})/customers
    • {tenantId} - your Microsoft Entra Directory (tenant) ID, a GUID
    • {environment} - typically Production or Sandbox
    • api/v2.0 - the standard API version
    • companies({companyId}) - BC supports multiple companies per tenant; scope to one by its GUID
    • customers - the entity set (also: items, salesInvoices, vendors, purchaseOrders, etc.)

    OData Web Service

    https://api.businesscentral.dynamics.com/v2.0/{tenantId}/{environment}/ODataV4/Company('CRONUS')/CustomerCard

    Notice: ODataV4 replaces api/v2.0, the company is referenced by its display name (URL-encoded), and CustomerCard is the service name you published in BC - not a Microsoft-defined entity name.

    Discovering Available Endpoints

    GET /ODataV4/$metadata
    Authorization: Bearer eyJ0eXAiOiJKV1Qi...

    This returns the full OData metadata document - an XML description of every entity, property, and relationship available on the tenant. Invaluable when exploring an unfamiliar environment or generating client code.

    OData Query Options in Practice

    These five query options cover ninety percent of real-world integration scenarios:

    $filter - Server-side filtering

    GET /customers?$filter=country eq 'GB' and balance gt 5000

    Supports comparison operators (eq, ne, gt, ge, lt, le), logical operators (and, or, not), and string functions (contains, startswith, tolower).

    $select - Field projection

    GET /customers?$select=number,displayName,email,balance,country

    This is the single biggest performance win available. Most BC entities expose 50+ fields; your UI probably needs 5–10. Always use $select in production integrations.

    $orderby - Sorting

    GET /salesInvoices?$orderby=invoiceDate desc,totalAmountIncludingTax desc

    $top and $skip - Pagination

    GET /items?$top=50&$skip=100

    BC also returns an @odata.nextLink property in large responses. Always follow that link for the next page rather than incrementing $skip manually - it accounts for server-side cursor management correctly.

    $expand - Embed related entities

    GET /salesInvoices?$expand=salesInvoiceLines&$top=10

    Returns parent invoices with all child lines embedded in a single request. Use sparingly - expanding deep hierarchies produces large payloads.

    Writing Data: POST, PATCH, DELETE

    Creating and updating records works as expected with one BC-specific requirement: updates via PATCH must include the If-Match header containing the record's current @odata.etag to prevent lost-update conflicts.

    PATCH /customers(7e3d4e00-1a2b-3c4d-5e6f-7a8b9c0d1e2f)
    Authorization: Bearer ...
    If-Match: W/"JzQ0O0xMSkdTZ0E5..."
    Content-Type: application/json
    
    {
      "displayName": "Acme Corporation Ltd.",
      "email": "accounts@acme.example"
    }

    Publishing Custom OData Web Services from Business Central

    The real power of OData surfaces when you publish your own endpoints. Any page or query in BC can be exposed as a web service in seconds - with no additional server infrastructure required.

    Step 1 - Create a Page in AL

    In Visual Studio Code with the AL Language extension, create a list page targeting your table:

    page 50100 "Purchase Requisition API"
    {
        PageType    = List;
        SourceTable = "Purchase Requisition";
        DelayedInsert = true;
        Editable    = true;
    
        layout
        {
            area(Content)
            {
                repeater(Group)
                {
                    field(requisitionId; Rec."Requisition ID") { Caption = 'requisitionId'; }
                    field(requestedBy;   Rec."Requested By")   { Caption = 'requestedBy'; }
                    field(itemNo;        Rec."Item No.")        { Caption = 'itemNo'; }
                    field(quantity;      Rec.Quantity)          { Caption = 'quantity'; }
                    field(status;        Rec.Status)            { Caption = 'status'; }
                    field(dueDate;       Rec."Due Date")        { Caption = 'dueDate'; }
                }
            }
        }
    }

    Step 2 - Publish via the Web Services Page

    In Business Central:

    1. Search for Web Services and open the list.
    2. Click New and set Object Type to Page.
    3. Enter your page ID (50100) and a service name (purchaseRequisitions).
    4. Check the Published toggle - the endpoint is immediately live.

    Your custom endpoint is now available at:

    GET /ODataV4/Company('Contoso')/purchaseRequisitions
        ?$filter=status eq 'Open'
        &$select=requisitionId,requestedBy,itemNo,quantity,dueDate
        &$orderby=dueDate asc

    Step 3 - Consume It from Your Web App

    const token = await getBCToken();
    
    const url = new URL(
      `https://api.businesscentral.dynamics.com/v2.0/${TENANT_ID}/Production` +
      `/ODataV4/Company('Contoso')/purchaseRequisitions`
    );
    url.searchParams.set("$filter", "status eq 'Open'");
    url.searchParams.set("$select", "requisitionId,requestedBy,itemNo,quantity,dueDate");
    url.searchParams.set("$orderby", "dueDate asc");
    url.searchParams.set("$top", "100");
    
    const response = await fetch(url.toString(), {
      headers: { "Authorization": `Bearer ${token}` }
    });
    
    const { value: requisitions } = await response.json();
    renderDashboard(requisitions);

    Real-World Integration Patterns

    Pattern 1: Read-Through Intranet Dashboard

    Your intranet homepage displays live BC data - open purchase orders, inventory alerts, top customers, unpaid invoices. The implementation pattern:

    • A lightweight backend (Azure Functions, Node.js API, ASP.NET minimal API) acquires an OAuth token on a server-to-server basis.
    • It calls BC with aggressive $select and $filter to fetch only what is needed.
    • Responses are cached in Redis or in-memory for 1–5 minutes depending on how frequently the data changes.
    • The intranet front end calls your backend API - never BC directly.

    Users never authenticate against BC, credentials never touch the browser, and the experience is fast because of caching.

    Pattern 2: Write-Back Employee Portal

    Employees submit expense claims, purchase requisitions, or time entries through a polished web form. The integration flow:

    1. User submits the form to your backend.
    2. Backend validates all fields (types, ranges, foreign key lookups against BC reference data).
    3. Backend transforms the payload into BC's expected schema and POSTs to the API.
    4. BC returns the newly created entity with its system-assigned ID.
    5. Backend returns the BC entity ID to the user as a reference number for tracking.

    Pattern 3: Event-Driven Bidirectional Sync

    For near-real-time sync with a CRM, e-commerce platform, or HR system, use BC's built-in webhook subscriptions rather than polling.

    // Register a webhook subscription via the BC API
    POST /api/v2.0/subscriptions
    Content-Type: application/json
    Authorization: Bearer ...
    
    {
      "notificationUrl": "https://yourapp.example.com/webhooks/bc",
      "resource": "/api/v2.0/companies({companyId})/customers",
      "clientState": "your-secret-validation-token"
    }

    BC sends a POST to your notificationUrl whenever a customer is created or modified. Subscriptions expire after a few days and must be renewed - build renewal into your integration from day one.

    Pattern 4: Bulk Analytics Extract

    For BI tools and data warehouse feeds, schedule a nightly job that pulls only records modified since the last run:

    GET /api/v2.0/companies({id})/customers
      ?$filter=lastModifiedDateTime gt 2026-05-24T00:00:00Z
      &$select=number,displayName,balance,country,lastModifiedDateTime
      &$top=1000

    Follow @odata.nextLink for subsequent pages until no link is returned. For very high volumes (hundreds of thousands of records), consider Microsoft Fabric Link for Dynamics 365 - it replicates BC tables into OneLake without any API call overhead.

    Handling API Responses and Pagination

    All BC API responses return JSON. A successful collection response looks like this:

    {
      "@odata.context": "https://api.businesscentral.dynamics.com/v2.0/.../customers",
      "@odata.count": 1247,
      "value": [
        {
          "id": "7e3d4e00-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
          "number": "C00010",
          "displayName": "Acme Corporation",
          "balance": 12450.00,
          "@odata.etag": "W/\"JzQ0O0xMSkdTZ0E5...\""
        }
        // ... more records
      ],
      "@odata.nextLink": "https://api.businesscentral.dynamics.com/v2.0/.../customers?$skiptoken=..."
    }

    Always store and follow @odata.nextLink when present. Also store the @odata.etag for any record you intend to update - you will need it for the If-Match header on PATCH requests.

    Performance and Throttling

    Business Central enforces API rate limits to protect shared tenant infrastructure. When you exceed them BC returns HTTP 429 Too Many Requests with a Retry-After header specifying how many seconds to wait.

    Production performance checklist:

    • Always use $select - most BC entities expose 50+ fields; requesting all of them multiplies payload size and processing time unnecessarily.
    • Filter server-side - use $filter to push filtering into BC rather than downloading all records and filtering in your application.
    • Cache reference data aggressively - items, customers, chart of accounts, and unit-of-measure codes change infrequently. Cache them for 15–60 minutes.
    • Implement exponential backoff with jitter on 429 responses - never tight-loop retry.
    • Use the $batch endpoint for bulk write operations to reduce round trips and token overhead.
    • Run bulk extracts off-peak - schedule large data pulls outside business hours to avoid competing with interactive users.
    • Check Rec.IsTemporary in AL event subscribers - skip business logic for temporary scratch records.

    Security and Governance

    An ERP integration is a high-value target. Apply these controls without exception:

    • Principle of least privilege - create a custom BC permission set exposing only the specific tables and operations your integration requires. Do not assign SUPER.
    • Rotate secrets on a schedule - client secrets should expire and be rotated. Prefer certificate credentials over secrets wherever your platform supports them.
    • Never expose BC credentials in front-end code - all OAuth token acquisition must happen on a trusted server-side component.
    • Log every API call - at minimum record the timestamp, calling application, endpoint path, HTTP method, and response status. Enable BC's Change Log for data-level audit trails.
    • Validate all inbound data rigorously - before pushing form input to BC, validate types, lengths, enumeration values, and foreign key existence.
    • Apply Conditional Access policies - in Entra ID restrict the integration app's sign-ins to known IP ranges or require managed device compliance.
    • Handle PII responsibly - customer and employee data is regulated under GDPR and similar regimes. Do not cache personal data longer than necessary; propagate deletion requests.

    Troubleshooting Common Issues

    401 Unauthorized - Either the token has expired (refresh on 401) or the Entra application is not registered inside BC with permission sets assigned. Both registrations are required - the Entra app registration alone is not sufficient.

    403 Forbidden on a specific entity - The permission set assigned to the integration app in BC does not include access to that table or operation. Adjust the permission set - not the Entra configuration.

    404 Not Found on a known entity - Almost always a company-scope problem. Verify that the company ID or name in your URL exactly matches a company that exists in the target environment.

    422 Unprocessable Entity - BC validation rejected your payload. The response body always contains a human-readable error message - log it, read it, and fix the payload. Common causes: missing required fields, a foreign key that does not exist, or a violated business rule.

    429 Too Many Requests - You are being throttled. Read the Retry-After header value in seconds and pause before retrying. Implement exponential backoff with jitter for sustained high-volume workloads.

    412 Precondition Failed on PATCH - Your If-Match ETag is stale - another process updated the record between your GET and PATCH. Re-fetch the record to get the current ETag, then retry your update with the fresh value.

    Getting Started: Your First Integration in 15 Minutes

    Follow these five steps to go from zero to a working authenticated API call:

    1. Register your app in Microsoft Entra ID and note the Application (Client) ID, Client Secret, and Tenant ID.
    2. Grant API permissions - add Dynamics 365 Business Central → API.ReadWrite.All and click Grant admin consent.
    3. Register inside BC - open the Microsoft Entra Applications page, add a record with your Client ID, and assign a permission set.
    4. Acquire a token using the client credentials flow (see code example in the Authentication section above).
    5. Verify authentication by calling GET /api/v2.0/companies - a 200 response with your company list confirms everything is wired correctly.

    From that foundation you can add endpoints incrementally - one entity, one integration pattern at a time.

    Closing: Build the Bridge, Then Iterate

    Business Central's combination of OData v4, the versioned REST API, and the ability to publish any custom page as a live endpoint makes it one of the most integration-friendly ERP platforms available today. You do not need expensive middleware, complex ETL pipelines, or a dedicated integration team to connect BC to your intranet or web applications.

    What you do need is a clear understanding of which interface fits each use case, a properly configured OAuth application, and a backend service that handles tokens, caching, and error recovery. The patterns in this guide cover every real-world scenario from a simple read-through dashboard to a bidirectional event-driven sync.

    Start with the simplest pattern that solves your immediate problem. Ship it, monitor it, and expand from there. The API will be there every step of the way.