# Vervoerderslabels

De labeldienst koopt verzendlabels bij de vervoerders die aan een account zijn gekoppeld (bijvoorbeeld UPS en Canada Post) en bewaart elk label als een order. Een integratie haalt de verzendmethoden van het account op, offreert een pakket, maakt de labelorder aan, koopt het label bij de gekozen dienst van de vervoerder, print de PDF, volgt het pakket en annuleert labels die niet worden gebruikt. De dienst is bedoeld voor webwinkels, magazijnsystemen en ordermanagementsystemen die pakketten via vervoerders verzenden in plaats van via eigen chauffeurs.

## 1. Wat u kunt bouwen

De voorbeelden in dit playbook volgen één bedrijf: **Northbound Outfitters**, een webwinkel voor outdooruitrusting die verzendt vanuit het magazijn aan 1200 Eglinton Ave E, Toronto. Het account heeft een methode voor Canada Post en een methode voor UPS. Een typische order is een tentdoos van 4,2 kg en 60 × 30 × 25 cm met bestemming Calgary; orders naar de Verenigde Staten gaan via UPS.

- **Keuze van de vervoerder bij de checkout.** De winkel offreert de winkelwagen van de klant bij Canada Post, toont de diensten met prijs en transittijd in dagen, en verzendt met de dienst waarvoor de klant heeft betaald.
- **Labels printen met één klik in het magazijn.** Het inpakstation maakt de labelorder aan wanneer een doos is ingepakt, koopt het label bij de gekozen dienst en print de PDF van de vervoerder op een thermische printer.
- **Grensoverschrijdende zendingen met douanegegevens.** Orders naar de Verenigde Staten bevatten artikelregels (omschrijving, aantal, waarde, HS-code), zodat het UPS-label met de handelsgegevens wordt uitgegeven.
- **Automatische statusupdates voor de klant.** De winkel slaat het trackingnummer van de vervoerder op, toont de publieke trackingtijdlijn op de orderpagina en werkt de order bij wanneer een `tracking.event`-webhook meldt dat het pakket is bezorgd.

## 2. Wat dit playbook behandelt

Dit playbook behandelt de v1-labeldienst (`/api/v1/labelservice/...`): één verzendmethode (één vervoerdersaccount) per aanroep. Gebruik deze wanneer de integratie al weet met welke verzendmethode wordt verzonden, of wanneer een bestaande koppeling met de labeldienst wordt onderhouden.

Voor nieuwe integraties is Uniorder (`/api/v1/uniorder/...`) het aanbevolen enkele toegangspunt. De Uniorder-playbooks, "Uniorder: één API voor elke zending" en "Offerte en order in één stroom", offreren alle diensten van de vervoerders van het account tegelijk (samen met de eigen bezorging van het bedrijf, waar die van toepassing is) en kopen het label bij de dienst die wordt gekozen door de `rate_id` ervan terug te sturen. Met dezelfde aanroepen wordt elke order daarna geprint, gevolgd en geannuleerd.

Andere playbooks behandelen de andere soorten zendingen:

- Bezorging door de eigen chauffeurs van het bedrijf: "Afhaling en bezorging (eigen vloot)".
- Een klantaccount dat verzendt via de diensten die het bedrijf aanbiedt: "Verzenddiensten".
- Goederen die in een magazijn worden bewaard en op verzoek worden uitgeleverd: "Opslag en uitslag".

## 3. Voordat u begint

