Integrating with Business Central
Connect Action Flows to Microsoft Dynamics 365 Business Central through an OpenAPI app that calls the Business Central API v2.0. With this setup, Agents can search vendors, create purchase invoices with lines, and call any other Business Central API endpoint. The running example in this guide is an accounts payable flow: an emailed PDF invoice is OCR'd, an Agent matches the vendor, and the invoice is created in Business Central — lines included.
What You Will Build
- A Microsoft Entra app registration with application permissions for Business Central, so Studio can call the API without a signed-in user (service-to-service authentication).
- The Business Central side of the grant — an enabled Microsoft Entra application card with permission sets assigned.
- An OpenAPI app in Studio with Service Principal credentials pointing at that registration.
- Tools that list companies, search vendors, create purchase invoices, and add invoice lines.
- An Action Flow that turns an emailed PDF invoice into a draft purchase invoice in Business Central.
Every tool definition below is ready-to-import JSON: open the app's Tools tab, select Import JSON, and paste the snippet — Studio creates the tool with its name, description, configuration, and parameter schema in one go. Importing a name that already exists prompts you to overwrite or rename.
Step 1 — Register a Microsoft Entra Application
Studio authenticates to Business Central with app-only (client credentials) authentication, which Business Central calls service-to-service (S2S) authentication. Create the app registration in the Microsoft Entra admin center of the Microsoft 365 tenant that hosts Business Central:
- Go to Microsoft Entra ID > App registrations > New registration.
- Enter a name, for example
Dooap Studio - Business Central. No redirect URI is needed. - Register the application and note the Application (client) ID and Directory (tenant) ID from the Overview page.
- Go to Certificates & secrets > New client secret, create a secret, and copy the secret value immediately — it is shown only once.
- Under API permissions > Add a permission > Microsoft APIs, select Dynamics 365 Business Central, choose Application permissions, and add
API.ReadWrite.All(access to APIs and web services).Automation.ReadWrite.Allis only needed for Business Central's automation APIs (company setup), not for this integration. - Select Grant admin consent for the tenant. A user with sufficient Entra privileges (for example Cloud Application Administrator) must perform this step.
Step 2 — Authorize the Application in Business Central
Consent alone does not let the application read or write data — Business Central must also know the application and what it is allowed to touch. Complete this inside the Business Central web client of each environment the integration targets:
- Search for the Microsoft Entra Applications page and open it.
- Select New. The Microsoft Entra application card opens.
- In Client ID, paste the Application (client) ID from Step 1, and fill in a description.
- Set State to Enabled.
- Assign permission sets covering the objects your tools use. Business Central refuses to assign
SUPERto applications — follow least privilege. For the tools in this guide the application needs access to vendors and purchase documents; a broad business permission set such as D365 BUS FULL ACCESS works for evaluation, and narrower purchase-document and vendor sets are the right choice for production.
Step 3 — Create the OpenAPI App in Studio
- Go to Tenant Admin > Apps.
- Select New, choose OpenAPI, and continue.
- Enter a clear name, such as
Business Central, and save the app. - Open the app and select Configure Credentials.
- Set Authentication Type to Service Principal and enter:
- Client ID — the Application (client) ID from Step 1.
- Client Secret — the secret value from Step 1.
- Azure Tenant ID — the Directory (tenant) ID from Step 1.
- Scope —
https://api.businesscentral.dynamics.com/.default. The dialog labels the field optional, but the token must be requested for the Business Central resource, so always set it for this integration. Studio appends/.defaultautomatically if you enter onlyhttps://api.businesscentral.dynamics.com.
- Use Test Connection to verify token acquisition, then save.
Studio now acquires and caches Business Central tokens for this app. In the definitions below, the token is injected with the {{APP_BEARER_TOKEN}} placeholder and masked in run logs.
Step 4 — Find the Environment Name and Company ID
Every Business Central API v2.0 request addresses one environment and one company:
https://api.businesscentral.dynamics.com/v2.0/{environmentName}/api/v2.0/companies({companyId})/...
- Environment name — shown in the Business Central admin center, for example
productionorsandbox. - Company ID — a GUID, discoverable through the API itself.
Add a small helper tool so you can resolve company IDs from inside Studio. On the Tools tab, select Import JSON and paste:
{
"name": "List_companies",
"description": "Lists the companies in the Business Central environment. Returns each company's id, name, and display name. The id is needed by the other Business Central tools.",
"configTemplate": {
"BaseUrl": "https://api.businesscentral.dynamics.com",
"Method": "GET",
"Path": "/v2.0/sandbox/api/v2.0/companies",
"Headers": {
"Authorization": "Bearer {{APP_BEARER_TOKEN}}"
}
},
"toolCallSchema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"type": "OpenAPI",
"appName": null,
"tags": "business central, lookup",
"responseSamples": null
}
Replace sandbox in the path with your environment name before running. Run the tool once from a Tool Call Step (or ask an Agent step to call it) and copy the id of the target company from the response.
The remaining tool definitions bake the environment name and company ID into the path for simplicity. In every snippet below, replace:
sandbox— with your environment name.00000000-0000-0000-0000-000000000000— with your company ID.
Use a separate Studio app per environment (for example Business Central - Sandbox and Business Central - Production) so an exported Action can be re-pointed by swapping app credentials rather than editing tool paths. If one environment hosts several companies, you can instead define company_id as a tool parameter with a value in DefaultParameterValues, the same way the parameters below work.
Step 5 — Search Vendors
Vendor matching is the first step of most AP flows: the invoice states a free-form supplier name, and the Agent must resolve it to a Business Central vendor number. On the Tools tab, select Import JSON and paste:
{
"name": "Search_vendors",
"description": "Searches Business Central vendors by partial display name. Returns matching vendors with their number, display name, and id.",
"configTemplate": {
"BaseUrl": "https://api.businesscentral.dynamics.com",
"Method": "GET",
"Path": "/v2.0/sandbox/api/v2.0/companies(00000000-0000-0000-0000-000000000000)/vendors",
"Query": {
"$filter": "contains(displayName,'{vendor_name_search_term}')",
"$select": "id,number,displayName,taxRegistrationNumber,currencyCode,blocked"
},
"Headers": {
"Authorization": "Bearer {{APP_BEARER_TOKEN}}"
}
},
"toolCallSchema": {
"type": "object",
"properties": {
"vendor_name_search_term": {
"type": "string",
"description": "Search term matched against part of the vendor display name"
}
},
"required": ["vendor_name_search_term"],
"additionalProperties": false
},
"type": "OpenAPI",
"appName": null,
"tags": "vendors",
"responseSamples": null
}
Two things to note in this definition:
- The
{vendor_name_search_term}placeholder sits inside aQuerytemplate value. Studio substitutes tool parameters into query values directly, so nox-parameter-locationsentry is needed for it. - The
$filteruses the ODatacontainsfunction, so an Agent can search with any fragment of the name.$selectkeeps the response small — vendor records carry many fields the Agent does not need.
Step 6 — Create Purchase Invoices
Creating an invoice in Business Central is two operations: create the invoice header, then add lines to it. Define both as tools. On the Tools tab, select Import JSON and paste:
{
"name": "Create_purchase_invoice",
"description": "Creates a new draft purchase invoice in Business Central. Returns the created invoice including its id, which is needed for adding invoice lines.",
"configTemplate": {
"BaseUrl": "https://api.businesscentral.dynamics.com",
"Method": "POST",
"Path": "/v2.0/sandbox/api/v2.0/companies(00000000-0000-0000-0000-000000000000)/purchaseInvoices",
"Headers": {
"Authorization": "Bearer {{APP_BEARER_TOKEN}}",
"Content-Type": "application/json"
}
},
"toolCallSchema": {
"type": "object",
"properties": {
"vendorNumber": { "type": "string", "description": "The Business Central vendor number, e.g. '20000'" },
"vendorInvoiceNumber": { "type": "string", "description": "The vendor's own invoice number as printed on the invoice" },
"invoiceDate": { "type": "string", "description": "Invoice date in YYYY-MM-DD format" },
"postingDate": { "type": "string", "description": "Posting date in YYYY-MM-DD format" },
"dueDate": { "type": "string", "description": "Due date in YYYY-MM-DD format" },
"currencyCode": { "type": ["string", "null"], "description": "ISO currency code, e.g. 'EUR'. Leave empty for the local currency." }
},
"required": [
"vendorNumber",
"vendorInvoiceNumber",
"invoiceDate",
"postingDate",
"dueDate",
"currencyCode"
],
"additionalProperties": false,
"x-parameter-locations": {
"vendorNumber": "body",
"vendorInvoiceNumber": "body",
"invoiceDate": "body",
"postingDate": "body",
"dueDate": "body",
"currencyCode": "body"
}
},
"type": "OpenAPI",
"appName": null,
"tags": "purchase invoice",
"responseSamples": "{\n \"id\": \"00000000-0000-0000-0000-000000000000\",\n \"number\": \"107001\",\n \"vendorNumber\": \"20000\",\n \"vendorInvoiceNumber\": \"INV-4711\",\n \"totalAmountIncludingTax\": 0,\n \"status\": \"Draft\"\n}"
}
The purchaseInvoices entity accepts many more optional body fields — buy-from and ship-to addresses, discount amounts, and so on. Add the ones your process needs as extra properties with "type": ["string", "null"] (all properties must appear in required for the Agent's schema; nullable types mark them optional in practice).
Then the line tool:
{
"name": "Add_purchase_invoice_line",
"description": "Adds a line to an existing draft purchase invoice in Business Central. Use sequence numbers 10000, 20000, 30000, ... for consecutive lines.",
"configTemplate": {
"BaseUrl": "https://api.businesscentral.dynamics.com",
"Method": "POST",
"Path": "/v2.0/sandbox/api/v2.0/companies(00000000-0000-0000-0000-000000000000)/purchaseInvoices({document_id})/purchaseInvoiceLines",
"Headers": {
"Authorization": "Bearer {{APP_BEARER_TOKEN}}",
"Content-Type": "application/json"
}
},
"toolCallSchema": {
"type": "object",
"properties": {
"document_id": { "type": "string", "description": "The GUID of the purchase invoice to add the line to (the 'id' returned by Create_purchase_invoice)" },
"sequence": { "type": "integer", "description": "Line sequence number: 10000 for the first line, 20000 for the second, and so on" },
"lineType": {
"type": "string",
"description": "Type of line",
"enum": ["Item", "Comment", "Resource", "Fixed Asset", "Charge"]
},
"lineObjectNumber": { "type": ["string", "null"], "description": "Item number or resource code depending on lineType. Leave empty for Comment lines." },
"description": { "type": "string", "description": "Description of the line" },
"quantity": { "type": ["number", "null"], "description": "Quantity. Not used on Comment lines." },
"unitCost": { "type": ["number", "null"], "description": "Unit cost. Not used on Comment lines." }
},
"required": [
"document_id",
"sequence",
"lineType",
"lineObjectNumber",
"description",
"quantity",
"unitCost"
],
"additionalProperties": false,
"x-parameter-locations": {
"document_id": "path",
"sequence": "body",
"lineType": "body",
"lineObjectNumber": "body",
"description": "body",
"quantity": "body",
"unitCost": "body"
}
},
"type": "OpenAPI",
"appName": null,
"tags": "purchase invoice",
"responseSamples": null
}
Finally a read-back tool, useful when a later step needs to inspect or extend an invoice — for example to find the next free sequence number before appending a comment line:
{
"name": "List_purchase_invoice_lines",
"description": "Lists the existing lines of a purchase invoice in Business Central, so the next sequence number can be determined before adding a new line or comment.",
"configTemplate": {
"BaseUrl": "https://api.businesscentral.dynamics.com",
"Method": "GET",
"Path": "/v2.0/sandbox/api/v2.0/companies(00000000-0000-0000-0000-000000000000)/purchaseInvoices({document_id})/purchaseInvoiceLines",
"Headers": {
"Authorization": "Bearer {{APP_BEARER_TOKEN}}"
}
},
"toolCallSchema": {
"type": "object",
"properties": {
"document_id": { "type": "string", "description": "The GUID of the purchase invoice whose lines should be listed" }
},
"required": ["document_id"],
"additionalProperties": false,
"x-parameter-locations": {
"document_id": "path"
}
},
"type": "OpenAPI",
"appName": null,
"tags": "purchase invoice",
"responseSamples": "{\n \"value\": [\n { \"id\": \"11111111-1111-1111-1111-111111111111\", \"sequence\": 10000, \"lineType\": \"Item\", \"description\": \"Office chair\", \"quantity\": 2, \"unitCost\": 120 },\n { \"id\": \"22222222-2222-2222-2222-222222222222\", \"sequence\": 20000, \"lineType\": \"Comment\", \"description\": \"Approved by AP team\" }\n ]\n}"
}
The responseSamples field is worth filling in on read tools: the Agent sees the sample and understands the response shape before its first call. When a tool is assigned to a step you can also set an Output Filter such as $.data.value.[*].{sequence,lineType} to strip the response down to the fields the step actually needs, saving tokens.
Building the Invoice Capture Flow
With the app and tools in place, the AP flow is two Actions chained with a Trigger Action step. Splitting it keeps the email handling reusable and the Business Central logic independently testable.
Action 1 — Receive the Email and Run OCR
- Trigger: Email Received on a Gateway Mailbox. Suppliers send invoices to a Studio-owned address; no Exchange app or credentials are needed.
- OCR Step: add the Read_Gateway_Email_Attachment input with
attachmentNameset to*.pdf, so the attached invoice is downloaded into the run before the step executes. Enable Store output for triggered Actions on the step so the OCR text is available downstream (see Multi-Action Flow with OCR). - Trigger Action Step: invoke the second Action. No payload mapping is required for the OCR text itself — the stored output is resolved through the run's trace ID.
Action 2 — Match the Vendor and Create the Invoice
This Action uses the From Action trigger and three Agent steps. Each step adds the Load_OCR_Output input, which loads the stored OCR text from Action 1.
Step 1 — VendorMatch. An Agent step with the Search_vendors tool. The prompt asks the Agent to resolve the supplier on the invoice to a vendor number and to end its response with an easy-to-parse line:
Try to find the vendor number from Business Central using the search tool
and the raw OCR output of the incoming purchase invoice.
Output the vendor number as the final line of your response, like this:
Vendor number: <vendor number>
If you cannot find the vendor, end with:
Vendor number: NOT_FOUND
Step 2 — CreateInvoice. An Agent step with the Create_purchase_invoice tool. It receives the previous step's analysis through a step-output reference and applies the same final-line convention for the created invoice's ID:
Create a purchase invoice in Business Central based on the OCR data
generated from the invoice image.
The previous agent already found a matching vendor, so use that vendor
number in the invoice. Previous agent analysis on the vendor:
{{VendorMatch.message}}
Output the invoice's document id (the `id` returned by the Business Central
API) as the final line of your response, like this:
Invoice id: <invoice id>
If you cannot create the invoice, end with:
Invoice id: NOT_CREATED
Step 3 — CreateInvoiceLines. An Agent step with the Add_purchase_invoice_line tool, referencing {{CreateInvoice.message}} to pick up the document ID, and instructed to report the created line IDs the same way. Give this step a higher iteration limit than the others — it makes one tool call per invoice line.
The final-line convention (Vendor number: ..., Invoice id: ...) makes each step's result trivially consumable by the next prompt. For machine-readable output instead, enable structured output on the Agent step with a JSON schema — for example { "po_number": "..." } — and reference the fields directly in later steps.
Extending the Flow
- Human-in-the-loop for missing data: a follow-up Action analyzes the OCR text for a PO number and, when missing, an Agent creates a Human Task asking AP staff to supply it — then posts the answer back onto the invoice as a
Commentline usingList_purchase_invoice_lines(to find the next sequence number) andAdd_purchase_invoice_line. - Notifications: a Trigger Action step passes the created document ID to a notification Action that posts an invoice summary to Teams or Slack through a separate OpenAPI app.
- Error handling: set an error Action on both Actions so failed runs notify the team with a link to the run.
More Business Central Operations
The same app can host any other Business Central API v2.0 operation as an additional tool, all with the same Authorization header and path prefix:
- Post the draft invoice —
POST .../purchaseInvoices({document_id})/Microsoft.NAV.postwith an empty body. Invoices created through the API are drafts; posting them is a deliberate separate call, which many teams keep behind a Human Task approval. - Download the invoice PDF —
GET .../purchaseInvoices({document_id})/pdfDocument/pdfDocumentContentreturns the rendered document. Define it as an input rather than a tool, so the binary response is stored as a run file. - Vendor management —
POST .../vendorscreates vendors; pair it with lookup tools for.../paymentTermsand.../paymentMethodsso an Agent can resolvepaymentTermsIdandpaymentMethodIdGUIDs before creating one. - Any other v2.0 entity — sales orders, items, G/L accounts, customers, and more follow the same pattern. Microsoft publishes an OpenAPI specification for the v2.0 APIs; the app's Import from OpenAPI option can generate tools from it directly — import only the operations you need, then trim the generated schemas.
Things to Know
- Invoices are created as drafts. Nothing is posted to the ledger by the tools in this guide. Posting is a separate bound-action call that you add — and gate — deliberately.
- One app per environment. The environment name and company ID are baked into tool paths, and credentials differ per environment anyway. Keep
sandboxandproductionas separate Studio apps and see Promoting from UAT to Production for moving Actions between them. - Company IDs are stable. The company GUID never changes, so baking it into the path is safe. Multi-company environments can expose the company as a tool parameter with a default instead.
- Query placeholders need no location entry. A
{parameter}inside aQuerytemplate value is substituted from the tool parameters directly;x-parameter-locationsis only needed to route parameters into the path or body. - Keep responses small. Business Central entities are wide. Use
$selectand$filterin query templates, and Output Filters on tool assignments, so Agent steps are not flooded with fields they do not need. Business Central also throttles heavy API traffic, so avoid unbounded listing loops. - Permission errors have two layers. Entra controls whether a token can be issued for the API; Business Central permission sets control what the application may do with it. Both must be in place.
- Client secrets expire. Track the secret's expiry date in Entra and update it in the app's Configure Credentials dialog before it lapses.
- Store the client secret only in the app credentials dialog, never in Action prompts, step parameters, or configuration templates.
Troubleshooting
- Token acquisition fails with
401 invalid_client— the client secret is expired or wrong. Create a new secret in Entra and update it in Configure Credentials. - Test Connection succeeds but every call returns
401 Unauthorized— the token carries no Business Central permission. Either theAPI.ReadWrite.Allapplication permission was never added or admin consent was never granted (Step 1), or the application card in Business Central is missing or State is not Enabled (Step 2). Test Connection verifies token acquisition only, not permissions. - Calls return
403 Forbiddenor a "You do not have permission" message — the application is enabled in Business Central but its permission sets do not cover the touched objects (Step 2). 404 Not Foundon every path — the environment name or company ID in the tool path is wrong. Verify the environment name in the Business Central admin center and re-runList_companies(Step 4).- Creating a line fails with a sequence error — the sequence number collides with an existing line. Call
List_purchase_invoice_linesand use the next free multiple of 10000.
Related Pages
- OpenAPI Apps — configuration templates, parameter schemas, and credential placeholders in detail.
- Gateway Mailboxes — receiving supplier emails without an Exchange app.
- Email in Actions — more patterns for email-triggered Action Flows.
- Multi-Action Flow with OCR — how stored OCR output travels between chained Actions.
- Human Tasks — adding approval and data-entry steps to the flow.