Batch calls with Business Central APIs (1) – Basic operation

18 Dec

A while ago I wrote about deep inserts with Business Central APIs. And I promised to write about batch calls too, so it’s about time to live up to that. 😀 Actually, I did some online sessions about it, like this one for DynamicsCon and also for Directions 2020. But I think it is still worthy to write it down. And because there is a lot to share about this topic, it’s my plan to write a series of posts. Today, we start with the basics of batch calls.

Background

When calling Business Central APIs you do one operation at a time. For example, you can only insert or modify one customer, or create one sales invoice. With deep inserts, it is possible to create header and lines together, and then you can create multiple lines. But that’s only possible on the line records, you still create one header at a time. With a batch request, it is possible to combine multiple operations in one request. The batch request is submitted as a single HTTP POST request to the $batch endpoint and the body contains a set of operations. From an optimization point of view, batching minimizes the number of requests from an API consumer because you combine multiple requests into one.

Batch URL

The $batch endpoint is available on all API endpoints. For the Business Central SaaS environment you can use this URL:

https://{{baseurl}}/api/v2.0/$batch

Everything I describe here also works for custom APIs. The URL will then look like:

https://{{baseurl}}/api/[publisher]/[group]/[version]/$batch

The parameter {{baseurl}} can be replaced with the standard endpoint URLs for Business Central as explained here: https://docs.microsoft.com/en-us/dynamics-nav/api-reference/v2.0/endpoints-apis-for-dynamics.

Request headers

Because the request body will be JSON the Content-Type header of the request must be set to appication/json. If the response should be JSON as well (and you want that!), then the Accept header must also be set to application/json. If you leave the Accept header out, then the response will be multipart/mixed. Here is the basic structure of the request, without the body. I leave out the Authorization header, but you need to add that obviously. 🙂

POST {{baseurl}}/api/v2.0/$batch
Content-Type: application/json
Accept: application/json

Request body

There are two body types supported when doing batch requests: multipart/mixed and application/json. Because multipart/mixed is more complex to use and read while the JSON body is much more readable and works fine with Business Central API, I will only discuss application/json in this blog post. The request body is a JSON with this basic format:

{
	"requests": []
}

As you can see, that’s quite a simple JSON payload, isn’t it?

The requests array must contain one ore more operations, and each of them must contains an id, the method and a URL and optionally also headers and a body. Here is an example of an operation to insert a single journal line.

{
	"method": "POST",
	"id": "r1",
	"url": "companies({{companyId}})/journals({{journalId}})/journalLines",
	"headers": {
		"Content-Type": "application/json"
	},
	"body": {
        "accountId": "{{accountId}}",
        "postingDate": "2020-10-20",
		"documentNumber": "SALARY2020-10",
		"amount": -3250,
		"description": "Salary to Bob"
	}
}

Each operation has a number of properties:

id

mandatory

Unique identification of this operation.

method

mandatory

One of the standard HTTP methods GET, POST, PATCH, PUT or DELETE. This value is case insensitive.

url

mandatory

Path to the API. This can be a relative path or an absolute path. 
relative path replaces the $batch part of the batch request url.
An absolute path replaces the complete url of the batch request.
The url may contain parameters like $select, $filter, etc.

headers

optional

List of headers for this operation.
Format: "header-name": "value"

body

optional

Mandatory if the request has method POST, PATCH or PUT.
The content of the body is the same as for a single request. For Business Central APIs this is usually a JSON payload. The headers must contain a Content-Type header that indicates the type of data in the body.

Let’s take the operation above, to insert a single journal line and compose a batch request that inserts multiple journal lines in one go. The request body looks like:

{
	"requests": [
		{
			"method": "POST",
			"id": "r1",
			"url": "companies({{companyId}})/journals({{journalId}})/journalLines",
			"headers": {
				"Content-Type": "application/json"
			},
			"body": {
			    "accountId": "{{accountId}}",
			    "postingDate": "2020-10-20",
			    "documentNumber": "SALARY2020-12",
			    "amount": -3250,
			    "description": "Salary to Bob"
			}
		},
		{
			"method": "POST",
			"id": "r2",
			"url": "companies({{companyId}})/journals({{journalId}})/journalLines",
			"headers": {
				"Content-Type": "application/json"
			},
			"body": {
			    "accountId": "{{accountId}}",
			    "postingDate": "2020-10-20",
			    "documentNumber": "SALARY2020-12",
			    "amount": -3500,
			    "description": "Salary to John"
	        }
		},
        {
			"method": "POST",
			"id": "r3",
			"url": "companies({{companyId}})/journals({{journalId}})/journalLines",
			"headers": {
				"Content-Type": "application/json"
			},
			"body": {
			    "accountId": "{{accountId2}}",
			    "postingDate": "2020-10-20",
			    "documentNumber": "SALARY2020-12",
			    "amount": 6750,
			    "description": "Salaries December 2020"
	        }
		}	
	]
}

As you can imagine, this can be done for inserting multiple customers, items, invoices, etc. The body can even be combined with deep inserts!

