> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ingestly.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Microsoft Business Central

> Write to Business Central from a pipeline using deep insert and OData $batch.

Ingestly writes to Microsoft Dynamics 365 Business Central via the [HTTP action](/nodes/http-action) and [callback output](/nodes/callback) nodes. Both authenticate through a **Business Central connector**, which supplies the OAuth bearer token and the API base URL so your node only types the relative path.

There are two write patterns. Pick the simpler one when it applies.

## 1. Connect

1. Open **Settings → Connectors** and create a connector of type **Business Central**.
2. Sign in with the Microsoft account that has access to the BC environment.
3. Choose the environment (`Production` or your sandbox name) and tenant.
4. Set the **Company ID** (the default company GUID) so Ingestly can build the company base path for you.
5. Save. The connector now exposes a base URL like `https://api.businesscentral.dynamics.com/v2.0/{tenantId}` and injects the bearer token on every request.

In any HTTP action or callback node, select this connector and your URL field becomes a relative path.

<Tip>When you select a Business Central connector in an HTTP action or callback output node and the **URL** field is still empty, Ingestly auto-seeds it with the company base path (built from the connector's environment and `Company ID`), so you only append the resource segment (for example `salesQuotes`). It only seeds a blank URL and never overwrites a URL you have already edited. This replaces the older Sales/Purchase Orders resource template picker.</Tip>

## 2. Deep insert (recommended for one parent + its children)

A **deep insert** is a single `POST` to a parent entity whose body contains its OData navigation children inline. BC creates the parent and all children atomically.

Use it when you want to create one parent (sales quote, sales order, sales invoice, purchase invoice, …) along with its lines in one call.

**HTTP action configuration**

| Field       | Value                                                     |
| ----------- | --------------------------------------------------------- |
| Connector   | Your Business Central connector                           |
| Method      | `POST`                                                    |
| URL         | `/production/api/v2.0/companies({companyId})/salesQuotes` |
| Body Format | `JSON`                                                    |
| Body        | The JSON below                                            |

```json theme={null}
{
  "customerId": "5608c139-9029-f111-9f24-7ced8dad939a",
  "documentDate": "2026-04-28",
  "externalDocumentNumber": "EXT-QUOTE-001",
  "salesQuoteLines": [
    {
      "lineType": "Item",
      "lineObjectNumber": "1896-S",
      "description": "First item",
      "quantity": 2,
      "unitPrice": 150
    },
    {
      "lineType": "Item",
      "lineObjectNumber": "1896-S",
      "description": "Second item",
      "quantity": 1,
      "unitPrice": 300
    }
  ]
}
```

Common parent / child pairs that support deep insert:

* `salesQuotes` → `salesQuoteLines`
* `salesOrders` → `salesOrderLines`
* `salesInvoices` → `salesInvoiceLines`
* `purchaseInvoices` → `purchaseInvoiceLines`

<Tip>If your scenario fits this shape, stop here. Deep insert is simpler, faster, and easier to debug than `$batch`.</Tip>

## 3. When deep insert isn't enough: use `$batch`

Deep insert only handles "one parent and its immediate children, all created together." Use OData `$batch` when you need any of:

* **Mix methods** in one round-trip transaction (`POST` + `PATCH` + `DELETE`).
* **Cross-entity transactions** across unrelated parents (e.g. create a customer *and* a vendor in one transaction).
* **Bulk creation** across many parents in a single HTTP call (50 quotes in one request rather than 50 requests).
* **Coordinate separate endpoints** in one transaction, such as creating a quote and an order together.

<Note>Business Central's `$batch` does not implement OData v4 `$<contentId>` URL references between operations, so each operation must specify its full target path. Ingestly rejects any operation whose `url` starts with `$` before the request is sent, with guidance to move the dependent operation into a second, chained callback. For "create parent A, then attach child to A's new id," use deep insert (children nested in the parent body), or chain a second [callback](/nodes/callback#chaining-callbacks) (see [Attach the source document to a record](#5-attach-the-source-document-to-a-record)).</Note>

Instead of asking you to handcraft the Business Central JSON batch payload, Ingestly accepts a structured **JSON description of the operations** and packages it into a `$batch` request with `Isolation: snapshot` on the server.

**HTTP action configuration**

| Field       | Value                           |
| ----------- | ------------------------------- |
| Connector   | Your Business Central connector |
| Method      | `POST`                          |
| URL         | `/production/api/v2.0/$batch`   |
| Body Format | `OData Batch`                   |
| Body        | The JSON below                  |

```json theme={null}
{
  "companyId": "11111111-2222-3333-4444-555555555555",
  "operations": [
    {
      "contentId": "quote",
      "method": "POST",
      "url": "salesQuotes",
      "body": {
        "customerId": "5608c139-9029-f111-9f24-7ced8dad939a",
        "documentDate": "2026-04-28",
        "externalDocumentNumber": "EXT-QUOTE-001",
        "salesQuoteLines": [
          { "lineType": "Item", "lineObjectNumber": "1896-S", "quantity": 2, "unitPrice": 150 },
          { "lineType": "Item", "lineObjectNumber": "1896-S", "quantity": 1, "unitPrice": 300 }
        ]
      }
    },
    {
      "contentId": "order",
      "method": "POST",
      "url": "salesOrders",
      "body": {
        "customerId": "5608c139-9029-f111-9f24-7ced8dad939a",
        "externalDocumentNumber": "EXT-ORDER-001"
      }
    }
  ]
}
```

### Body shape

| Field        | Required | Description                                                                                                                                                                                                                                                                    |
| ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `companyId`  | no       | Business Central company GUID. When set, every operation `url` is automatically prefixed with `companies(<companyId>)/`. Operations whose `url` starts with `/`, `companies(`, or `$` keep their original URL (escape hatch for absolute paths and `$<contentId>` references). |
| `operations` | yes      | Array of write operations to run in one transaction.                                                                                                                                                                                                                           |

### Operation shape

| Field        | Required | Description                                                                                                                                                                                                                                                                                                                                                            |
| ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `method`     | yes      | One of `POST`, `PUT`, `PATCH`, `DELETE`. `GET` is not allowed in batch write mode.                                                                                                                                                                                                                                                                                     |
| `url`        | yes      | Relative URL under `/api/v2.0`. With `companyId` set at the top, write `salesQuotes`; without it, write the full `companies({companyId})/salesQuotes`. A URL that starts with `$` (a `$<contentId>` reference to another operation) is rejected, because Business Central does not resolve those inside a batch: chain a second callback instead (see the note above). |
| `contentId`  | no       | A string id echoed back on the operation's response entry (`responses[].id`) so you can find its result. Auto-assigned `1`, `2`, … when omitted. Business Central does not resolve `$<contentId>` URL references between operations (see the note above).                                                                                                              |
| `headers`    | no       | Per-operation headers (e.g. `If-Match: *` for PATCH). The inner `Content-Type` is managed by the server.                                                                                                                                                                                                                                                               |
| `body`       | no       | The operation's JSON body.                                                                                                                                                                                                                                                                                                                                             |
| `binaryBody` | no       | A Base64 string sent as the operation's raw body with `Content-Type: application/octet-stream`. Mutually exclusive with `body`. Use it to upload file bytes inside a batch, typically `{{$document.file.base64}}`.                                                                                                                                                     |

The body editor validates the shape live: missing fields, unknown keys, and wrong methods are flagged inline as you type. An operation that sets both `body` and `binaryBody`, or a `binaryBody` that is not valid Base64, is rejected with a per-operation error.

<Note>To attach the source document to a record you create in the batch, you cannot upload it in the same batch: Business Central does not resolve `$<contentId>` references, so the upload cannot target the just-created record. Chain a second callback instead. See [Attach the source document to a record](#5-attach-the-source-document-to-a-record).</Note>

### Templating

The whole body is a string passed through Ingestly's template engine, so you can build the operations array dynamically from upstream node output, e.g. `{{aggregator.payload.operations}}`.

## 4. Notes & limits

* **One transaction per request.** Ingestly sends `Isolation: snapshot`, so the operations run in one Business Central transaction when the invoked APIs do not force their own commit.
* **Response is JSON.** BC replies with a `responses` array containing one inner response per operation.
* **Connector base URL is required.** The Business Central connector enforces a list of allowed hosts; absolute URLs are rejected for safety.
* **Parent plus children.** Put child collections like `salesQuoteLines` inside the parent `salesQuotes` body. Use extra batch operations for separate endpoints like `salesOrders`.
* **Write back approved values.** To push approved or reconciled values into Business Central, set the [Callback node](/nodes/callback) **Body format** to `Connector Write-back`. It complements the deep insert and `$batch` patterns above for cases where you map specific fields back after a Review task.

## 5. Attach the source document to a record

Business Central stores a file attachment as a `documentAttachments` record plus a separate binary upload of the file content. That is two calls, and the second needs the id the first returns. Business Central does not resolve `$<contentId>` references inside a `$batch` (see the note in section 3), so the two operations cannot ride in one batch. Chain two [callbacks](/nodes/callback#chaining-callbacks) after the callback that creates the record.

This recipe attaches the run's source PDF to a purchase invoice created by an upstream callback named `invoice`. It was verified against a live Business Central sandbox on 2026-07-24.

**Callback A: create the attachment record (JSON `POST`)**

| Field       | Value                                        |
| ----------- | -------------------------------------------- |
| Method      | `POST`                                       |
| URL         | `companies({companyId})/documentAttachments` |
| Body format | `JSON`                                       |

```json theme={null}
{
  "parentType": "Purchase Invoice",
  "parentId": "{{invoice.payload.responses[0].body.id}}",
  "fileName": "{{$document.file.name}}"
}
```

`parentId` reads the created invoice's system id from the upstream callback's batch [response](/nodes/callback#chaining-callbacks). `parentType` is the Business Central document type, exactly `Purchase Invoice` for a purchase invoice.

**Callback B: upload the file bytes (Binary `PATCH`)**

Chain this callback after Callback A (named `attach` here).

| Field       | Value                                                                                 |
| ----------- | ------------------------------------------------------------------------------------- |
| Method      | `PATCH`                                                                               |
| URL         | `companies({companyId})/documentAttachments({{attach.payload.id}})/attachmentContent` |
| Body format | `Binary (base64)`                                                                     |
| Body        | `{{$document.file.base64}}`                                                           |
| Header      | `If-Match: *`                                                                         |

The [binary body](/nodes/callback#binary-body) sends the raw file bytes with `application/octet-stream`. The `If-Match: *` header is required: Business Central rejects a content stream `PATCH` without it (see the [error playbook](#6-business-central-error-playbook)).

<Warning>This two-callback flow is not a single transaction. If Callback A creates the record but Callback B fails, the invoice and an empty attachment record already exist in Business Central, and a re-run can leave an orphaned empty attachment. Remove stray attachment records if a run fails between the two steps.</Warning>

## 6. Business Central error playbook

Business Central returns terse OData errors. These are the ones you are most likely to hit when writing records and attachments, with the fix for each.

| Error code / message                                                         | Cause                                                                                                                               | Fix                                                                                                                                               |
| ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BadRequest_InvalidToken`, "Could not validate the client concurrency token" | A content stream `PATCH` (such as `attachmentContent`) was sent without a concurrency header                                        | Add the header `If-Match: *` to the operation                                                                                                     |
| `BadRequest_NotFound`, "Error in query syntax"                               | A malformed entity key or URL: an unclosed parenthesis, or an empty key left by a template token that did not resolve               | Check the URL and confirm every `{{...}}` token in it resolves to a value                                                                         |
| "Request Id reference \[...] not found in effective depends-on-Ids"          | An operation URL used a `$<contentId>` reference to an earlier operation, which Business Central does not support inside a `$batch` | Move the dependent operation into a chained [callback](/nodes/callback#chaining-callbacks) and reference the previous callback's response instead |
