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.
The three levels
Getting these apart is the single most important thing:
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.
The URLs are built from your app's domain at install time. If the domain changes, they are stale and the ERP calls the wrong host. Every existing shipping app ships a later migration that rewrites createShipmentSyncUrl and createLabelSyncUrl for that reason — plan for it.
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:
parcelState is one of OPEN, REQUESTED, COMPLETED, ERROR, MANUAL, CANCELLED.
Where things are stored
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.
**carrierResponse**** is capped at 255 characters.** Do not put the raw carrier response in it — the label, customs documents and validation messages have their own homes. Store a small summary (tracking number, document id) and truncate defensively.
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:
"false" is a truthy string. A restored session with sandbox: "false" that is not coerced will send production credentials to the sandbox endpoint — or worse, the reverse.
Install and uninstall
Because carriers and shipping methods belong to the customer, install is a two-step conversation:
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?