The response

Just like the request body is a combination of multiple operations, the response body is a combination of the multiple results. Let’s take a look at the response body for the batch request example:

{
    "responses": [
        {
            "id": "r1",
            "status": 201,
            "headers": {
                "location": "https://bcsandbox.docker.local:7048/bc/api/v2.0/companies(9f161476-1d3d-eb11-bb72-000d3a2b9218)/journals(f91409ba-1d3d-eb11-bb72-000d3a2b9218)/journalLines(6a9fec9f-6a40-eb11-a853-d0e7bcc597da)",
                "content-type": "application/json; odata.metadata=minimal",
                "odata-version": "4.0"
            },
            "body": {
                "@odata.context": "https://bcsandbox.docker.local:7048/bc/api/v2.0/$metadata#companies(9f161476-1d3d-eb11-bb72-000d3a2b9218)/journals(f91409ba-1d3d-eb11-bb72-000d3a2b9218)/journalLines/$entity",
                "@odata.etag": "W/\"JzQ0O0hOcTdrU25sTDNHNnRjTnBqMHNNMm94ZDUwK1JFK0txSmtkc0VQemN6Nmc9MTswMDsn\"",
                "id": "6a9fec9f-6a40-eb11-a853-d0e7bcc597da",
                "journalId": "f91409ba-1d3d-eb11-bb72-000d3a2b9218",
                "journalDisplayName": "DEFAULT",
                "lineNumber": 10000,
                "accountType": "G_x002F_L_x0020_Account",
                "accountId": "ae4110b4-1d3d-eb11-bb72-000d3a2b9218",
                "accountNumber": "60700",
                "postingDate": "2020-10-20",
                "documentNumber": "SALARY2020-12",
                "externalDocumentNumber": "",
                "amount": -3250.00,
                "description": "Salary to Bob",
                "comment": "",
                "taxCode": "NONTAXABLE",
                "balanceAccountType": "G_x002F_L_x0020_Account",
                "balancingAccountId": "00000000-0000-0000-0000-000000000000",
                "balancingAccountNumber": "",
                "lastModifiedDateTime": "2020-12-17T13:20:26.873Z"
            }
        },
        {
            "id": "r2",
            "status": 201,
            "headers": {
                "location": "https://bcsandbox.docker.local:7048/bc/api/v2.0/companies(9f161476-1d3d-eb11-bb72-000d3a2b9218)/journals(f91409ba-1d3d-eb11-bb72-000d3a2b9218)/journalLines(6b9fec9f-6a40-eb11-a853-d0e7bcc597da)",
                "content-type": "application/json; odata.metadata=minimal",
                "odata-version": "4.0"
            },
            "body": {
                "@odata.context": "https://bcsandbox.docker.local:7048/bc/api/v2.0/$metadata#companies(9f161476-1d3d-eb11-bb72-000d3a2b9218)/journals(f91409ba-1d3d-eb11-bb72-000d3a2b9218)/journalLines/$entity",
                "@odata.etag": "W/\"JzQ0O1cwbTBRYms5SVVjVEMzbzhCckhyc25YMzJ3N2paRGJWUXVyNDlTSGwvcU09MTswMDsn\"",
                "id": "6b9fec9f-6a40-eb11-a853-d0e7bcc597da",
                "journalId": "f91409ba-1d3d-eb11-bb72-000d3a2b9218",
                "journalDisplayName": "DEFAULT",
                "lineNumber": 20000,
                "accountType": "G_x002F_L_x0020_Account",
                "accountId": "ae4110b4-1d3d-eb11-bb72-000d3a2b9218",
                "accountNumber": "60700",
                "postingDate": "2020-10-20",
                "documentNumber": "SALARY2020-12",
                "externalDocumentNumber": "",
                "amount": -3500.00,
                "description": "Salary to John",
                "comment": "",
                "taxCode": "NONTAXABLE",
                "balanceAccountType": "G_x002F_L_x0020_Account",
                "balancingAccountId": "00000000-0000-0000-0000-000000000000",
                "balancingAccountNumber": "",
                "lastModifiedDateTime": "2020-12-17T13:20:26.927Z"
            }
        },
        {
            "id": "r3",
            "status": 201,
            "headers": {
                "location": "https://bcsandbox.docker.local:7048/bc/api/v2.0/companies(9f161476-1d3d-eb11-bb72-000d3a2b9218)/journals(f91409ba-1d3d-eb11-bb72-000d3a2b9218)/journalLines(6c9fec9f-6a40-eb11-a853-d0e7bcc597da)",
                "content-type": "application/json; odata.metadata=minimal",
                "odata-version": "4.0"
            },
            "body": {
                "@odata.context": "https://bcsandbox.docker.local:7048/bc/api/v2.0/$metadata#companies(9f161476-1d3d-eb11-bb72-000d3a2b9218)/journals(f91409ba-1d3d-eb11-bb72-000d3a2b9218)/journalLines/$entity",
                "@odata.etag": "W/\"JzQ0O25SN2NHT2s3QklhTVVNUDlwMlp6ZCtkdm12T3ZrUllNdnJ4aHZnbm5yV0U9MTswMDsn\"",
                "id": "6c9fec9f-6a40-eb11-a853-d0e7bcc597da",
                "journalId": "f91409ba-1d3d-eb11-bb72-000d3a2b9218",
                "journalDisplayName": "DEFAULT",
                "lineNumber": 30000,
                "accountType": "G_x002F_L_x0020_Account",
                "accountId": "844110b4-1d3d-eb11-bb72-000d3a2b9218",
                "accountNumber": "20700",
                "postingDate": "2020-10-20",
                "documentNumber": "SALARY2020-12",
                "externalDocumentNumber": "",
                "amount": 6750.00,
                "description": "Salaries December 2020",
                "comment": "",
                "taxCode": "NONTAXABLE",
                "balanceAccountType": "G_x002F_L_x0020_Account",
                "balancingAccountId": "00000000-0000-0000-0000-000000000000",
                "balancingAccountNumber": "",
                "lastModifiedDateTime": "2020-12-17T13:20:26.947Z"
            }
        }
    ]
}

