Skip to content

API Reference

This reference is for integrators. All endpoints live under

https://api.businesscentral.dynamics.com/v2.0/{tenant}/{environment}/api/altenbrand/mcIntegration/v1.0/companies({companyId})/
Endpoint Method Purpose
labelRequests POST, GET Request a label, read the outcome
shipmentLabels GET Read labels (no documents, no addresses)
shipmentLabels({id})/Microsoft.NAV.cancel POST Cancel the shipment at the carrier
shipmentLabels({id})/Microsoft.NAV.refreshTracking POST Ask the carrier for the delivery status
shipmentLabelDocuments({id}) GET Label, return label, customs document, COD label as Base64
shipmentTrackingEvents GET Read the tracking history: every scan the carrier reported
shipmentPackages GET The parcels of a multi-parcel shipment, one tracking number per carton
shipmentProviders GET Read the shipment providers and what they support
shipmentProviders({id})/Microsoft.NAV.getProducts POST Read the products of a provider
shipmentProviders({id})/Microsoft.NAV.getLabelArchive POST Labels and customs documents of many orders as one ZIP
shipmentProviders({id})/Microsoft.NAV.closeOutDay POST Close the day at the carrier, with its document
shipmentProviders({id})/Microsoft.NAV.getManifestDocument POST The document of an earlier close-out
shipmentProviders({id})/Microsoft.NAV.validateSalesOrder POST Check a document up front, without a label and without calling the carrier

Request instead of direct write

Labels are never inserted or modified through the API. The client creates a request; the app validates it, creates the label through the hub and writes the outcome back to the request. This keeps licence check, shipping rules and tracking out of reach of any client.


Requesting a label

POST …/labelRequests
Content-Type: application/json

{
  "externalReference": "WMS-2026-000481",
  "source": "WMS",
  "salesOrderNo": "SO-2026-01187",
  "providerCode": "DHL",
  "weightGrams": 1850,
  "mode": "Sync",
  "servicesJson": "[{\"code\":\"AGE_CHECK\",\"parameter1\":\"A18\"},{\"code\":\"INSURANCE\",\"parameter1\":\"2500.00\",\"parameter2\":\"EUR\"}]"
}
{
  "id": "5f0c…",
  "entryNo": 42,
  "status": "Completed",
  "shipmentLabelId": "b71e…",
  "shipmentLabelEntryNo": 10342,
  "trackingNo": "00340434161094015902",
  "errorMessage": ""
}

Request fields

Field Required Meaning
externalReference yes Reference of the calling system, unique together with source
source recommended Identifier of the system, e.g. WMS or SHOP
salesOrderNo / salesShipmentNo exactly one Sales order or posted sales shipment
providerCode no Shipment provider; empty = shipping agent of the document, default provider or the only enabled one
labelType no Outbound (default) or Return for a return label
weightGrams no Parcel weight; 0 = calculated document weight
productCode no Shipping agent service code that selects the carrier product
mode no Sync or Async; empty = default mode of the setup
servicesJson no JSON array of additional services, see below
packagesJson no JSON array of the parcels of a multi-parcel shipment, see below. Send only; read the parcels back through shipmentPackages
customsJson no Customs declaration, see below. Send only
shipToName … shipToPhone no Replace the ship-to address of the document
lengthCm, widthCm, heightCm no Parcel dimensions

Return label

POST .../labelRequests

{
  "externalReference": "SHOP-RET-4711",
  "source": "SHOP",
  "salesOrderNo": "SO-2026-01187",
  "providerCode": "DHL",
  "labelType": "Return"
}

A return label is built from the document alone. Weight, product code, services, dimensions and a ship-to override are not applied but rejected with an error: a silently ignored field would be worse than a clear refusal. The guard "one open label per document" does not apply, because a return label belongs to a document that already carries its outbound label. The provider must support returns, which supportsReturnLabels of the shipmentProviders endpoint tells you. The return shipment number comes back in trackingNo, the document as returnLabelBase64.

Additional services

servicesJson is a JSON array of objects with code, parameter1 and parameter2. The codes are carrier-specific and come from the respective connector. A connector evaluates the field only if it subscribes to the hub event OnApplyLabelServiceSelections. If it does not, servicesJson is silently ignored: the request ends with Completed, but the label does not carry the service.

Carrier providerCode Evaluates servicesJson Codes
DHL Paket DHL yes 17 codes
DPD (Webservice) DPD yes 6 codes
DPD Austria DPDAT yes 2 codes
Rhenus RHENUS yes 2 codes: fixed date, Avise
GLS GLS yes 8 codes
UPS UPS yes 5 codes
Hermes HERMES no no services per label
DHL Express DHLX no no services per label

