# Versandetiketten

Eine Carrier-Sendung kaufen: Methoden listen → Preis anfragen → Label anlegen → PDF laden → tracken → Ereignisse empfangen → stornieren (oder den Tag abschließen).

Diese Anleitung dokumentiert ausschließlich die folgenden Operationen. Führen Sie sie der Reihe nach aus.

Ersetzen Sie `YOUR_HOST`, `ACCESS_TOKEN` und `shipping_method` durch Ihre Werte. Methoden-IDs unterscheiden sich je Konto — niemals fest kodieren.

## 1. Anmelden

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

```
Authorization: Bearer ACCESS_TOKEN
```

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

[REST-Handbuch](/api/documentation#/paths/v1-user-login/post) · [GraphQL-Handbuch](/api/graphql/documentation#/user/userLogin)

**Überprüfung:** Login liefert `access_token`. Ein späterer Aufruf ohne Token liefert `401`.

## 2. Versandarten listen

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingMethodList \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"detail": true}'
```

Jede Zeile hat:

| Feld | Verwendung |
|---|---|
| `id` | `shipping_method` in jedem späteren Aufruf |
| `name` | Anzeigename |
| `unique_identifier` | Stabiler Code |
| `options.signature_option` | Unterschrift verfügbar |
| `options.insurance_option` | Versicherung verfügbar |
| `options.multi_package` | Mehr als ein Stück |

**GraphQL:** `labelserviceGetShippingMethodList` ([GraphQL-Handbuch](/api/graphql/documentation#/labelservice/labelserviceGetShippingMethodList)) (JSON-Skalar).

**Überprüfung:** die Liste ist nicht leer. Sie haben eine `id` gewählt und wissen, ob diese Methode Unterschrift, Versicherung und mehrere Pakete zulässt. Eine leere Liste bedeutet: auf dem Konto ist keine Methode aktiviert.

## 3. Preis anfragen

Trockenlauf. Der Carrier wird nach einem Preis gefragt; nichts wird gebucht. Der Body hat dieselbe Form wie beim Anlegen. `shipping_method` ist Pflicht.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "shipping_method": 59,
    "name": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "6701 RUE HADLEY",
    "city": "Montreal",
    "province": "QC",
    "postcode": "H4E3R3",
    "country": "CA",
    "weight": 1,
    "length": 30,
    "width": 20,
    "height": 10,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "DEV-LABEL-001",
    "sender_name": "Acme Warehouse",
    "sender_telephone": "5555555555",
    "sender_address_1": "2667 boul des sources",
    "sender_city": "Pointe-Claire",
    "sender_province": "QC",
    "sender_postcode": "H9S2H9",
    "sender_country": "CA"
  }'
```

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

Für mehr als ein Stück senden Sie `packages: [{ ref, weight, length, width, height, weight_unit, dimension_unit }]`. `shipping_from` (Adressbuch-ID) oder `shipping_from_code` kann den `sender_*`-Block ersetzen.