As you can see, each operation has a corresponding result in the response, identified by the id. You should always use the id of the individual operation to find the corresponding result. Don’t assume that the results will always be in the same order as the request! They may seem to be in the same order, but the OData standard describes that the results can be in any order.

Each operation result has a status, which is the HTTP status that you would normally get for a single request. It includes the response headers and response body of the individual operation. The response of the batch request itself will always have status 200 if the server was able to read the batch request. Even if the batch request has operations that couldn’t be processed because of an error condition, the batch response status will still be 200. So don’t only look at the response status of the batch request, you need to read the response body to find out the results of each operation. Only when the batch request body is malformed, or you are not authorized, then the whole batch request will fail and you will get a status 500 (malformed batch request) or status 401 (not authorized).

So far it’s really simple, isn’t it? In the next blog posts in this series, I will cover topics like:

  • What happens if an error occurs in one of the requests
  • Process a batch as one big transaction
  • Combine multiple operations types, like POST and GET
  • Define the order of operations
  • Reduce the size of the response payload

Don’t expect 5 additional blog posts… I’m going to combine them of course, as a batch… you get it? 😉

13 thoughts on “Batch calls with Business Central APIs (1) – Basic operation

  1. This is a really great article! The official docs never mentioned about sending the id keys. I have a question – is it mandatory to sending the ids like r1, r2 etc. I tried sending ids – unique1, random2 etc – and was getting timeouts in this pattern.

    • You can use any value in the id, as long as it’s unique.

      The timeout might be different issue. Are you using decimals in the JSON payload? Try to change the Content-Type header of the request to: application/json;IEEE754Compatible=true

  2. Pingback: Batch calls with Business Central (2) – Error handling - Kauffmann @ Dynamics NAV - Dynamics 365 Business Central/NAV User Group - Dynamics User Group

  3. Pingback: Batch calls with Business Central APIs (3) – Tips and Tricks - Kauffmann @ Dynamics NAV - Dynamics 365 Business Central/NAV User Group - Dynamics User Group

  4. Pingback: Dynamics 365 Business Central: Batch API calls and max number of operations - Microsoft Dynamics NAV Community

  5. Pingback: Dynamics 365 Business Central: Batch API calls and max number of operations - Dynamics 365 Business Central Community

  6. Pingback: Dynamics 365 Business Central: Batch API calls and max number of operations - Stefano Demiliani's NAV Blog - Dynamics 365 Business Central/NAV User Group - Dynamics User Group

  7. Thanks a lot for sharing your knowledge. This article really helps me out. Any concerns on the batch size? Do you think it’s possible to do a batch POST into Business Central with more than let’s say 100,000 items in one batch file?

    Greetings from Germany.

    Simon

  8. Thank you! Indeed it requires some testing to get a better understanding for the API limitations described in the link you’ve shared. So at least I know what I’ve to do next. 😉

    Cheers!

  9. Hi,
    I tried batch posting for 4 records after enabling following parameters in headers
    “Prefer”: “odata.continue-on-error”,
    “OData-Isolation”: “snapshot”,

    My observations
    1. If 1st record has an error and rest 3 record is good then no record gets inserted in Business central but I could not see success response in Postman response window for last 3 records.
    2. If 1st,3rd and 4th records are good but if some issue is there in 2nd record then 1st record gets created in Business Central but I could not see success response in Postman response window for last 2 records. Success response is there for 1st record in Postman
    3. If 1st,2nd and 4th records are good but if some issue is there in 3rd record then 1st and 2nd records gets created in Business Central but I could not see success response in Postman response window for last record. Success response is there for 1st and 2nd record in Postman.

    Not sure where I am getting wrong while running Batch processing.
    Thanks in Advance

    Manu

  10. Do you have any thoughts how to achieve this with CAL. Create Bulk OData transfer from NAV 13 to BC using Custom API Page?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.