- **Account.** Gebruik een bedrijfsaccount (klantaccount), een medewerker van dat account, of een klantaccount van een bedrijf. Een bedrijfsaccount ziet de eigen verzendmethoden. Een klantaccount ziet alleen de methoden die het bedrijf eraan heeft toegewezen, en elk gekocht label wordt van het saldo afgeschreven; wanneer het bedrijf Auto Pause Label Service voor die klant heeft ingeschakeld, wordt een label geweigerd zolang saldo plus krediet het niet dekt.
- **API-toestemming.** Op het account moet API-toegang zijn ingeschakeld. Zonder deze geeft elke aanroep van de labeldienst `401` met `Unauthorized` terug.
- **Verzendmethoden.** Er moet ten minste één verzendmethode actief zijn op het account (voor klanten: aan de klant toegewezen). Methode-id's verschillen per account en mogen niet worden gehardcodeerd; lees ze uit in stap 5.
- **Testgegevens.** Gebruik een testverzendmethode of een sandbox van de vervoerder waar die is geconfigureerd (tarieven bevatten dan `test_mode: true`), en een bestemming die u beheert. Annuleer elk testlabel dat op een live methode is gekocht.
- **Omgang met tokens.** Meld u aan vanaf uw server en bewaar het token daar. Plaats het token of het wachtwoord niet in een browser of mobiele applicatie.
- **Plaatshouders.** Vervang `YOUR_HOST` door de host van uw platform en `ACCESS_TOKEN` door het token uit stap 4. Vervang de waarden van `shipping_method` door de id's van uw account.

## 4. Authenticeren

Elke aanroep van de labeldienst vereist een bearer-token. De integratie meldt zich eenmaal aan, slaat `access_token` en `expires_at` op de server op en meldt zich opnieuw aan voordat het token verloopt.

