# Angebot und Auftrag in einem Ablauf

Dieser Leitfaden führt Anfrage für Anfrage durch die Uniorder-API (`/api/v1/uniorder/...`), in der Reihenfolge, in der eine Integration aufgebaut wird: anmelden, Angebot anfragen, zur gewählten `rate_id` anlegen, Label drucken, Auftrag abrufen, verfolgen und stornieren sowie Sendungen im Stapel verarbeiten. Ein Angebot listet alle Wege, auf denen das Konto ein Paket versenden kann: die Zustellung durch das Unternehmen selbst und auf Anforderung jeden Label-Service der Carrier. Ein Auftrag mit einer `rate_id` wird für diesen Service angelegt: ein Zustellauftrag oder ein Label-Auftrag, dessen Label beim angefragten Carrier-Service gekauft wird. Er richtet sich an Entwickler von Onlineshops, Auftragsverwaltungssystemen und ERP-Systemen, die über ein Unternehmenskonto versenden.

## 1. Was Sie damit bauen können

Die folgenden Beispiele beziehen sich auf ein Unternehmen: **Fleurs du Plateau**, ein Blumengeschäft in 4500 Rue Saint-Denis, Montreal (H2J 2L3), das Blumensträuße online verkauft. Ein typisches Paket ist ein Karton mit 1,2 kg und 40 × 25 × 25 cm an Jane Recipient, 6841 Rue Saint-Denis, Montreal (H2S 2S3), unter dem Webauftrag `WEB-10045`.

- **Ein Checkout mit allen Versandoptionen.** Der Shop fragt das Paket einmal an, zeigt die lokale Zustellung am selben Tag neben jedem Label-Service der Carrier des Kontos an, jeweils mit Preis, und legt den Auftrag dann zur vom Kunden gewählten Option an.
- **Automatischer Labeldruck.** Sobald der Auftrag angelegt ist, lädt der Shop das Label-PDF herunter und sendet es an den Drucker des Packplatzes, unabhängig davon, ob das Paket vom Unternehmen oder von einem Carrier zugestellt wird.
- **Eine Auftragsseite mit aktuellem Tracking.** Die Auftragsseite des Kunden zeigt den Status und die Ereigniszeitleiste der Sendung, nach der Zustellung des Blumenstraußes auch den Zustellnachweis.
- **Ein nächtlicher Stapel aus dem ERP.** Die Großhandelsaufträge des Tages werden in einem eingereihten Job mit bis zu 500 Zeilen angefragt und angelegt, und jedes Ergebnis wird über `reference` seiner Auftragszeile zugeordnet.

## 2. Was dieser Leitfaden abdeckt

Dies ist der Schritt-für-Schritt-Leitfaden zur Uniorder-API. Die Übersicht darüber, was Uniorder bietet und warum, finden Sie unter **Uniorder: eine API für jede Sendung**; dieser Leitfaden enthält die Anfragen, Antworten und Prüfungen für jeden Aufruf.

Uniorder ist der empfohlene einheitliche Einstiegspunkt für neue Integrationen, die Pakete per lokaler Zustellung oder per Carrier-Label versenden: Es ersetzt getrennte Aufrufe der API für lokale Zustellung und der API für Carrier-Labels durch eine einzige Anfragestruktur. Die bisherigen Endpunkte, die unter **Abholung und Zustellung (eigene Flotte)** und **Versandetiketten** beschrieben sind, bleiben verfügbar und unverändert. Uniorder gilt nicht für Versandservices, die ein Kundenkonto bucht, und nicht für Lager- und Versandaufträge; verwenden Sie dafür **Versandservice** und **Lagerung und Versand**.

## 3. Bevor Sie beginnen

