For the complete documentation index, see llms.txt. This page is also available as Markdown.

Shipping / Carrier

Connect a carrier: carrier types, label creation, tracking and write-back.

A shipping app connects a carrier — DHL, GLS, DPD, UPS, Deutsche Post Internetmarke — to the ERP. It turns a shipment into a label and a tracking number.

Read App Backends first; this page only covers what is specific to shipping.

The three levels

Getting these apart is the single most important thing:

Level
German
Who creates it

Carrier type

Versendertyp

Your app, on install — this is your backend

Carrier

Versender

The customer, in ERP settings

Shipping method

Versandart

The customer, assigned to a carrier

Your app registers the carrier type and nothing else. The customer then creates carriers that use your type, and shipping methods on those carriers. Your app only ever activates or deactivates them — never creates or deletes them.

Registering the carrier type

There is no framework helper for this — it's a plain API call in your migration. Check first whether it already exists, then create it:

await app.erp.fetch(`/community/${app.version}/vds/carrier-type`, {
  method: 'POST',
  body: JSON.stringify({
    label: 'DHL App',
    description: 'DHL Anbindung',
    appId: app.client.appIdentifier,
    active: true,
    backendType: 'APP',
    canPrintLabel: true,
    createShipmentSyncUrl: `${baseUrl}/api/shipment/create`,
    createLabelSyncUrl: `${baseUrl}/api/label/create`,
    trackingUrlTemplate:
      'https://www.dhl.de/de/privatkunden/dhl-sendungsverfolgung.html?piececode=TRACKING_NUMBER',
  }),
});

The two …SyncUrl fields are the important part: the ERP calls these URLs synchronously. Your app is not polling — it is being called.

Register two carrier types: production and one whose label ends in (Sandbox). That is how the apps model sandbox mode, and how they detect it at runtime.

The two-phase flow

Phase 1 — shipment created → createShipmentSyncUrl

The ERP posts the shipment. Note that the shipment property arrives as a JSON string, not an object:

Your job here is not to talk to the carrier. It is to resolve the configuration and stamp it onto the shipment and every parcel:

Then PUT /vds/shipment/{id} to persist it.

This snapshot is deliberate: the label phase reads settings from the parcel, not from current configuration. A label reprinted next month must use the settings that were active when the shipment was created.

Phase 2 — label requested → createLabelSyncUrl

Skip parcels that are already COMPLETED, then per parcel:

1

Read configuration

Read config from parcel.packageParameter — never reload it.

2

Build the carrier request

Map addresses and build the carrier request.

3

Call the carrier API

Call the carrier API.

4

Upload the label

Upload the label to the DMS.

5

Write the result back

Write the result back to the parcel.

parcelState is one of OPEN, REQUESTED, COMPLETED, ERROR, MANUAL, CANCELLED.

Where things are stored

Thing
Location

Tracking number

parcel.trackingNumber

Label file

DMS resource, attributed refType: 'PARCEL', refId: <parcel id>

Carrier reply summary

parcel.carrierResponse

Carrier validation messages

parcel.validationErrors

Tracking events

parcel.trackingDetails[], normalised to CREATED, PICKUP, TRANSIT, DELIVERED, RETURN, EXCEPTION

The label is never stored on the parcel — it goes to the DMS and is linked by attribution. Customs documents are uploaded the same way.

Credentials

The ERP carrier record holds no credentials — it has only a label, its type reference and free-form parameters. Store credentials in your app, keyed by carrierId, so one tenant can run several accounts of the same carrier.

Sandbox and production credentials are separate. Since the sandbox flag round-trips as a string, coerce it explicitly:

Install and uninstall

Because carriers and shipping methods belong to the customer, install is a two-step conversation:

1

Activate carrier types

Run migrations, then activate your carrier types (infrastructure — always safe).

2

Let the user select what to activate

Read back the carriers and shipping methods that use your types, and let the user pick which to activate.

Activate carriers before shipping methods, so a method is never briefly active without an active carrier. On uninstall, reverse it: shipping methods → carriers → carrier types. Deactivate only — deleting would destroy customer configuration and prevent selective reactivation.

What to watch out for

Field lengths. Carriers reject overlong fields rather than truncating them. DHL address fields are typically 35 characters, not the 50 the schema suggests — 50 applies to dangerous goods only. Truncate deliberately, and make it switchable per carrier. Never truncate an email address: a cut address fails format validation, so omit the field instead.

Country codes differ per carrier. Some want ISO-2, some ISO-3. Do not derive one from the other by slicing — AUT sliced to two characters is AU, which is Australia. Map explicitly.

Weight and dimension units. Read the unit from the parcel instead of assuming. Carriers expect wildly different units — kilograms, pounds, even 10-gram increments. Zero weight usually needs a minimum substituted.

Customs is product-dependent. Only certain international products accept customs data. An incomplete customs configuration can silently produce a label with no customs information at all — validate before sending.

Fail onto the parcel, not the request. A carrier error is normal operation, not a server error. Set parcelState = 'ERROR' with validationErrors so the user sees what went wrong on the shipment, and keep the endpoint's response successful.

Cancellation varies. Some carriers offer a real void/cancel API; others have none, in which case you can only delete the label document and reset the parcel state.

Tracking URL placeholders. trackingUrlTemplate uses TRACKING_NUMBER, and some carriers additionally need POSTAL_CODE. If a placeholder in the template has no value at runtime, the ERP shows no tracking link at all rather than a broken one.

Last updated

Was this helpful?