With the release of Business Central 2020 wave 2, a new version of the standard API set has been released. This new version got its own documentation here: https://docs.microsoft.com/en-us/dynamics-nav/api-reference/v2.0/. But while looking into it I was missing an overview of what has been changed between v1.0 and v2.0. And nothing was mentioned in the what’s new overview either. The only resource I’ve found so far is this video from the virtual launch event: https://events1.social27.com/MSDyn365BCLaunchEvent/agenda/player/72576. Which is a great video by the way, but I that kind of guy that prefers to read instead of watching. So I decided to dive in and try to compile a complete list of all these changes. I guess I can’t be 100% complete here, but with your help we can expand this list. If you notice any change that should be on this list, then please drop me an email or write it in the comments below.
URL
First of all, the URL has been changed. For version v2.0 you need to use /api/v2.0 in the URL. The full URL of the API in a production environment on SaaS is now:
https://api.businesscentral.dynamics.com/v2.0/production/api/v2.0
Don’t let the double v2.0 in the URL confuse you. The first v2.0 is the version of the online platform that supports multiple environments. It allows you to use your own names for the environments. The second v2.0 is the api version that you want to call.
For an on-prem server, or docker container, the URL is now:
https://mysandbox:7048/bc/api/v2.0
Whereas mysandbox is the name of your container or server where the Business Central service tier is running.
One important thing to mention here: the v1.0 version is still available. Your existing integrations with API v1.0 will not break because v2.0 was introduced. This is a very common scenario in the API world, you get the time to implement the new version. Of course you will be encouraged to do so because v1.0 will be removed at a certain point in the future. But hey, the API beta version should already been gone by now but it is still available.
Source code
The source code of API v1.0 and v2.0 can be found on GitHub in the ALAppExtensions repository:
https://github.com/microsoft/ALAppExtensions/tree/master/Apps/W1/APIV1
https://github.com/microsoft/ALAppExtensions/tree/master/Apps/W1/APIV2
Just clone the repository and you have full access to the code, which can be very helpful!
Differences
The most important question is of course how different the API v2.0 is from v1.0. Are there many new fields, did the structure change, etc.? Best way to see the differences the two version is by comparing the metadata. And there is a massive difference between the two versions. See here for yourself: https://editor.mergely.com/oTms3IZF. Let’s look at a number of important differences.
Nested objects
In v1.0 there was something called Edm Types or complex types. This was a feature to return nested JSON objects in the result of an API call. In the picture below that is the case with the address property in the customer API.
// Customers API v1.0
{
"@odata.etag": "W/\"JzQ0O0NVYTIrTXBEV0V6VGZtcStpa3Zaekp0clFGZFRndDArZFJSRFh6eUkrL0U9MTswMDsn\"",
"id": "983833b3-b1fd-ea11-bc7d-00155df3a615",
"number": "10000",
"displayName": "Adatum Corporation",
"type": "Company",
"phoneNumber": "",
"email": "robert.townes@contoso.com",
"website": "",
"taxLiable": true,
"taxAreaId": "df3a33b3-b1fd-ea11-bc7d-00155df3a615",
"taxAreaDisplayName": "ATLANTA, GA",
"taxRegistrationNumber": "",
"currencyId": "00000000-0000-0000-0000-000000000000",
"currencyCode": "USD",
"paymentTermsId": "aa3733b3-b1fd-ea11-bc7d-00155df3a615",
"shipmentMethodId": "00000000-0000-0000-0000-000000000000",
"paymentMethodId": "8a3a33b3-b1fd-ea11-bc7d-00155df3a615",
"blocked": " ",
"lastModifiedDateTime": "2020-09-23T15:31:09.68Z",
"address": {
"street": "192 Market Square",
"city": "Atlanta",
"state": "GA",
"countryLetterCode": "US",
"postalCode": "31772"
}
}
In the code of the v1.0 API page this was defined as follows (note the property ODataEDMType):
field(address; PostalAddressJSON)
{
Caption = 'address', Locked = true;
ODataEDMType = 'POSTALADDRESS';
ToolTip = 'Specifies the address for the customer.';
trigger OnValidate()
begin
PostalAddressSet := TRUE;
end;
}
To read more about the property ODataEDMType, I recommend reading this detailed post from Vjeko about this topic. The point I would like to make here is that this property is not used anymore in the v2.0 API. Not any of the APIs does have a nested data structure that is defined in this way. Instead, you will see first-level properties or navigation properties. The address property in the customer API for example has been replaced by first-level properties. In the picture below you can see that we now have all address fields directly on the same level as the other properties, just as they appear in the table.
// Customers API v2.0
{
"@odata.etag": "W/\"JzQ0O0NVYTIrTXBEV0V6VGZtcStpa3Zaekp0clFGZFRndDArZFJSRFh6eUkrL0U9MTswMDsn\"",
"id": "983833b3-b1fd-ea11-bc7d-00155df3a615",
"number": "10000",
"displayName": "Adatum Corporation",
"type": "Company",
"addressLine1": "192 Market Square",
"addressLine2": "",
"city": "Atlanta",
"state": "GA",
"country": "US",
"postalCode": "31772",
"phoneNumber": "",
"email": "robert.townes@contoso.com",
"website": "",
"taxLiable": true,
"taxAreaId": "df3a33b3-b1fd-ea11-bc7d-00155df3a615",
"taxAreaDisplayName": "ATLANTA, GA",
"taxRegistrationNumber": "",
"currencyId": "00000000-0000-0000-0000-000000000000",
"currencyCode": "USD",
"paymentTermsId": "aa3733b3-b1fd-ea11-bc7d-00155df3a615",
"shipmentMethodId": "00000000-0000-0000-0000-000000000000",
"paymentMethodId": "8a3a33b3-b1fd-ea11-bc7d-00155df3a615",
"blocked": " ",
"lastModifiedDateTime": "2020-09-23T15:31:09.68Z"
}
An example of a navigational property can be found in the items API. In v1.0 the properties of the Base Unit of Measure are represented as a nested object.
// Items API v1.0
{
"@odata.etag": "W/\"JzQ0O1NsakZCQWNRU21INi8xOS9zWFhXSWRwWGxNbVArOXdjWVM0NGxqOUJxUUU9MTswMDsn\"",
"id": "a23833b3-b1fd-ea11-bc7d-00155df3a615",
"number": "1896-S",
"displayName": "ATHENS Desk",
"type": "Inventory",
"itemCategoryId": "2fd92eb9-b1fd-ea11-bc7d-00155df3a615",
"itemCategoryCode": "TABLE",
"blocked": false,
"baseUnitOfMeasureId": "023933b3-b1fd-ea11-bc7d-00155df3a615",
"gtin": "",
"inventory": 4,
"unitPrice": 1000.8,
"priceIncludesTax": false,
"unitCost": 780.7,
"taxGroupId": "ee3a33b3-b1fd-ea11-bc7d-00155df3a615",
"taxGroupCode": "FURNITURE",
"lastModifiedDateTime": "2020-09-23T15:31:11.057Z",
"baseUnitOfMeasure": {
"code": "PCS",
"displayName": "Piece",
"symbol": null,
"unitConversion": null
}
}
In the Items API v2.0 not all of these Unit of Measure properties are included as first-level properties. Only the Base Unit of Measure Code is included as that is a field in the table.
// Items API v2.0
{
"@odata.etag": "W/\"JzQ0O0xYWXRncXdObnU0Q2ppc25kV1Jac2NNMHZmUExXSjNVSy8yWGZBSFdXY0E9MTswMDsn\"",
"id": "a23833b3-b1fd-ea11-bc7d-00155df3a615",
"number": "1896-S",
"displayName": "ATHENS Desk",
"type": "Inventory",
"itemCategoryId": "2fd92eb9-b1fd-ea11-bc7d-00155df3a615",
"itemCategoryCode": "TABLE",
"blocked": false,
"gtin": "",
"inventory": 4,
"unitPrice": 1000.8,
"priceIncludesTax": false,
"unitCost": 780.7,
"taxGroupId": "ee3a33b3-b1fd-ea11-bc7d-00155df3a615",
"taxGroupCode": "FURNITURE",
"baseUnitOfMeasureId": "023933b3-b1fd-ea11-bc7d-00155df3a615",
"baseUnitOfMeasureCode": "PCS",
"lastModifiedDateTime": "2020-09-23T15:34:03.207Z"
}
So, where did those properties go? The Items API v2.0 has a new navigation property, called unitOfMeasure. A navigation property represents data from related tables and can be optionally included in the returned data. To add this as to the result, you need to add the parameter ?$expand=unitOfMeasure. The URL then looks like:
https://api.businesscentral.dynamics.com/v2.0/production/api/v2.0/company({id})/items?$expand=unitOfMeasure
The result now looks like this:
// Items API v2.0
{
"@odata.etag": "W/\"JzQ0O0xYWXRncXdObnU0Q2ppc25kV1Jac2NNMHZmUExXSjNVSy8yWGZBSFdXY0E9MTswMDsn\"",
"id": "a23833b3-b1fd-ea11-bc7d-00155df3a615",
"number": "1896-S",
"displayName": "ATHENS Desk",
"type": "Inventory",
"itemCategoryId": "2fd92eb9-b1fd-ea11-bc7d-00155df3a615",
"itemCategoryCode": "TABLE",
"blocked": false,
"gtin": "",
"inventory": 4,
"unitPrice": 1000.8,
"priceIncludesTax": false,
"unitCost": 780.7,
"taxGroupId": "ee3a33b3-b1fd-ea11-bc7d-00155df3a615",
"taxGroupCode": "FURNITURE",
"baseUnitOfMeasureId": "023933b3-b1fd-ea11-bc7d-00155df3a615",
"baseUnitOfMeasureCode": "PCS",
"lastModifiedDateTime": "2020-09-23T15:34:03.207Z",
"unitOfMeasure": {
"@odata.etag": "W/\"JzQ0O25yOXVDUTNxWDdtMTRIcnZ5UHZpOGp2Q0lQWFl1NFhqbzA0OTdXOGNPbDA9MTswMDsn\"",
"id": "023933b3-b1fd-ea11-bc7d-00155df3a615",
"code": "PCS",
"displayName": "Piece",
"internationalStandardCode": "EA",
"symbol": "",
"lastModifiedDateTime": "2020-09-23T15:31:13.643Z"
}
}
Another property that was changed in the same way is the property dimensions. Previously, you would find dimensions of a journal line under the property dimensions. In v2.0 this has been changed to dimensionSetLines and needs to be expanded.
To find all available navigational properties, you can read either the documentation. If an API has a navigation property is will be documented. For the Items API it can be found here. You can of course also look into the metadata, where you will find the complete definition in XML.
The change to use navigation properties instead of complex types has a big effect on performance. Complex types needed to be constructed by code, but the navigation properties are just other API pages (or subpages if you want). Which results in less code, but you need to remember to include $expand= in the url to get those navigation properties.
Relationship multiplicities
The move from complex types to API subpages comes with another change that was needed. The complex types could be defined as an object or as a collection. But API subpages were always treated as a collection, even if they only could contain one record. Here is an example what I mean. In the source code of the customers API page you will find this subpage:
part(customerFinancialDetails; 20048)
{
Caption = 'Customer Financial Details', Locked = true;
EntityName = 'customerFinancialDetail';
EntitySetName = 'customerFinancialDetails';
SubPageLink = SystemId = FIELD(SystemId);
}
The metadata shows what the result will look like, as a collection:
<NavigationProperty Name="customerFinancialDetails" Type="Collection(Microsoft.NAV.customerFinancialDetail)" Partner="customer" ContainsTarget="true">
<ReferentialConstraint Property="id" ReferencedProperty="id" />
</NavigationProperty>
When you call the customers API with $expand=customerFinancialDetails you will get the details in an array. But the page subpage itself is based on the same table customer and just includes a number of flowfields. This is done for performance reasons, by the way. In API v1.0 the JSON looks like this:
{
"id": "5eea6afd-4a2a-eb11-bb4f-000d3a25f2a9",
"number": "10000",
"displayName": "Adatum Corporation",
"type": "Company",
"customerFinancialDetails": [
{
"@odata.etag": "W/\"JzQ0O25GVTRvaWdTUk13aG9TOUEySzhYaTM1WE84SkxDeXF5MEdTeUlZblhGSlk9MTswMDsn\"",
"id": "5eea6afd-4a2a-eb11-bb4f-000d3a25f2a9",
"number": "10000",
"balance": 0,
"totalSalesExcludingTax": 223598.4,
"overdueAmount": 0
}
]
}
As you can see, the array contains only one record, and it will always contain just one record. With complex types, you could define this in the Edm type definition. But Edm types are now not used anymore. Instead, Business Central v17 allows to specify the multiplicity as a property on the page part and that’s being used in API v2.0 app to get a single nested object instead of a collection:
part(customerFinancialDetails; "APIV2 - Cust Financial Details")
{
//TODO - WaitingModernDevProperty, Caption = 'Customer Financial Details';
CaptionML = ENU = 'Multiplicity=ZeroOrOne';
EntityName = 'customerFinancialDetail';
EntitySetName = 'customerFinancialDetails';
SubPageLink = SystemId = Field(SystemId);
}
The metadata now shows that this is a single object instead of a collection. But be careful, because it is a single object it’s referenced by its EntityName, not by the EntitySetName!
<NavigationProperty Name="customerFinancialDetail" Type="Microsoft.NAV.customerFinancialDetail" ContainsTarget="true">
<ReferentialConstraint Property="id" ReferencedProperty="id" />
</NavigationProperty>
This is another breaking change with API v2.0. In this example, we must add $expand=customerFinancialDetail to the URL (without the s at the end). The result now looks like:
{
"id": "5eea6afd-4a2a-eb11-bb4f-000d3a25f2a9",
"number": "10000",
"displayName": "Adatum Corporation",
"type": "Company",
"customerFinancialDetail": {
"id": "5eea6afd-4a2a-eb11-bb4f-000d3a25f2a9",
"number": "10000",
"balance": 0,
"totalSalesExcludingTax": 223598.4,
"overdueAmount": 0
}
}
Back to the property that is used to define the multiplicity. At the release of Business Central v17, there was no property available. As a workaround the CaptionML property is used in this way:
CaptionML = ENU = 'Multiplicity=ZeroOrOne';
Multiplicuty can be set as ‘ZeroOrOne’ or ‘Many’. The default is ‘Many’, which will result in a collection. In runtime 6.1 the new property Multiplicity is already available, however, it doesn’t work yet. If you need this in your custom APIs, then you have to use the workaround with CaptionML for now.
Key fields in the url
In v1.0 there were still some APIs that didn’t use the SystemId as the primary key. But in v2.0 that has changed. All entities can be retrieved with the SystemId, which is a unique GUID. In case you have scenario to create a new record and provide the SystemId, that’s also possible in API v2.0.
Enums
Another change in the schema of API v2.0 is the possibility to use enums. To use enums, you must add ?$schemaversion=2.0 to the API url. If you do so, you will get different behavior for fields that are based on enums. All exposed fields that were of type option in v1.0 are converted into enums for v2.0. The difference can be explored here: https://editor.mergely.com/IWfP3kR0/.
Here is an example of what is added to the schema:
<EnumType Name="contactType">
<Member Name="Company" Value="0" />
<Member Name="Person" Value="1" />
</EnumType>
<EnumType Name="customerBlocked">
<Member Name="_x0020_" Value="0" />
<Member Name="Ship" Value="1" />
<Member Name="Invoice" Value="2" />
<Member Name="All" Value="3" />
</EnumType>
Those fields were previously exposed as Edm.String, but now they are strong typed:
<Property Name="blocked" Type="Microsoft.NAV.customerBlocked" />
This is very helpful in case you want to know which values are available for enum fields. For example to verify data before sending it or to display only valid values in a dropdown to a user.
What about that strange value “_x0020_”? That’s of course the empty ordinal value, also known as ” “. It’s the Unicode encoded representation of a space. You would expect that you also need to use this Unicode encoding when posting data. But it turns out that spaces can still be used. Below are two examples of a JSON body for the customers API. The first one is only accepted when you use schema version 2.0, the second one works fine in all cases.
// This body works only with ?$schemaversion=2.0
{
"displayName": "Demo Enum",
"blocked": "_x0020_"
}
// This body works for both schemaversions
{
"displayName": "Demo Enum",
"blocked": " "
}
In case you want to present the possible options to a user, then you need to decode the Unicode value to get back the original value. But wait… there is another new feature in API v2.0 that’s even better…
Captions
Yep… captions are now exposed by a specific endpoint. For those integrations that need to present fields and/or enum values in a UI, they can grab the captions coming from BC, including the translations! The endpoint is named entityDefinitions. Actually, this is not only available for v2.0, it’s also available for v1.0. But for v1.0 it doesn’t make much sense, because most translations are missing in v1.0. In other words, it’s a new platform feature for APIs in Business Central v17, and API v2.0 is the first app to make use of it.
Ok, what is it?
The entityDefinitions endpoint provides multilanguage captions for the entity, the entity set plus all properties of the entity. And if you include schema version 2.0, then you get the enum captions included. This is how the structure looks like:
{
"entityName": "customer",
"entitySetName": "customers",
"entityCaptions": [],
"entitySetCaptions": [],
"properties": [],
"actions": [],
"enumMembers": []
}
Each array contains multiple translations. For example, the entityCaptions looks like this:
"entityCaptions": [
{
"languageCode": 3079,
"caption": "Debitor"
},
{
"languageCode": 2055,
"caption": "Debitor"
},
{
"languageCode": 1044,
"caption": "Kunde"
},
{
"languageCode": 1031,
"caption": "Debitor"
},
{
"languageCode": 1053,
"caption": "Kund"
},
{
"languageCode": 4105,
"caption": "Customer"
},
{
"languageCode": 2057,
"caption": "Customer"
},
{
"languageCode": 5129,
"caption": "Customer"
},
{
"languageCode": 3081,
"caption": "Customer"
},
{
"languageCode": 1030,
"caption": "Kunde"
},
{
"languageCode": 1043,
"caption": "Klant"
},
{
"languageCode": 2067,
"caption": "Klant"
},
{
"languageCode": 1049,
"caption": "Клиент"
},
{
"languageCode": 1033,
"caption": "Customer"
},
{
"languageCode": 1040,
"caption": "Cliente"
},
{
"languageCode": 2064,
"caption": "Cliente"
},
{
"languageCode": 1035,
"caption": "Asiakas"
},
{
"languageCode": 1029,
"caption": "Zákazník"
},
{
"languageCode": 3082,
"caption": "Cliente"
},
{
"languageCode": 3084,
"caption": "Client"
},
{
"languageCode": 4108,
"caption": "Client"
},
{
"languageCode": 2058,
"caption": "Cliente"
},
{
"languageCode": 1039,
"caption": "Viðskiptamaður"
},
{
"languageCode": 1036,
"caption": "Client"
},
{
"languageCode": 2060,
"caption": "Client"
}
]
The properties contains a list of all properties with their captions (only showing displayName here):
"properties": [
{
"name": "displayName",
"captions": [
{
"languageCode": 3079,
"caption": "Anzeigename"
},
{
"languageCode": 2055,
"caption": "Anzeigename"
},
{
"languageCode": 1044,
"caption": "Visningsnavn"
},
{
"languageCode": 1031,
"caption": "Anzeigename"
},
{
"languageCode": 1053,
"caption": "Visningsnamn"
},
{
"languageCode": 4105,
"caption": "Display Name"
},
{
"languageCode": 2057,
"caption": "Display Name"
},
{
"languageCode": 5129,
"caption": "Display Name"
},
{
"languageCode": 3081,
"caption": "Display Name"
},
{
"languageCode": 1030,
"caption": "Visningsnavn"
},
{
"languageCode": 1043,
"caption": "Weergavenaam"
},
{
"languageCode": 2067,
"caption": "Weergavenaam"
},
{
"languageCode": 1049,
"caption": "Отображаемое имя"
},
{
"languageCode": 1033,
"caption": "Display Name"
},
{
"languageCode": 1040,
"caption": "Nome visualizzato"
},
{
"languageCode": 2064,
"caption": "Nome visualizzato"
},
{
"languageCode": 1035,
"caption": "Näyttönimi"
},
{
"languageCode": 1029,
"caption": "Zobrazit název"
},
{
"languageCode": 3082,
"caption": "Nombre para mostrar"
},
{
"languageCode": 3084,
"caption": "Nom d’affichage"
},
{
"languageCode": 4108,
"caption": "Nom d’affichage"
},
{
"languageCode": 2058,
"caption": "Nombre para mostrar"
},
{
"languageCode": 1039,
"caption": "Birtingarheiti"
},
{
"languageCode": 1036,
"caption": "Nom d’affichage"
},
{
"languageCode": 2060,
"caption": "Nom d’affichage"
}
]
}
]
Where do the captions and the translations come from? Not from the base app! Every API page has captions defined as you can see here in the first lines of the customers API:
page 30009 "APIV2 - Customers"
{
APIVersion = 'v2.0';
EntityCaption = 'Customer';
EntitySetCaption = 'Customers';
ChangeTrackingAllowed = true;
DelayedInsert = true;
EntityName = 'customer';
EntitySetName = 'customers';
ODataKeyFields = SystemId;
PageType = API;
SourceTable = Customer;
Extensible = false;
layout
{
area(content)
{
repeater(Group)
{
field(id; SystemId)
{
Caption = 'Id';
Editable = false;
}
field(number; "No.")
{
Caption = 'No.';
}
field(displayName; Name)
{
Caption = 'Display Name';
ShowMandatory = true;
[further lines truncated]
You can see the property EntityCaption and EntitySetCaption, both of which are also visible in the JSON from entityDefinitions. Same counts for the caption on the fields. The translations of these captions are included in the API app. They are not taken from the base app. This is something to keep in mind, as this can be really confusing!
If you use schema version 2.0, then you also get the enums in this way:
{
"entityName": "customerBlocked",
"entitySetName": null,
"entityCaptions": [],
"entitySetCaptions": [],
"properties": [],
"actions": [],
"enumMembers": [
{
"name": "_x0020_",
"value": 0,
"captions": [
{
"languageCode": 1033,
"caption": " "
}
]
},
{
"name": "Ship",
"value": 1,
"captions": [
{
"languageCode": 1033,
"caption": "Ship"
}
]
},
{
"name": "Invoice",
"value": 2,
"captions": [
{
"languageCode": 1033,
"caption": "Invoice"
}
]
},
{
"name": "All",
"value": 3,
"captions": [
{
"languageCode": 1033,
"caption": "All"
}
]
}
]
}
What is strange here is that we only see captions for language code 1033 (ENU). Somehow, the translations of the enum “Customer Blocked” are not included. I checked the translation files of the base app (because the enums are defined in the base app), but the translations for those enums are definitely there. My guess is that the entityDefinitions only takes translations from the API app and does not look into the base app. This is something for Microsoft to fix!
This new entityDefinitions endpoint also works with custom APIs. Just include the captions and translations and you’re all set!
That’s what I could find about the new API features in Business Central v17 and API v2.0. If you have any addition, please let me know!