- **Konto.** Verwenden Sie ein Unternehmenskonto (Kundenkonto) oder ein Mitarbeiterkonto des Unternehmens mit API-Berechtigung. Auch ein Kundenkonto des Unternehmens kann Uniorder aufrufen und erhält Angebote und Rechnungen stets für sich selbst. Das Anlegen eines Zustellauftrags erfordert die Berechtigung zur Auftragserteilung.
- **Kunden.** Ein Unternehmens- oder Mitarbeiterkonto kann mit `customer_id` oder `customer_code` im Angebot für einen seiner Kunden anfragen und bestellen; die `rate_id` trägt dann diesen Kunden, und der Preis richtet sich nach dem Tarifplan des Kunden.
- **Label-Services.** Um `label_service`-Tarife zu erhalten, muss für das Konto (oder den angegebenen Kunden) mindestens ein Label-Carrier-Konto konfiguriert sein.
- **Testdaten.** Verwenden Sie für `self_delivery`-Tarife eine Adresse innerhalb des Zustellgebiets des Unternehmens und Testreferenzen wie `WEB-10045`, die anschließend storniert werden können.
- **Tokens.** Fordern Sie das Zugriffstoken von Ihrem Server an und bewahren Sie es dort auf. Senden Sie es niemals an einen Browser oder eine mobile App.
- **Platzhalter.** Ersetzen Sie `YOUR_HOST` durch den API-Host Ihrer Umgebung und `ACCESS_TOKEN` durch das Token aus Schritt 4.

## 4. Anmelden

Jeder Uniorder-Aufruf erfolgt im Namen eines Kontos. Melden Sie sich einmal von Ihrem Server aus an, speichern Sie das zurückgegebene Token und senden Sie es bei jeder Anfrage mit.