**REST:** `POST /api/v1/user/login` — [REST-handboek](/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":"shipping@northbound-outfitters.ca","password":"your_password"}'
```

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
  "token_type": "Bearer",
  "expires_at": "2027-09-28 10:15:00"
}
```

- `access_token`: stuur dit bij elke latere aanroep mee als de onderstaande header.
- `expires_at`: meld u vóór dit tijdstip opnieuw aan.

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL gebruikt dezelfde header op `POST /api/graphql`.

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

**Verificatie:** aanmelden geeft `access_token` terug. Volgende verzoeken zonder dit token geven `401` terug.

## 5. Verzendmethoden ophalen

De lijst met methoden vertelt de integratie met welke vervoerdersaccounts mag worden verzonden en welke opties elk account accepteert. Bewaar de `id` van elke methode die u gebruikt; dit is de `shipping_method` van elke latere aanroep.

**REST:** `POST /api/v1/labelservice/getShippingMethodList` — [REST-handboek](/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}'
```

```json
[
  {
    "id": 59,
    "name": "Canada Post",
    "unique_identifier": "CPC-TOR",
    "options": {
      "signature_option": true,
      "insurance_option": true,
      "insurance_value": true,
      "multi_package": true
    },
    "package_type": {
      "parcel": {
        "name": "Parcel",
        "options": { "weight_options": true, "dimension_options": true }
      }
    },
    "from_contry_limit": ["CA"],
    "isUploadMethod": false
  },
  {
    "id": 61,
    "name": "UPS",
    "unique_identifier": "UPS-TOR",
    "options": {
      "signature_option": true,
      "insurance_option": true,
      "insurance_value": true,
      "multi_package": true
    }
  }
]
```

Elke rij bevat:

| Veld | Gebruik |
|---|---|
| `id` | `shipping_method` in elke latere aanroep |
| `name` | Weergavenaam |
| `unique_identifier` | Stabiele code |
| `options.signature_option` | Handtekening beschikbaar |
| `options.insurance_option` | Verzekering beschikbaar |
| `options.multi_package` | Meer dan één stuk |
| `package_type` | Geaccepteerde `package_type`-codes, en of elk ervan gewicht en afmetingen vereist |
| `from_contry_limit` | Landen waarin het adres van de afzender mag liggen |
| `services` | Vervoerders en diensten achter de methode; met de codes kan een offerte worden beperkt via `carriers` / `services` |

Stuur `"id": 59` om slechts één methode te lezen, of `"detail": false` om alleen `id`, `name` en `unique_identifier` te ontvangen.

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

**Verificatie:** de lijst is niet leeg. U hebt één `id` gekozen en weet of die methode handtekening, verzekering en meerdere pakketten toestaat. Een lege lijst betekent dat er geen methode op het account is ingeschakeld.

## 6. Offreren

Een offerte vraagt de vervoerder om prijzen zonder iets aan te maken: de tijdelijke order die voor het verzoek wordt gebruikt, wordt verwijderd en er wordt niets in rekening gebracht. Northbound Outfitters roept deze aan bij de checkout om de diensten van Canada Post voor de winkelwagen te tonen. De body heeft dezelfde vorm als in stap 7. `shipping_method` is verplicht.

**REST:** `POST /api/v1/labelservice/rate` — [REST-handboek](/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": "Emily Tremblay",
    "telephone": "4035550182",
    "email": "emily.tremblay@example.com",
    "address_1": "1415 17 Ave SW",
    "city": "Calgary",
    "province": "AB",
    "postcode": "T2T0C8",
    "country": "CA",
    "weight": 4.2,
    "length": 60,
    "width": 30,
    "height": 25,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "NB-10482",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA"
  }'
```

```json
{
  "result": true,
  "shipping_price": "24.86",
  "price_details": { "shipping_fee": "24.86" },
  "rates": [
    {
      "carrier_name": "canadapost",
      "currency": "CAD",
      "service_code": "canadapost_expedited_parcel",
      "service_name": "CANADAPOST EXPEDITED PARCEL",
      "price": 24.86,
      "transit_days": 3,
      "test_mode": false
    },
    {
      "carrier_name": "canadapost",
      "currency": "CAD",
      "service_code": "canadapost_xpresspost",
      "service_name": "CANADAPOST XPRESSPOST",
      "price": 38.12,
      "transit_days": 2,
      "test_mode": false
    }
  ],
  "best_rate": {
    "service_code": "canadapost_expedited_parcel",
    "price": 24.86,
    "transit_days": 3
  }
}
```

- `rates[]`: één vermelding per dienst van de vervoerder, met `price`, `currency`, `transit_days` en `price_detail` (basistarief, brandstoftoeslag, belastingen). Toon deze aan de klant.
- `best_rate` / `shipping_price`: het eerste tarief dat de methode teruggeeft.
- Een offerte bevat geen `rate_id` en geen order-`id`. Labels worden gekocht uit de tarieven van de order die in stap 7 wordt aangemaakt.

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

Voor meer dan één stuk stuurt u `packages: [{ ref, weight, length, width, height, weight_unit, dimension_unit }]`. `shipping_from` (adresboek-id) of `shipping_from_code` kan het `sender_*`-blok vervangen. `carriers` en `services` beperken de offerte tot de opgegeven codes.

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

**Verificatie:** `result` is true en u hebt een prijs (en transittijd in dagen, wanneer de vervoerder die meestuurt). Is er geen tarief, corrigeer dan bestemming / pakket / methode **voordat** u aanmaakt.

## 7. De labelorder aanmaken

Deze aanroep maakt de labelorder aan en vraagt de vervoerder om de tarieven voor die zending. Hij geeft de order-`id` en één `rate_id` per dienst terug. Het label is op dit moment nog niet gekocht en er wordt niets in rekening gebracht; stap 8 koopt het. Northbound Outfitters roept deze aan wanneer de doos is ingepakt en slaat `id` op bij de order `NB-10482`.

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

Dezelfde body als in stap 6. Stuur `Idempotency-Key` mee: een nieuwe poging met dezelfde sleutel en dezelfde body geeft het eerste antwoord terug in plaats van een tweede order aan te maken.

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nb-10482-label" \
  -d '{
    "shipping_method": 59,
    "name": "Emily Tremblay",
    "telephone": "4035550182",
    "email": "emily.tremblay@example.com",
    "address_1": "1415 17 Ave SW",
    "city": "Calgary",
    "province": "AB",
    "postcode": "T2T0C8",
    "country": "CA",
    "weight": 4.2,
    "length": 60,
    "width": 30,
    "height": 25,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "NB-10482",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA"
  }'