Parameters are limited to 50 characters each. A code the connector does not know, a missing mandatory parameter or a parameter in the wrong format end the request with an error; no label is created with a service silently missing.

Amounts: DHL, GLS and UPS expect the point as decimal separator (2500.00). DPD and DPD Austria read the amount in the number format of the session, that is the language of the Microsoft Entra application. Create the application with language English and send 2500.00; a German session would read 2500.00 as 250000.

Multi-parcel shipments

When a shipment travels in several cartons, packagesJson describes them. The carrier then issues one tracking number per carton.

POST .../labelRequests

{
  "externalReference": "WMS-2026-000481",
  "source": "WMS",
  "salesOrderNo": "SO-2026-01187",
  "providerCode": "DHL",
  "packagesJson": "[{\"weightGrams\":2500,\"lengthCm\":40,\"widthCm\":30,\"heightCm\":20,\"packageContent\":\"Books\",\"referenceNo\":\"WMS-4711-1\",\"lines\":[{\"salesLineNo\":10000,\"itemNo\":\"1000\",\"quantity\":2}]},{\"weightGrams\":1800}]"
}
Field per parcel Required Meaning
weightGrams yes Weight of this carton
lengthCm, widthCm, heightCm no Dimensions of this carton
packagingMaterialCode no Packaging material
packageContent no Description of the content
referenceNo no Reference of the calling system for this carton
lines no What is in this carton: salesLineNo, itemNo, quantity

The order in the array is the parcel number: the first entry becomes parcel 1.

packagesJson is a write field. Reading a request returns it empty; the outcome lives in shipmentPackages, with the tracking number of every carton.

Weight and dimensions belong to the parcels

For a multi-parcel request the hub derives the weight and the dimensions of the shipment from the parcels. weightGrams, lengthCm, widthCm and heightCm on the request itself are therefore rejected rather than silently overruled. A return label takes no parcels.

The provider must support multi-parcel shipments, which supportsMultiColli of the shipmentProviders endpoint tells you. Otherwise the request ends with status = Error and a readable reason.

Reading the parcels

GET .../shipmentPackages?$filter=shipmentLabelEntryNo eq 801

Fields: id, entryNo, shipmentLabelEntryNo, packageNo, providerCode, salesOrderNo, salesShipmentNo, trackingNo, trackingUrl, shipmentNo, weightGrams, lengthCm, widthCm, heightCm, packagingMaterialCode, packageContent, referenceNo, isReturn, createdDateTime.

shipmentLabels shows the shipment as a whole; the tracking number per carton lives here only. The tracking history also keeps its events per carton, through trackingNo.

Customs data

For shipments outside the customs union merchantCENTRAL builds the customs declaration from the source document. When the calling system keeps its own customs data, or the document holds none, it brings the declaration along:

POST .../labelRequests

{
  "externalReference": "SHOP-2026-0815",
  "salesOrderNo": "SO-2026-01187",
  "providerCode": "DHL",
  "shipToCountryCode": "CH",
  "customsJson": "{\"exportType\":\"Commercial Goods\",\"exportDescription\":\"Books\",\"postalChargesAmount\":12.5,\"postalChargesCurrency\":\"EUR\",\"items\":[{\"itemNo\":\"1000\",\"description\":\"Book\",\"countryOfOriginCode\":\"DE\",\"hsCode\":\"49019900\",\"quantity\":2,\"unitPrice\":19.99,\"currencyCode\":\"EUR\",\"netWeightGrams\":500,\"grossWeightGrams\":550}]}"
}
Header field Meaning
exportType Other, Present, Commercial Sample, Document, Return of Goods, Commercial Goods
exportDescription Description of the export
postalChargesAmount, postalChargesCurrency Postage for customs clearance
permitNo, attestationNo, mrn Permit, attestation, movement reference number
shipperCustomsRef, consigneeCustomsRef Customs references of shipper and consignee
electronicExportNotification Electronic export notification
Field per item Required Meaning
description yes Description of the goods
quantity yes Quantity
itemNo no Item number
countryOfOriginCode no Country of origin
hsCode no Commodity code
unitPrice, currencyCode no Unit price and currency; the line amount is calculated
netWeightGrams, grossWeightGrams no Net and gross weight

Replace, do not mix

Items you send replace the ones derived from the document. A mixture of both would be a declaration nobody vouches for. Header fields you leave empty keep the value from the document: an empty field means "unchanged", not "clear it".

A return label takes no customs declaration. An item without a description or without a quantity is refused rather than handed to the carrier as an incomplete declaration.

