> For the complete documentation index, see [llms.txt](https://developer.vario-software.de/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.vario-software.de/documentation/apps/app-backends/shipping-carrier.md).

# 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**.

{% hint style="info" %}
Read [App Backends](/documentation/apps/app-backends.md) first; this page only covers what is specific to shipping.
{% endhint %}

## 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:

```javascript
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.

{% hint style="info" %}
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.
{% endhint %}

{% hint style="warning" %}
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.
{% endhint %}

## 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:

```javascript
const shipment = JSON.parse(body.shipment);
```

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**:

```javascript
const settings = await getDeliveryMethodSettings({ 
  carrierId: data.carrier.id, 
  deliveryMethodId: data.deliveryMethodId, 
}); 

data.shipmentParameter = settings; 
data.parcels.forEach(parcel => { parcel.packageParameter = settings; });
```

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:

{% stepper %}
{% step %}

## Read configuration

Read config from `parcel.packageParameter` — never reload it.
{% endstep %}

{% step %}

## Build the carrier request

Map addresses and build the carrier request.
{% endstep %}

{% step %}

## Call the carrier API

Call the carrier API.
{% endstep %}

{% step %}

## Upload the label

Upload the label to the **DMS**.
{% endstep %}

{% step %}

## Write the result back

Write the result back to the parcel.
{% endstep %}
{% endstepper %}

```javascript
parcel.parcelState = 'COMPLETED'; 
parcel.trackingNumber = response.shipmentNo; 
parcel.carrierResponse = /* small summary only — see below */; 
parcel.validationErrors = mapValidationMessages(response.validationMessages ?? []); 

await app.erp.fetch(`/vds/shipment/${shipment.id}/parcels/${parcel.id}`, { 
  method: 'PUT', 
  body: 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.

{% hint style="warning" %}
`**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.
{% endhint %}

## 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:

```javascript
this.sandbox = String(sandbox) === 'true';
```

{% hint style="warning" %}
`"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.
{% endhint %}

## Install and uninstall

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

{% stepper %}
{% step %}

## Activate carrier types

Run migrations, then activate your carrier types (infrastructure — always safe).
{% endstep %}

{% step %}

## 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.
{% endstep %}
{% endstepper %}

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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developer.vario-software.de/documentation/apps/app-backends/shipping-carrier.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