```

```json
{
  "result": true,
  "id": 128455,
  "shipping_price": "24.86",
  "price_details": { "shipping_fee": "24.86" },
  "rates": [
    {
      "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10",
      "carrier_name": "canadapost",
      "service_code": "canadapost_expedited_parcel",
      "service_name": "CANADAPOST EXPEDITED PARCEL",
      "price": 24.86,
      "currency": "CAD",
      "transit_days": 3
    },
    {
      "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c11",
      "carrier_name": "canadapost",
      "service_code": "canadapost_xpresspost",
      "service_name": "CANADAPOST XPRESSPOST",
      "price": 38.12,
      "currency": "CAD",
      "transit_days": 2
    }
  ],
  "best_rate": {
    "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10",
    "service_code": "canadapost_expedited_parcel",
    "price": 24.86
  }
}
```

| Veld | Gebruik |
|---|---|
| `id` | Superroute-order-id — kopen, downloaden en annuleren |
| `rates[].rate_id` | De dienst die in stap 8 wordt gekocht; alleen geldig voor deze order |
| `rates[].price` | Prijs van die dienst |
| `shipping_price` | Prijs van `best_rate` |

Een zending naar de Verenigde Staten gaat via de UPS-methode, met de artikelregels die voor de douane vereist zijn. Dit voorbeeld gebruikt de vorm `packages`, waarin de artikelen per doos worden meegegeven:

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nb-10497-label" \
  -d '{
    "shipping_method": 61,
    "name": "Daniel Price",
    "telephone": "2065550117",
    "email": "daniel.price@example.com",
    "address_1": "500 Mercer St",
    "city": "Seattle",
    "province": "WA",
    "postcode": "98109",
    "country": "US",
    "package_type": "parcel",
    "paid_by": 1,
    "ref": "NB-10497",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA",
    "packages": [
      {
        "ref": "NB-10497-1",
        "weight": 2.6,
        "length": 45,
        "width": 30,
        "height": 20,
        "weight_unit": 2,
        "dimension_unit": 2,
        "items": [
          {
            "name": "Down sleeping bag",
            "description": "Down-filled sleeping bag, -7 C rating",
            "quantity": 1,
            "unit_price": 289.00,
            "currency": "CAD",
            "weight": 1.6,
            "hscode": "9404400000",
            "sku": "NB-SB-7C",
            "unit": "PCS"
          },
          {
            "name": "Camp stove",
            "description": "Canister camp stove",
            "quantity": 1,
            "unit_price": 79.00,
            "currency": "CAD",
            "weight": 1.0,
            "hscode": "7321111000",
            "sku": "NB-ST-01",
            "unit": "PCS"
          }
        ]
      }
    ]
  }'
```