Behaviour

  • Sync answers with the final outcome, typically one to three seconds.
  • Async answers immediately with status = Pending; the client reads the request later via GET …/labelRequests({id}) or ?$filter=externalReference eq '…'.
  • Input errors (missing reference, unknown document, invalid JSON, duplicate reference) return HTTP 400. The request is not stored.
  • Guards (module not active, provider not resolvable, label exists, hourly limit) store the request with status = Error and a readable errorMessage.
  • Carrier errors store the request with status = Error; errorMessage carries the core carrier message, shipmentLabelEntryNo points to the failed label.

Status values

Status Meaning
Pending Waiting for the job queue
Processing Being processed
Completed Label created, trackingNo filled
Error Rejected or failed at the carrier, see errorMessage
Cancelled Cancelled before processing

Reading labels

GET …/shipmentLabels?$filter=salesOrderNo eq 'SO-2026-01187'
GET …/shipmentLabels({id})

Fields: id, entryNo, providerCode, salesOrderNo, salesShipmentNo, status, shipmentNo, trackingNo, trackingUrl, labelFormat, weightGrams, productCode, referenceNo, deliveryStatus, lastTrackingUpdate, createdDateTime, cancelledDateTime, errorMessage.

Documents

GET …/shipmentLabelDocuments({id})

Returns labelBase64, returnLabelBase64, customsDocBase64 and codLabelBase64. The format is in labelFormat (PDF or ZPL). Only documents stored in Business Central are returned; a label that was never stored comes back empty.

Actions

POST …/shipmentLabels({id})/Microsoft.NAV.cancel
POST …/shipmentLabels({id})/Microsoft.NAV.refreshTracking

cancel cancels at the carrier and answers true. refreshTracking asks the carrier and answers with the new delivery status as text. Both actions check that the module is active and licensed.

Do not poll tracking in a tight loop

The carriers grant only a limited number of tracking calls per day and key, DHL for example 250. The scheduled tracking of the connector draws on the same budget. refreshTracking is therefore rate limited: Max Tracking Refreshes per Hour in the setup, 60 by default, 0 disables the guard. Once the window is exhausted the action fails without asking the carrier.

For a running status feed the business event DeliveryStatusChanged is the right way. It reports every change by itself and costs no tracking call at all.


Providers, products and pre-flight validation

GET .../shipmentProviders
GET .../shipmentProviders?$filter=enabled eq true

Fields: id, code, name, enabled, status, shippingAgentCode, defaultServiceCode, defaultLabelFormat, supportsReturnLabels, supportsCancellation, supportsTracking, supportsCustoms, supportsMultiColli, supportsManifesting, moduleCode, connectorVersion.

An external system can thus answer for itself which providers it may address and what each of them can do, instead of hard-coding codes.

Products

POST .../shipmentProviders({id})/Microsoft.NAV.getProducts

Returns [{"code":"V01PAK","name":"DHL Paket"}, ...]. A code from there fits the productCode field of a request, so an external system does not have to hard-code product codes.

The codes of the value added services for servicesJson still come from the documentation of the respective connector, see the table under Additional services.

Bulk archive

When several orders ship at once, one call fetches all their documents as a single ZIP file:

POST .../shipmentProviders({id})/Microsoft.NAV.getLabelArchive
{
  "salesOrderNosJson": "[\"SO-2026-01187\",\"SO-2026-01188\"]",
  "onlyNotExported": true,
  "markAsExported": true
}
{
  "documentCount": 5,
  "skippedCount": 0,
  "markedCount": 4,
  "fileName": "ShipmentLabels_20260909_144311.zip",
  "archiveBase64": "UEsDBBQ…"
}

The archive holds the shipping label, return label, COD label, customs document and QR code of every shipment, and one shipping label per carton for multi-parcel shipments. File names carry the tracking number.

Field Meaning
documentCount Number of documents in the archive
skippedCount Shipments that held no document; counted, not an error
markedCount Shipments stamped as fetched
fileName Suggested file name with a timestamp
archiveBase64 The ZIP file, Base64 encoded; empty when nothing was collected

Only shipments with status Created or Manifested are collected, and only those of the provider being addressed. A sales order named twice counts once.

Collecting in batches

Switch Effect
onlyNotExported Takes only shipments whose documents were never handed out
markAsExported Stamps the shipments of the finished archive as fetched

Both together give the usual routine: every run collects what has appeared since the last one. A second call with the same list of orders then comes back empty.

The stamp is set only once the archive exists. A run that hands out nothing does not claim its shipments were fetched.

The stamp is a filter, not a lock: without onlyNotExported documents that were already fetched come again. When a shipment was last handed out is shown as lastExportedDateTime on the shipmentLabels endpoint.