**REST:** `POST /api/v1/user/login` — [REST-Handbuch](/api/documentation#/paths/v1-user-login/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/user/login \
  -H "Content-Type: application/json" \
  -d '{"email":"orders@fleursduplateau.example","password":"your_password"}'
```

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
```

- `access_token`: Setzen Sie es in den Header jeder weiteren Anfrage:

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL verwendet denselben Header auf `POST /api/graphql`.

**GraphQL:** `userLogin` ([GraphQL-Handbuch](/api/graphql/documentation#/user/userLogin))

**Überprüfung:** Die Anmeldung liefert `access_token`. Nachfolgende Anfragen ohne dieses Token liefern `401`.

## 5. Angebot für jeden Service anfragen

Das Angebot listet alle Wege, auf denen das Paket versendet werden kann, jeweils mit einem Preis und einer `rate_id`. Der Checkout zeigt die Tarife als Optionen an; es wird nichts angelegt oder gebucht.

**REST:** `POST /api/v1/uniorder/rate` — [REST-Handbuch](/api/documentation#/paths/v1-uniorder-rate/post)

Absender und Empfänger sind vollständige Adressen; nur `from_address_2` und `to_address_2` sind optional. Jedes Paket benötigt `weight`, `length`, `width` und `height`. Setzen Sie `quote_labels` auf `true`, um die Label-Services der Carrier hinzuzufügen; Namen und Telefonnummern beider Seiten sind dann erforderlich. Ein Unternehmens- oder Mitarbeiterkonto kann mit `customer_id` oder `customer_code` für einen seiner Kunden anfragen. Ein Lieferzeitfenster (`time_window_start`, `time_window_end`, Format `YYYY-MM-DD HH:MM:SS`) wird berücksichtigt, wenn der Preis davon abhängt.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "quote_labels": true,
    "packages": [{
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

`weight_unit`: `1` g, `2` kg, `3` oz, `4` lb. `dimension_unit`: `1` mm, `2` cm, `3` m, `4` in.

```json
{
  "result": true,
  "rates": [
    {
      "rate_id": "eyJpdiI6Ik1rT2Z...",
      "type": "self_delivery",
      "channel_id": null,
      "shipping_price": "14.60",
      "currency": "CAD",
      "price_details": { "shipping_fee": 12.92, "sub_total": "12.92" },
      "warning": null
    },
    {
      "rate_id": "eyJpdiI6IlpxR0...",
      "type": "label_service",
      "shipping_price": "18.40",
      "currency": "CAD",
      "shipping_method_id": 72,
      "shipping_method_name": "UPS",
      "carrier_name": "ups",
      "service_code": "ups_standard",
      "service_name": "UPS STANDARD",
      "transit_days": 3
    }
  ],
  "errors": []
}
```

- `type` `self_delivery`: Zustellung durch das Unternehmen. Höchstens einer pro Angebot.
- `type` `label_service`: einer pro Service jedes Label-Kontos. Zeigen Sie dem Kunden `service_name`, `shipping_price` und `transit_days` an.
- `errors` listet, was nicht angefragt werden konnte, mit dem jeweiligen `type`. Eine Adresse außerhalb des Zustellgebiets ist ein Fehler vom Typ `self_delivery` mit dem Code `OUT_OF_DELIVERY_AREA`; zeigen Sie dann nur die Label-Services an.
- `rate_id` ist 30 Minuten gültig und nur für das Konto, das das Angebot angefragt hat. Bewahren Sie sie mit der Checkout-Sitzung auf.
- `result` ist `true`, wenn mindestens ein Tarif gefunden wurde.

**GraphQL:** `uniorderRate` ([GraphQL-Handbuch](/api/graphql/documentation#/orders/uniorderRate)). Die Antwort ist ein JSON-Skalar, daher hat die Operation kein Selection-Set.

```graphql
mutation QuoteBouquet($packages: [Json]!) {
  uniorderRate(
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    quote_labels: true
    packages: $packages
  )
}
```

Variablen:

```json
{ "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**Überprüfung:** `rates` enthält für eine Adresse im Gebiet einen `self_delivery`-Tarif und mit `quote_labels` einen `label_service`-Tarif pro Carrier-Service. Es wird nichts angelegt.

## 6. Auftrag zum gewählten Tarif anlegen

Wenn der Kunde bezahlt, legt der Shop den Auftrag mit der `rate_id` der gewählten Option und derselben Sendung an. Die `rate_id` bestimmt den Service; kein anderer Teil der Anfrage wählt ihn aus.

**REST:** `POST /api/v1/uniorder` — [REST-Handbuch](/api/documentation#/paths/v1-uniorder/post)

Senden Sie bei jedem Anlegen einen `Idempotency-Key`-Header, eindeutig pro Auftrag. Eine Wiederholung mit demselben Schlüssel und demselben Body liefert die erste Antwort mit `replayed` `true` und legt keinen zweiten Auftrag an.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: checkout-10045" \
  -d '{
    "rate_id": "eyJpdiI6Ik1rT2Z...",
    "ref": "WEB-10045",
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_email": "jane@example.com",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "delivery_instruction": "Ring the bell at the side door.",
    "packages": [{
      "ref": "WEB-10045-1",
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

Ein `self_delivery`-Tarif legt einen Zustellauftrag an. Bei `type` `D` ist der Empfänger der Halt; setzen Sie `need_pick_up` auf `1`, damit das Paket beim Absender abgeholt wird. Bei `type` `P` ist der Absender der Halt.

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800001"],
  "shipping_price": "14.60",
  "quoted_price": "14.60",
  "currency": "CAD"
}
```

Ein `label_service`-Tarif legt einen Label-Auftrag an und kauft das Label beim angefragten Carrier-Service. `type` muss `D` sein, und `from_name`, `from_telephone`, `to_name` und `to_telephone` sind erforderlich. Hätte der Kunde UPS STANDARD gewählt, lautete die Antwort:

```json
{
  "result": true,
  "type": "label_service",
  "id": 123457,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800002"],
  "shipping_price": "18.40",
  "quoted_price": "18.40",
  "currency": "CAD",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456784",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `id`: Speichern Sie sie mit dem Webauftrag; jeder spätere Aufruf verwendet sie.
- `tracking_numbers`: die eigenen Tracking-Nummern der Sendung, eine pro Paket.
- `shipping_price`: der berechnete Preis. Der Auftrag wird beim Anlegen bepreist; `quoted_price` ist der Preis im Angebot. Beide können voneinander abweichen.
- `label.main_tracking_number` und `label.shipping_label` (nur Label-Auftrag): die Tracking-Nummer des Carriers und das Label-PDF in Base64.
- `result` `false` mit dem Code `LABEL_PURCHASE_FAILED` (nur Label-Auftrag): Der Auftrag existiert, hat aber kein Label. Behalten Sie die `id` und fahren Sie mit Schritt 11 fort.

**GraphQL:** `uniorderCreate` ([GraphQL-Handbuch](/api/graphql/documentation#/orders/uniorderCreate))

```graphql
mutation CreateBouquetOrder($packages: [Json]!) {
  uniorderCreate(
    rate_id: "eyJpdiI6Ik1rT2Z..."
    ref: "WEB-10045"
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    packages: $packages
  )
}
```

Variablen:

```json
{ "packages": [{ "ref": "WEB-10045-1", "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**Überprüfung:** `result` ist `true` und `id` ist gesetzt. Eine abgelaufene oder einem anderen Konto gehörende `rate_id` liefert `400` mit dem Code `RATE_ID_INVALID`, und es wird nichts angelegt.

## 7. Label drucken

Der Packplatz druckt das Label, sobald der Auftrag existiert. Derselbe Aufruf liefert bei einem Zustellauftrag das eigene Label des Unternehmens und bei einem Label-Auftrag das gekaufte Carrier-Label.

**REST:** `GET /api/v1/uniorder/{orderId}/label` — [REST-Handbuch](/api/documentation#/paths/v1-uniorder-orderId--label/get)

```bash
curl "https://YOUR_HOST/api/v1/uniorder/123456/label?hide_sender_address=0" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "format": "pdf",
  "tracking_numbers": ["SR26092800001"],
  "pdf_data": "JVBERi0xLjQK..."
}
```

- `pdf_data`: das Label-PDF in Base64. Dekodieren Sie es und senden Sie die Datei an den Drucker.
- `hide_sender_address`, `hide_receiver_address` (`1` zum Ausblenden): gelten für das eigene Label des Unternehmens bei einem Zustellauftrag.
- `label_status` (Label-Auftrag): `ready`, wenn die Datei geliefert wird. Hat der Carrier die Datei noch nicht erstellt, lautet die Antwort `200` mit `result` `false` und `label_status` `pending`; fordern Sie das Label später erneut an.
- Dieser Aufruf kauft nie ein Label: Ein noch nicht gekauftes Label liefert `409` mit dem Code `LABEL_PURCHASE_FAILED`. Kaufen Sie es mit Schritt 11.

**GraphQL:** `uniorderLabel` ([GraphQL-Handbuch](/api/graphql/documentation#/orders/uniorderLabel))

```graphql
query {
  uniorderLabel(order_id: 123456, hide_sender_address: 0)
}
```

**Überprüfung:** `result` ist `true`, und das dekodierte `pdf_data` lässt sich als PDF öffnen und zeigt die Tracking-Nummer des Auftrags.

## 8. Auftrag abrufen

Der Shop ruft den Auftrag ab, um seinen Status, seine Adressen und Pakete auf der Auftragsseite oder in einer Kundendienstansicht anzuzeigen.

**REST:** `GET /api/v1/uniorder/{orderId}` — [REST-Handbuch](/api/documentation#/paths/v1-uniorder-orderId/get)

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "order_type": "D",
  "status": "pending",
  "created_at": "2026-10-02 09:14:05",
  "time_window_start": "2026-10-02 13:00:00",
  "time_window_end": "2026-10-02 17:00:00",
  "from": { "name": "Fleurs du Plateau", "address": "4500 Rue Saint-Denis", "city": "Montreal", "postcode": "H2J2L3" },
  "to": { "name": "Jane Recipient", "address": "6841 Rue Saint-Denis", "address_2": "Apt 2", "city": "Montreal", "postcode": "H2S2S3" },
  "packages": [
    { "id": 998877, "ref": "WEB-10045-1", "tracking_number": "SR26092800001", "weight": 1.2 }
  ],
  "shipping_price": "14.60",
  "currency": "CAD"
}
```

- `type`: `self_delivery` oder `label_service`; die übrigen Felder haben für beide dieselbe Struktur.
- `status`: `pending`, `in_transit`, `out_for_pickup`, `out_for_delivery`, `ready_for_self_pickup`, `delivered`, `exception` oder `cancelled` bei einem Zustellauftrag und `label_pending`, `label_purchased` oder `cancelled` bei einem Label-Auftrag.
- `label` (nur Label-Auftrag): der Carrier, der Service, `carrier_tracking_numbers` und `label_status` (`not_purchased`, `pending`, `ready` oder `failed`).

**GraphQL:** `uniorder` ([GraphQL-Handbuch](/api/graphql/documentation#/orders/uniorder))

```graphql
query {
  uniorder(order_id: 123456)
}
```

**Überprüfung:** Der Auftrag liefert seinen `status` und seine `packages`, und `ref` stimmt mit dem Webauftrag überein.

## 9. Auftrag verfolgen

Die Auftragsseite zeigt die Zeitleiste der Sendung. Lesen Sie sie, wenn der Kunde die Seite öffnet, oder halten Sie sie über Webhooks aktuell.

**REST:** `GET /api/v1/uniorder/{orderId}/tracking` — [REST-Handbuch](/api/documentation#/paths/v1-uniorder-orderId--tracking/get)

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456/tracking \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "status": "delivered",
  "tracking_numbers": ["SR26092800001"],
  "events": [
    { "code": "delivered", "description": "Delivered", "location": "Montreal", "time": "2026-10-02 15:42:10", "time_zone": "America/Toronto", "source": "shipper" }
  ],
  "proofs": [
    { "type": "photo", "url": "https://YOUR_HOST/storage/pod/123456.jpg", "uploaded_at": "2026-10-02 15:42:08" }
  ]
}
```

- `events`: die Zeitleiste, die neuesten zuerst, jeweils mit `code`, `description`, `location` und Zeit.
- `proofs`: Dateien des Zustellnachweises. Zeigen Sie sie an, sobald `status` `delivered` ist.
- `carrier` (nur Label-Auftrag): Name des Carriers, Tracking-Nummer und Tracking-Link (`tracking_url`).

**GraphQL:** `uniorderTracking` ([GraphQL-Handbuch](/api/graphql/documentation#/orders/uniorderTracking))

```graphql
query {
  uniorderTracking(order_id: 123456)
}
```

**Überprüfung:** Der Tracking-Aufruf liefert `result` `true`, den `status` des Auftrags und seine `events`.

## 10. Auftrag stornieren

Storniert der Kunde den Webauftrag, storniert der Shop die Sendung mit demselben Aufruf für einen Zustellauftrag und einen Label-Auftrag. Ein Label wird zuerst beim Carrier annulliert.

**REST:** `POST /api/v1/uniorder/{orderId}/cancel` — [REST-Handbuch](/api/documentation#/paths/v1-uniorder-orderId--cancel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123456/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cancel-10045" \
  -d '{}'
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "already_cancelled": false,
  "message": "The order has been cancelled."
}
```

- `already_cancelled`: `true`, wenn der Auftrag bereits vor diesem Aufruf storniert war. Behandeln Sie dies als Erfolg.
- Wird der Auftrag nicht storniert, lautet die Antwort `409`, und der Auftrag bleibt unverändert: `ORDER_STATUS_NOT_CANCELLABLE` (zu spät für eine Stornierung), `ORDER_CANCEL_REFUSED` (derzeit nicht stornierbar) oder `LABEL_CANCEL_FAILED` (der Carrier hat das Label nicht annulliert). Lassen Sie den Webauftrag offen und bearbeiten Sie die Sendung manuell.

**GraphQL:** `uniorderCancel` ([GraphQL-Handbuch](/api/graphql/documentation#/orders/uniorderCancel))

```graphql
mutation {
  uniorderCancel(order_id: 123456)
}
```

**Überprüfung:** `result` ist `true`. Eine erneute Stornierung desselben Auftrags liefert `already_cancelled` `true`.

## 11. Label später kaufen (nur nach LABEL_PURCHASE_FAILED)

Dieser Schritt gilt nur für einen Label-Auftrag, dessen Anlage mit `LABEL_PURCHASE_FAILED` beantwortet wurde. Die Antwort war `200` mit `result` `false`, dem Code `LABEL_PURCHASE_FAILED` und der Auftrags-`id`: Der Auftrag bleibt ohne Label erhalten. Senden Sie den Auftrag nicht erneut; kaufen Sie das Label für diesen Auftrag.

**REST:** `POST /api/v1/uniorder/{orderId}/label` — [REST-Handbuch](/api/documentation#/paths/v1-uniorder-orderId--label/post)

Die Anlage, bei der der Labelkauf fehlschlug, lieferte:

```json
{
  "result": false,
  "code": "LABEL_PURCHASE_FAILED",
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "tracking_numbers": ["SR26092800003"],
  "quoted_price": "18.40",
  "message": "The quoted service is not offered for this shipment."
}
```

Label für den Auftrag `123458` kaufen:

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123458/label \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: label-10046" \
  -d '{}'
```

Das Label wird bei dem Service gekauft, der beim Anlegen des Auftrags gewählt wurde. Um bei einem anderen Service desselben Kontos zu kaufen, senden Sie im Body eine neue `label_service`-`rate_id` aus Schritt 5 (`{"rate_id": "eyJpdiI6IlpxR0..."}`). Ein bereits gekauftes Label wird zurückgegeben und nicht erneut gekauft.

```json
{
  "result": true,
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "shipping_price": "18.40",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456791",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `label.shipping_label`: das Label-PDF in Base64; drucken Sie es wie in Schritt 7.
- Erneut `result` `false` mit `LABEL_PURCHASE_FAILED`: Der Carrier hat wieder abgelehnt. Versuchen Sie es später erneut oder kaufen Sie mit einer neuen `rate_id` bei einem anderen Service.

**GraphQL:** `uniorderPurchaseLabel` ([GraphQL-Handbuch](/api/graphql/documentation#/orders/uniorderPurchaseLabel))

```graphql
mutation {
  uniorderPurchaseLabel(order_id: 123458)
}
```

**Überprüfung:** `result` ist `true` und `label.shipping_label` enthält das PDF, oder `label.label_status` ist `pending`, während der Carrier die Datei erstellt.

## 12. Stapel

Stapel fragen viele Sendungen in einem Aufruf an oder legen sie an, zum Beispiel die Großhandelsaufträge des ERP. Jede Zeile durchläuft den Einzelaufruf und liefert, was dieser liefern würde; eine fehlgeschlagene Zeile hält die anderen Zeilen nicht auf.

**REST:** `POST /api/v1/uniorder/rate/batch` — [REST-Handbuch](/api/documentation#/paths/v1-uniorder-rate-batch/post) · `POST /api/v1/uniorder/batch` — [REST-Handbuch](/api/documentation#/paths/v1-uniorder-batch/post)

Bis zu 20 Zeilen pro Aufruf, beantwortet in derselben Antwort: `shipments` für den Angebotsstapel, `orders` für den Anlagestapel. Jede Zeile hat dieselben Felder wie der Einzelaufruf sowie eine optionale `reference`, die mit ihrem Ergebnis zurückgegeben wird.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/batch \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: erp-2026-10-01" \
  -d '{
    "orders": [
      {
        "reference": "ERP-7781",
        "rate_id": "eyJpdiI6Ik1rT2Z...",
        "ref": "ERP-7781",
        "from_name": "Fleurs du Plateau",
        "from_telephone": "5145550100",
        "from_address": "4500 Rue Saint-Denis",
        "from_city": "Montreal",
        "from_province": "QC",
        "from_country": "CA",
        "from_postcode": "H2J2L3",
        "to_name": "Jane Recipient",
        "to_telephone": "5145550199",
        "to_address": "6841 Rue Saint-Denis",
        "to_city": "Montreal",
        "to_province": "QC",
        "to_country": "CA",
        "to_postcode": "H2S2S3",
        "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }]
      }
    ]
  }'
```

```json
{
  "result": true,
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `results`: eines pro Zeile, mit dem `index` der Zeile, ihrer `reference` sowie dem `status` und `body`, die der Einzelaufruf liefern würde. Ordnen Sie jedes Ergebnis über `reference` seiner Auftragszeile zu.

**REST:** `POST /api/v1/uniorder/rate/batch-async` — [REST-Handbuch](/api/documentation#/paths/v1-uniorder-rate-batch-async/post) · `POST /api/v1/uniorder/batch-async` — [REST-Handbuch](/api/documentation#/paths/v1-uniorder-batch-async/post) · `GET /api/v1/uniorder/jobs/{jobId}` — [REST-Handbuch](/api/documentation#/paths/v1-uniorder-jobs-jobId/get)

Bis zu 500 Zeilen, eingereiht als ein Job. Der Aufruf liefert eine `job_id`; lesen Sie den Job, bis `status` den Wert `done` hat, und lesen Sie dann `results`. Wird derselbe Stapel erneut gesendet, während der erste noch eingereiht ist, wird der erste Job mit `duplicate` `true` zurückgegeben.

```bash
curl https://YOUR_HOST/api/v1/uniorder/jobs/8813 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "job_id": 8813,
  "kind": "create",
  "status": "done",
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `status`: `queued`, `done` oder `failed` mit einer `message`, wenn der Job nicht verarbeitet werden konnte.
- Ein Job wird einmal ausgeführt und nicht wiederholt. Eine `rate_id`, die abläuft, bevor ihre Zeile ausgeführt wird, liefert für diese Zeile `RATE_ID_INVALID`; senden Sie den Anlage-Job kurz nach Abschluss des Angebots-Jobs.

**GraphQL:** `uniorderRateBatch` ([GraphQL-Handbuch](/api/graphql/documentation#/orders/uniorderRateBatch)) · `uniorderCreateBatch` ([GraphQL-Handbuch](/api/graphql/documentation#/orders/uniorderCreateBatch)) · `uniorderRateBatchAsync` ([GraphQL-Handbuch](/api/graphql/documentation#/orders/uniorderRateBatchAsync)) · `uniorderCreateBatchAsync` ([GraphQL-Handbuch](/api/graphql/documentation#/orders/uniorderCreateBatchAsync)) · `uniorderJob` ([GraphQL-Handbuch](/api/graphql/documentation#/orders/uniorderJob))

```graphql
query {
  uniorderJob(job_id: 8813)
}
```

**Überprüfung:** Ein Stapel liefert ein Ergebnis pro Zeile; ein asynchroner Job erreicht `status` `done`.

## 13. Fehlerbehandlung

| Situation | HTTP-Status | Code | Vorgehen der Integration |
|---|---|---|---|
| Ein Pflichtfeld fehlt oder ist fehlerhaft | 400 | `VALIDATION_FAILED` | Das in `message` genannte Feld korrigieren und die Anfrage erneut senden. |
| Der Empfänger liegt außerhalb des Zustellgebiets (Angebot) | 200 | `OUT_OF_DELIVERY_AREA` in `errors` | Nur die `label_service`-Tarife anbieten. |
| Die `rate_id` ist abgelaufen, fehlerhaft oder gehört zu einem anderen Konto | 400 | `RATE_ID_INVALID` | Ein neues Angebot anfragen und mit dessen `rate_id` anlegen. Es wurde nichts angelegt. |
| Der Label-Auftrag wurde angelegt, sein Label aber nicht gekauft | 200 (`result` `false`) | `LABEL_PURCHASE_FAILED` | Die `id` behalten; das Label mit `POST /api/v1/uniorder/{orderId}/label` kaufen. Den Auftrag niemals erneut anlegen. |
| Das Label wird angefordert, bevor es gekauft wurde | 409 | `LABEL_PURCHASE_FAILED` | Das Label mit `POST /api/v1/uniorder/{orderId}/label` kaufen. |
| Der Auftrag ist zu weit fortgeschritten für eine Stornierung | 409 | `ORDER_STATUS_NOT_CANCELLABLE` | Den Auftrag unverändert lassen; die Rücksendung gesondert bearbeiten. |
| Der Auftrag kann derzeit nicht storniert werden | 409 | `ORDER_CANCEL_REFUSED` | Den Auftrag unverändert lassen; später erneut versuchen oder sich an das Unternehmen wenden. |
| Der Carrier hat das Label nicht annulliert | 409 | `LABEL_CANCEL_FAILED` | Der Auftrag ist unverändert; die Stornierung später erneut versuchen. |
| Der Auftrag oder Job existiert nicht oder gehört zu einem anderen Konto | 404 | `ORDER_NOT_FOUND` | Die mit dem Webauftrag gespeicherte `id` prüfen. |
| Ein `Idempotency-Key` wird mit einem anderen Body wiederverwendet | 409 | `IDEMPOTENCY_CONFLICT` | Für eine andere Anfrage einen neuen Schlüssel verwenden. |
| Das Token fehlt oder ist abgelaufen, oder das Konto darf keine Aufträge erteilen | 401 | — | Erneut anmelden; die Berechtigungen des Kontos prüfen. |

## Testliste

Verwenden Sie eine Test-`ref` wie `WEB-10045`:

- [ ] Das Angebot liefert für eine Adresse im Gebiet einen `self_delivery`-Tarif.
- [ ] Mit `quote_labels` liefert das Angebot `label_service`-Tarife, jeweils mit einer `rate_id`.
- [ ] Ein Auftrag mit einer `self_delivery`-`rate_id` liefert `id` und `tracking_numbers`.
- [ ] Ein Auftrag mit einer `label_service`-`rate_id` liefert das Label des angefragten Service.
- [ ] Derselbe `Idempotency-Key` legt keinen zweiten Auftrag an.
- [ ] Eine `rate_id`, die älter als 30 Minuten ist, liefert `RATE_ID_INVALID`.
- [ ] Das Label jedes Auftrags lässt sich zu einem druckbaren PDF dekodieren.
- [ ] Der Auftrag, sein Label und sein Tracking lassen sich mit der `id` aus dem Anlegen abrufen.
- [ ] Die Stornierung eines Testauftrags liefert `result: true`; eine erneute Stornierung liefert `already_cancelled: true`.
- [ ] Nach `LABEL_PURCHASE_FAILED` kauft `POST /api/v1/uniorder/{orderId}/label` das Label für denselben Auftrag.
- [ ] Ein Stapel mit zwei Zeilen liefert zwei Ergebnisse mit ihrer `reference`.