**GraphQL:** `labelserviceSubmitOrder` ([GraphQL-handboek](/api/graphql/documentation#/labelservice/labelserviceSubmitOrder)). De REST-body komt in `input`:

```graphql
mutation {
  labelserviceSubmitOrder(input: {
    shipping_method: 59
    name: "Emily Tremblay"
    telephone: "4035550182"
    address_1: "1415 17 Ave SW"
    city: "Calgary"
    province: "AB"
    postcode: "T2T0C8"
    country: "CA"
    weight: 4.2
    length: 60
    width: 30
    height: 25
    dimension_unit: 2
    weight_unit: 2
    package_type: "parcel"
    ref: "NB-10482"
    sender_name: "Northbound Outfitters"
    sender_telephone: "4165550140"
    sender_address_1: "1200 Eglinton Ave E"
    sender_city: "Toronto"
    sender_province: "ON"
    sender_postcode: "M3C1H9"
    sender_country: "CA"
  })
}
```

**Verificatie:** het antwoord bevat een `id` en ten minste één `rates[].rate_id`. Bewaar beide. Dezelfde `Idempotency-Key` met dezelfde body geeft dezelfde `id` terug en maakt geen tweede order aan.

## 8. Het label kopen en het zendingsdetail lezen

Deze aanroep koopt het label bij de gekozen dienst, brengt het in rekening en geeft de trackingnummers van de vervoerder terug. Wanneer het label al is gekocht, wordt alleen het detail gelezen, zodat een herhaalde aanroep nooit twee keer koopt. Northbound Outfitters stuurt de `rate_id` van de dienst waarvoor de klant heeft betaald.

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

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

```json
{
  "id": 128455,
  "shippingPrice": "24.86",
  "mainTrackingNumber": "7023210039414604",
  "trackingNumber": "7023210039414604",
  "needSubmitShippingInformation": false,
  "rate": {
    "carrier_name": "canadapost",
    "price": 24.86,
    "price_detail": [
      { "name": "Base charge", "amount": 18.40 },
      { "name": "Fuel surcharge", "amount": 3.60 },
      { "name": "GST", "amount": 1.10 }
    ],
    "tax_items": ["HST", "GST", "PST", "QST"]
  },
  "labelStatus": "ready",
  "shippingLabel": "JVBERi0xLjQKMS... (base64 encoded)"
}
```

| Veld | Gebruik |
|---|---|
| `mainTrackingNumber` | Trackingnummer van de vervoerder voor het eerste pakket; geef dit aan de klant |
| `trackingNumber` | Trackingnummers van de vervoerder voor alle pakketten, gescheiden door komma's |
| `shippingPrice` | In rekening gebracht bedrag |
| `labelStatus` | `ready`: `shippingLabel` bevat de PDF. `pending`: gekocht en in rekening gebracht, maar de vervoerder heeft het bestand nog niet aangemaakt; roep later opnieuw aan. `failed`: het ophalen op de achtergrond is gestopt; een nieuwe aanroep start het opnieuw |
| `needSubmitShippingInformation` | `true` wanneer voor deze methode de zendingsinformatie moet worden ingediend (stap 13) |

`type` kan `ORDER_ID` (standaard), `TRACKING_NUMBER` (het Superroute-pakketnummer) of `THIRD_PARTY_TRACKING_NUMBER` (het nummer van de vervoerder) zijn. Stuur `rate_id` mee, zodat het label bij de door u gekozen dienst wordt gekocht; zonder deze koopt de methode tegen het standaardtarief.

**GraphQL:** `labelserviceGetShippingDetail` ([GraphQL-handboek](/api/graphql/documentation#/labelservice/labelserviceGetShippingDetail))

```graphql
mutation {
  labelserviceGetShippingDetail(
    id: "128455"
    type: "ORDER_ID"
    rate_id: "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10"
  )
}
```

**Verificatie:** `mainTrackingNumber` is niet leeg en `labelStatus` is `ready` (of `pending`, waarna het bij een latere aanroep `ready` wordt). Een tweede aanroep geeft hetzelfde trackingnummer en dezelfde `shippingPrice` terug.

## 9. De PDF downloaden

Het magazijn print het label van de vervoerder via deze aanroep. Als het label nog niet is gekocht, koopt de eerste aanroep het tegen het standaardtarief, zoals in stap 8; roep eerst stap 8 aan om de dienst vast te leggen.

**REST:** `POST /api/v1/labelservice/getShippingLabel` — [REST-handboek](/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": "128455",
    "type": "ORDER_ID",
    "base64": 1
  }'
```

```json
"JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURlY29kZT4+..."
```

- Met `base64: 1` is de body de PDF als één base64-tekenreeks; decodeer deze en stuur deze naar de printer.
- Met `base64: 0` is de response het PDF-bestand zelf (`application/pdf`).

`type` kan `ORDER_ID` (standaard), `TRACKING_NUMBER` of `THIRD_PARTY_TRACKING_NUMBER` (het nummer van de vervoerder) zijn. Dit is het **officiële label van de vervoerder**. Het aantal stuks ligt vast door de boeking.

**GraphQL:** `labelserviceGetShippingLabel` ([GraphQL-handboek](/api/graphql/documentation#/labelservice/labelserviceGetShippingLabel)). GraphQL geeft altijd de base64-tekenreeks terug.

**Verificatie:** de PDF opent en toont de barcode / het trackingnummer van de vervoerder uit stap 8. Print één testexemplaar en gooi het daarna weg — geef geen testlabel aan een vervoerder.

## 10. Volgen

De winkel toont de voortgang van het pakket op de orderpagina van de klant. Het publieke trackingendpoint vereist geen token en accepteert het nummer van de vervoerder uit stap 8.

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

```bash
curl https://YOUR_HOST/api/v1/tracking/7023210039414604
```

```json
{
  "result": true,
  "is_third_party_tracking": true,
  "deliveried": false,
  "data": [
    {
      "tracking_event_status_id": 430,
      "otep_status": "in_transit",
      "description": "Item in transit",
      "location_city": "Mississauga",
      "updated_at_localized": "2026-09-29 18:42"
    }
  ]
}
```

**GraphQL:** `trackingPublic` ([GraphQL-handboek](/api/graphql/documentation#/tracking/trackingPublic))

```graphql
query {
  trackingPublic(trackingNumber: "7023210039414604") {
    result
    deliveried
    is_third_party_tracking
    data {
      tracking_event_status_id
      otep_status
      description
      updated_at_localized
    }
    third_party_info { tracking_number status carrier_tracking_link }
    proofs { file_id type full_url signed_url }
  }
}
```

- `is_third_party_tracking` is true wanneer de events van de vervoerder komen.
- `data`: nieuwste event eerst. Vertak op `tracking_event_status_id` / `otep_status`, niet op `description`. Vroege events kunnen nog "informatie ingediend" zijn totdat de vervoerder het pakket scant.
- `deliveried` is true en `500` betekent bezorgd; `proofs[]` kan dan een handtekening (`type` `1`) of foto (`type` `2`) bevatten.

**Verificatie:** de opzoeking geeft de zending terug die u zojuist hebt aangemaakt. Een onbekend of geannuleerd nummer geeft `404` met `result: false` terug.

## 11. Gebeurtenismeldingen configureren

Webhooks vervangen polling: de server van de winkel ontvangt elke scan van de vervoerder en werkt de order bij zonder stap 10 volgens een schema aan te roepen.

| Instelling | Event | Wanneer |
|---|---|---|
| `tracking_event_webhook_url` | `tracking.event` | Scans van de vervoerder, onderweg voor bezorging, bezorgd |
| `order_status_change_webhook_url` | `order.status_change` | Status in uw systeem |
| `order_create_webhook_url` | `order.created` | Er is een labelorder aangemaakt (stap 7); voor labelorders alleen verzonden wanneer `order_created_webhook_all_types` gelijk is aan `1` |

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

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

```json
{
  "result": true,
  "changed_keys": [
    "tracking_event_webhook_url",
    "order_create_webhook_url",
    "order_created_webhook_all_types",
    "webhook_sign_secret",
    "webhook_verify_ssl"
  ],
  "recipient_type": "business",
  "settings": {
    "webhook_sign_secret": "************CRET",
    "tracking_event_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_create_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_created_webhook_all_types": 1,
    "webhook_verify_ssl": 1
  }
}
```

- Alleen de sleutels die u stuurt, worden gewijzigd; een onbekende sleutel wordt geweigerd met `400`.
- `changed_keys` vermeldt wat is opgeslagen. Het geheim wordt altijd gemaskeerd teruggegeven.

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

Elk `tracking.event` bevat `order_id`, `tracking_event_status_id`, `tracking_event_key`, `tracking_number` en `external_tracking_number`; koppel het via `order_id` (de `id` uit stap 7) aan uw order.

Verifieer **v2** over de ruwe body: `HMAC_SHA256(timestamp + "." + raw_body, secret)` tegen `X-Webhook-Signature-V2`. Dedupliceer op `X-Webhook-Event-Id`. Antwoord met **2xx binnen 3 seconden**.

```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;
}
```

`order_cancel_failed_webhook_url` (`order.cancel_failed`) wordt niet verzonden voor labelannuleringen in stap 12; een geweigerde labelannulering wordt gemeld in de response van die aanroep.

**Verificatie:** één test-`submitOrder` levert `order.created` op met de order-`id`, en de eerste scan van de vervoerder levert `tracking.event` op. Een ongeldige handtekening moet door de ontvanger met `401` worden geweigerd.

## 12. Annuleren

Een label dat niet wordt verzonden, wordt geannuleerd zodat de vervoerder het niet factureert; het bedrag wordt aan het account terugbetaald. Annuleren is alleen mogelijk zolang de vervoerder het nog toestaat (meestal vóór de ophaling).

**REST:** `POST /api/v1/labelservice/cancelShippingLabel` — [REST-handboek](/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" \
  -H "Idempotency-Key: nb-10482-cancel" \
  -d '{"id": 128455}'
```

```json
{
  "result": true,
  "message": "Shipping Label cancelled successfully"
}
```

- Stuur precies één van `id` (de order-id) of `tracking_number` (het Superroute-trackingnummer of dat van de vervoerder). Beide sturen geeft `400` terug.
- `result: true`: de vervoerder heeft de annulering geaccepteerd en het labelbedrag is terugbetaald.

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

Een vervoerder die het pakket al heeft, weigert: het antwoord is `400` met `result: false` en het bericht van de vervoerder. Een order waarvan het label nooit is gekocht, kan niet via deze aanroep worden geannuleerd.

**Verificatie:** het antwoord is `result: true`, en publieke tracking voor dat nummer geeft `404` terug. Een nieuwe poging met dezelfde `Idempotency-Key` geeft het opgeslagen antwoord terug; een nieuw annuleringsverzoek voor dezelfde order geeft `400` `This order already cancelled` terug.

## 13. Zendingsinformatie indienen en de dag afsluiten (alleen als deze methode dit vereist)

Sommige vervoerders vereisen dat de zendingen van de dag vóór de ophaling worden doorgegeven (een manifest). Stap 8 meldt dit per order in `needSubmitShippingInformation`. Verzamel die order-id's gedurende de dag en dien ze in na het laatste label.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitShippingInformation \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ids": [128455, 128461, 128470]}'
```

```json
{
  "result": true,
  "message": "Processed 3 orders. Success: 3, Failed: 0",
  "data": {
    "total_processed": 3,
    "success_count": 3,
    "failure_count": 0,
    "details": [
      { "order_id": 128455, "result": true, "message": "Successful" },
      { "order_id": 128461, "result": true, "message": "Successful" },
      { "order_id": 128470, "result": true, "message": "Successful" }
    ]
  }
}
```

- `details[]`: één regel per order; dien de orders met `result: false` opnieuw in nadat u de oorzaak in `message` hebt verholpen.
- `404` `No eligible orders found for shipping information submission`: geen van de id's heeft een gekocht label waarvoor nog indiening nodig is.

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

Sluit daarna de dag af. De aanroep heeft geen body en omvat alle labelorders van de aanroeper.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/endofday \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "success": 0,
  "failed": 0,
  "success_ids": [],
  "failed_ids": []
}
```

- `400` met `There are orders need to submit shipping information`: voor sommige gekochte labels moet de informatie nog worden ingediend; dien ze in met `submitShippingInformation` en roep opnieuw aan.

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

Sla deze stap over wanneer geen enkele order van de dag `needSubmitShippingInformation: true` heeft gemeld.

**Verificatie:** `submitShippingInformation` meldt `failure_count: 0`, en `endofday` antwoordt met `200`. Voer dit eerst uit op een testmethode.

## 14. Fouten afhandelen

Fouten van de labeldienst bevatten een `message`; een `code` is alleen aanwezig waar de tabel er een vermeldt.

| Situatie | HTTP-status | Code | Wat de integratie doet |
|---|---|---|---|
| Token ontbreekt of is verlopen, of API-toegang is niet ingeschakeld | `401` | — (`Unauthorized`) | Meld u opnieuw aan; als het probleem aanhoudt, vraag het bedrijf API-toegang in te schakelen |
| `shipping_method` ontbreekt of is niet beschikbaar voor de aanroeper | `400` | — | Laad de lijst met methoden opnieuw (stap 5) en gebruik een `id` daaruit |
| `package_type` wordt niet door de methode aangeboden | `400` | — | Gebruik een sleutel uit `package_type` van stap 5 |
| Ongeldig adres of pakket, of de vervoerder geeft geen tarief terug | `400` | — (bericht van de vervoerder) | Toon het bericht, corrigeer de gegevens en offreer opnieuw |
| `auto_deduplication` is `1` en de `ref` bestaat al | `400` | — (`exist_order_ids`) | Gebruik de bestaande order uit `exist_order_ids` in plaats van een nieuwe aan te maken |
| Dezelfde `Idempotency-Key` met een andere body | `409` | `IDEMPOTENCY_CONFLICT` | Gebruik een nieuwe sleutel voor een ander verzoek |
| Dezelfde `Idempotency-Key` terwijl het eerste verzoek nog wordt verwerkt | `409` | `IDEMPOTENCY_IN_PROGRESS` | Wacht `Retry-After` seconden en probeer het opnieuw met dezelfde sleutel en body |
| Saldo plus krediet van de klant dekt het label niet | `400` | `INSUFFICIENT_BALANCE` | Waardeer op met de gegevens uit `insufficient_balance` (`shortfall`, `add_funds_url`) en roep stap 8 opnieuw aan |
| Label gekocht, bestand van de vervoerder nog niet gereed | `400` bij de eerste aankoop, later `200` | `shipment_label_not_ready` | Wacht zolang `labelStatus` gelijk is aan `pending`; roep stap 8 opnieuw aan wanneer het `failed` is |
| Order-id of nummer is niet van de aanroeper | `401` | — (`Not Auth`) | Controleer de id en `type`; gebruik het account dat de order heeft aangemaakt |
| Annulering geweigerd door de vervoerder, of de order is al geannuleerd | `400` | — | Behandel het label als verzonden (of al geannuleerd); probeer het niet opnieuw |
| Trackingnummer onbekend of geannuleerd | `404` | — | Toon de tijdlijn voor dat nummer niet meer |
| `endofday` met zendingen die nog niet zijn ingediend | `400` | — | Voer `submitShippingInformation` uit voor die orders en roep daarna opnieuw aan |

## Testlijst

Gebruik een bestemming die u beheert en een methode die kan worden geannuleerd:

- [ ] De lijst met methoden is niet leeg; u hebt één `id` vastgelegd.
- [ ] De offerte geeft een prijs terug voor die methode en bestemming.
- [ ] Aanmaken geeft een order-`id` en `rates[].rate_id` terug; dezelfde `Idempotency-Key` maakt geen tweede order aan.
- [ ] `getShippingDetail` met de gekozen `rate_id` geeft `mainTrackingNumber` terug; een tweede aanroep brengt niet opnieuw kosten in rekening.
- [ ] De label-PDF opent en toont het trackingnummer van de vervoerder.
- [ ] Publieke tracking vindt de zending via dat nummer.
- [ ] `tracking.event` (en `order.created`, indien ingeschakeld) komt binnen; de v2-handtekening wordt geverifieerd.
- [ ] Een grensoverschrijdende testzending met `items` wordt door de vervoerder geaccepteerd.
- [ ] Annuleren slaagt, **of** u hebt bevestigd dat deze methode na de boeking niet kan worden geannuleerd.
- [ ] Als de methode een dagafsluiting vereist, wordt een testrun zonder fouten voltooid.