At most a hundred orders per call

Base64 grows the answer by a third. Past a hundred orders the call fails with a readable message instead of quietly shortening the list. Ask for larger runs in batches.

Pre-flight validation

POST .../shipmentProviders({id})/Microsoft.NAV.validateSalesOrder
{ "salesOrderNo": "SO-2026-01187" }

POST .../shipmentProviders({id})/Microsoft.NAV.validateSalesShipment
{ "salesShipmentNo": "PS-2026-00912" }

Response:

{ "valid": false, "errors": ["Ship-to postal code is missing.", "The parcel weight exceeds 31.5 kg."] }

The check creates nothing and never calls the carrier. Use it to check a document up front, for example while packing: the calling system reports what is missing right away instead of leaving a failed label behind.


End of day

At the end of the shipping day the created shipments are handed over to the carrier. One call does that and brings the accompanying document with it:

POST .../shipmentProviders({id})/Microsoft.NAV.closeOutDay
{}
{
  "manifestedCount": 29,
  "manifestedAt": "2026-09-09T15:14:04Z",
  "documentAvailable": true,
  "documentBase64": "JVBERi0…"
}
Field Meaning
manifestedCount Shipments handed over
manifestedAt When the close-out happened; empty when nothing was handed over
documentAvailable Whether a document came with it
documentBase64 The document, Base64 encoded

Every shipment of the provider with status Created is handed over; afterwards they read Manifested.

A quiet day is not an error

With nothing open the call answers manifestedCount = 0. A system that closes out on a schedule has to survive a day without shipments rather than handle an error.

No document is not an error either

Some carriers hand out no accompanying document, and test environments often lack it. The close-out itself has happened all the same: documentAvailable is false and the shipments are handed over.

The document of an earlier day comes from

POST .../shipmentProviders({id})/Microsoft.NAV.getManifestDocument
{ "manifestDate": "2026-09-09" }

The provider must support closing out, which supportsManifesting of the shipmentProviders endpoint tells you.


Tracking history

GET .../shipmentTrackingEvents?$filter=shipmentLabelEntryNo eq 801
GET .../shipmentTrackingEvents?$filter=trackingNo eq '00340434161094015902'

Where shipmentLabels tells you where a parcel stands, this endpoint tells you how it got there. Sorted by time, newest event first.

Fields: id, entryNo, shipmentLabelEntryNo, providerCode, trackingNo, eventDateTime, deliveryStatus, eventCode, description, location, countryCode, signedBy, recordedDateTime.

{
  "entryNo": 4711,
  "shipmentLabelEntryNo": 801,
  "providerCode": "DHL",
  "trackingNo": "00340434161094015902",
  "eventDateTime": "2026-09-09T07:12:00Z",
  "deliveryStatus": "Delivered",
  "eventCode": "DELIVERY",
  "description": "The shipment has been delivered",
  "location": "Bonn",
  "countryCode": "DE",
  "signedBy": "M. Musterfrau"
}

deliveryStatus is carrier-neutral and uses the same scale as shipmentLabels. eventCode is the carrier's own code, for support cases with the carrier. It is stored in upper case, so compare it case-insensitively.

Poll this endpoint, not refreshTracking

The history is a by-product of the tracking that the carrier module runs anyway. Reading it costs no carrier call and is not rate limited. refreshTracking asks the carrier and is.

Only what the carrier reports

A history is as complete as the carrier's answer. If tracking is not set up for a provider the list stays empty, which is not an error. Whether a provider supports tracking is shown in supportsTracking of the shipmentProviders endpoint.

The history is written by the carrier modules only. Through the API it is read-only.


Business events

For Power Automate and Dataverse the app provides three business events in the category merchantCENTRAL Shipment. The payload carries identifiers, numbers and states only; whoever needs addresses reads them through the API with the rights of their own connection.

Event Payload
ShipmentLabelCreated labelId, labelEntryNo, providerCode, salesOrderNo, salesShipmentNo, trackingNo
ShipmentLabelFailed labelId, labelEntryNo, providerCode, salesOrderNo, salesShipmentNo, errorMessage
DeliveryStatusChanged labelId, labelEntryNo, providerCode, trackingNo, oldStatus, newStatus

The events are raised only while the module is active and licensed.


Permissions

Permission set For whom
Integration API - Client The Entra application of the external system. Create and read requests, read labels and documents, execute the API pages. Add D365 BASIC, D365 READ, merchantCENTRAL User and the user set of the respective carrier, see Setup.
Integration API - User Shipping staff: monitor, retry and cancel requests.
Integration API - Admin Setup, wizard, job queue.