**GraphQL:** `labelserviceRate` ([GraphQL-Handbuch](/api/graphql/documentation#/labelservice/labelserviceRate)).

**Überprüfung:** `result` ist true und Sie haben einen Preis (und Transit-Tage, wenn der Carrier sie sendet). Gibt es keinen Preis, Ziel / Paket / Methode **vor** dem Anlegen korrigieren.

## 4. Label anlegen

Damit wird die Sendung beim Carrier gebucht.

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

Derselbe Body wie Schritt 3. `Idempotency-Key` senden.

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-label-001" \
  -d '{
    "shipping_method": 59,
    "name": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "6701 RUE HADLEY",
    "city": "Montreal",
    "province": "QC",
    "postcode": "H4E3R3",
    "country": "CA",
    "weight": 1,
    "length": 30,
    "width": 20,
    "height": 10,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "DEV-LABEL-001",
    "sender_name": "Acme Warehouse",
    "sender_telephone": "5555555555",
    "sender_address_1": "2667 boul des sources",
    "sender_city": "Pointe-Claire",
    "sender_province": "QC",
    "sender_postcode": "H9S2H9",
    "sender_country": "CA"
  }'
```

**GraphQL:** `labelserviceSubmitOrder` ([GraphQL-Handbuch](/api/graphql/documentation#/labelservice/labelserviceSubmitOrder)).

```json
{
  "result": true,
  "id": 12345,
  "shipping_price": "12.50",
  "tracking_numbers": ["1Z999AA10123456784"],
  "external_id": "EXT_12345"
}
```

| Feld | Verwendung |
|---|---|
| `id` | Superroute-Auftrags-ID — Download und Stornierung |
| `tracking_numbers` | Diese dem Empfänger geben; Tracking akzeptiert sie |
| `external_id` | Carrier-Sendungs-ID |
| `shipping_price` | Abgerechneter Betrag |

**Überprüfung:** `tracking_numbers` ist nicht leer. Speichern Sie `id` und die Nummern. Derselbe `Idempotency-Key` darf kein zweites Label kaufen.

## 5. PDF laden

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "12345",
    "type": "ORDER_ID",
    "base64": 1
  }'
```

`type` kann `ORDER_ID`, `TRACKING_NUMBER` oder `REF` sein. `base64: 0` streamt ein PDF. Das ist das **offizielle Carrier-Label**. Die Stückzahl ist durch die Buchung fest.

**GraphQL:** `labelserviceGetShippingLabel` ([GraphQL-Handbuch](/api/graphql/documentation#/labelservice/labelserviceGetShippingLabel)).

**Überprüfung:** das PDF öffnet sich und zeigt den Carrier-Barcode / die Tracking-Nummer aus Schritt 4. Eine Testkopie drucken, dann wegwerfen — kein Testlabel an einen Carrier geben.

## 6. Verfolgen

Kein Token. Eine Nummer aus `tracking_numbers` verwenden:

```bash
curl https://YOUR_HOST/api/v1/tracking/1Z999AA10123456784
```

[REST-Handbuch](/api/documentation#/operations/getPublicTracking) · [GraphQL-Handbuch](/api/graphql/documentation#/tracking/trackingPublic)

**GraphQL:**

```graphql
query {
  trackingPublic(trackingNumber: "1Z999AA10123456784") {
    result
    deliveried
    is_third_party_tracking
    data {
      tracking_event_status_id
      tracking_event_key
      description
      updated_at_localized
    }
    proofs { file_id type full_url signed_url }
  }
}
```

`is_third_party_tracking` ist true, wenn Ereignisse vom Carrier kommen. Verzweigen Sie nach `tracking_event_status_id` / `tracking_event_key`, nicht nach `description`. Das neueste Ereignis steht zuerst in `data`. Frühe Ereignisse können noch „Informationen übermittelt“ sein, bis der Carrier das Paket scannt. `500` ist zugestellt; `proofs[]` kann dann Unterschrift (`type` `1`) oder Foto (`type` `2`) enthalten.

**Überprüfung:** die Abfrage liefert die Sendung, die Sie gerade angelegt haben. Eine unbekannte Nummer ist `result: false` / 404.

## 7. Ereignisbenachrichtigungen konfigurieren

| Einstellung | Ereignis | Wann |
|---|---|---|
| `order_create_webhook_url` | `order.created` | `id` und Carrier-Tracking-Nummern speichern |
| `tracking_event_webhook_url` | `tracking.event` | Carrier-Scan, unterwegs zur Zustellung, zugestellt |
| `order_status_change_webhook_url` | `order.status_change` | Status in Ihrem System |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | Stornierung abgelehnt, weil der Carrier das Paket schon hat |

**REST:** `PUT /api/v1/webhook-settings` — [REST-Handbuch](/api/documentation#/paths/v1-webhook-settings/put)

**GraphQL:** `webhookSettingsUpdate` ([GraphQL-Handbuch](/api/graphql/documentation#/webhooks/webhookSettingsUpdate)).

```bash
curl -X PUT https://YOUR_HOST/api/v1/webhook-settings \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "order_create_webhook_url": "https://erp.example.com/hooks/superroute",
    "tracking_event_webhook_url": "https://erp.example.com/hooks/superroute",
    "webhook_sign_secret": "YOUR_LONG_RANDOM_SECRET",
    "webhook_verify_ssl": 1
  }'
```

Prüfen Sie **v2** über den Rohkörper: `HMAC_SHA256(timestamp + "." + raw_body, secret)` gegen `X-Webhook-Signature-V2`. Deduplizieren Sie über `X-Webhook-Event-Id`. Antworten Sie **2xx in unter 3 Sekunden**.

```php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE_V2'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $raw, $sharedSecret);
if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}
```

**Überprüfung:** ein Test-`submitOrder` erzeugt `order.created` mit diesen `tracking_numbers`. Ein falsches Secret ist `401` von Ihrem Empfänger.

## 8. Stornieren

Nur solange der Carrier es noch zulässt (meist vor der Abholung).

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/cancelShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"12345","type":"ORDER_ID"}'
```

**GraphQL:** `labelserviceCancelShippingLabel` ([GraphQL-Handbuch](/api/graphql/documentation#/labelservice/labelserviceCancelShippingLabel)).

Ein Carrier, der das Paket bereits hat, lehnt ab — das ist `order.cancel_failed`.

**Überprüfung:** eine zweite Stornierung ist sicher. Tracking behandelt die Sendung nicht mehr als aktiv.

## 9. Tagesabschluss (nur wenn diese Methode ihn braucht)

Manche Carrier brauchen ein tägliches Manifest.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/endofday \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"shipping_method": 59}'
```

**GraphQL:** `labelserviceEndofday` ([GraphQL-Handbuch](/api/graphql/documentation#/labelservice/labelserviceEndofday)).

Einmal nach dem letzten Label des Versandtags aufrufen. Diesen Schritt überspringen, wenn die Methode aus Schritt 2 das nicht verlangt.

**Überprüfung:** der Erfolgsbody (oder die Carrier-Bestätigung) listet die Labels von heute. Zuerst auf einer Testmethode ausführen.

## Testliste

Verwenden Sie ein Ziel, das Sie kontrollieren, und eine Methode, die stornierbar ist:

- [ ] Methodenliste ist nicht leer; Sie haben eine `id` gespeichert.
- [ ] Rate liefert einen Preis für diese Methode und dieses Ziel.
- [ ] Submit liefert `tracking_numbers`; derselbe `Idempotency-Key` kauft kein zweites Label.
- [ ] Das Label-PDF öffnet sich und zeigt die Carrier-Tracking-Nummer.
- [ ] Öffentliches Tracking findet die Sendung unter dieser Nummer.
- [ ] `order.created` kommt an; v2-Signatur prüft erfolgreich.
- [ ] Stornieren gelingt, **oder** Sie haben bestätigt, dass diese Methode nach der Buchung nicht stornierbar ist.
- [ ] Wenn die Methode Tagesabschluss braucht, läuft ein Testdurchlauf ohne Fehler durch.
