# Offerte en order in één stroom

Dit playbook doorloopt de Uniorder-API (`/api/v1/uniorder/...`) verzoek voor verzoek, in de volgorde waarin een integratie wordt gebouwd: authenticeren, offreren, aanmaken tegen de gekozen `rate_id`, het label printen, de order lezen, volgen en annuleren, en zendingen in batches verwerken. Eén offerte toont elke manier waarop het account een pakket kan verzenden: bezorging door het bedrijf zelf en, op verzoek, elke labeldienst van een vervoerder. Bestellen met een `rate_id` maakt de order aan voor die dienst: een bezorgorder, of een labelorder waarvan het label wordt gekocht bij de geoffreerde dienst van de vervoerder. Het is geschreven voor ontwikkelaars van webwinkels, ordermanagementsystemen en ERP's die via een bedrijfsaccount verzenden.

## 1. Wat u kunt bouwen

De onderstaande voorbeelden volgen één bedrijf: **Fleurs du Plateau**, een bloemist aan 4500 Rue Saint-Denis, Montreal (H2J 2L3), die boeketten online verkoopt. Een typisch pakket is één doos van 1,2 kg en 40 × 25 × 25 cm voor Jane Recipient, 6841 Rue Saint-Denis, Montreal (H2S 2S3), onder de webwinkelorder `WEB-10045`.

- **Een checkout die elke verzendoptie aanbiedt.** De winkel offreert het pakket één keer en toont lokale bezorging op dezelfde dag naast elke labeldienst van een vervoerder van het account, elk met de prijs, en maakt daarna de order aan tegen de optie die de klant heeft gekozen.
- **Automatisch labels printen.** Wanneer de order is aangemaakt, downloadt de winkel de label-PDF en stuurt deze naar de printer van het inpakstation, ongeacht of het pakket door het bedrijf of door een vervoerder wordt bezorgd.
- **Een orderpagina met actuele tracking.** De orderpagina van de klant toont de status en de tijdlijn van gebeurtenissen van de zending, met het afleverbewijs zodra het boeket is bezorgd.
- **Een nachtelijke batch vanuit het ERP.** De groothandelsorders van de dag worden in één taak van maximaal 500 rijen in de wachtrij geofferd en aangemaakt, en elk resultaat wordt via `reference` aan de eigen orderregel gekoppeld.

## 2. Wat dit playbook behandelt

Dit is het stapsgewijze playbook van de Uniorder-API. Het overzicht van wat Uniorder biedt, en waarom, staat in **Uniorder: één API voor elke zending**; dit playbook geeft de verzoeken, responses en controles voor elke aanroep.

Uniorder is het aanbevolen enkele toegangspunt voor nieuwe integraties die pakketten verzenden via lokale bezorging of via een vervoerderslabel: het vervangt aparte aanroepen van de API voor lokale bezorging en de API voor vervoerderslabels door één vorm van verzoek. De eerdere endpoints die in **Afhaling en bezorging (eigen vloot)** en **Vervoerderslabels** worden beschreven, blijven beschikbaar en ongewijzigd. Uniorder is niet van toepassing op verzenddiensten die door een klantaccount worden geboekt, noch op orders voor opslag en uitslag; gebruik daarvoor **Verzenddiensten** en **Opslag en uitslag**.

## 3. Voordat u begint

- **Account.** Gebruik een bedrijfsaccount (klantaccount), of een medewerkersaccount van het bedrijf, met API-toestemming. Een klantaccount van het bedrijf kan Uniorder ook aanroepen en wordt altijd als zichzelf geoffreerd en gefactureerd. Voor het aanmaken van een bezorgorder is de toestemming voor het plaatsen van orders vereist.
- **Klanten.** Een klant- of medewerkersaccount kan offreren en bestellen voor een van zijn klanten met `customer_id` of `customer_code` in de offerte; de `rate_id` bevat dan die klant, en de prijs volgt het plan van de klant.
- **Labeldiensten.** Om `label_service`-tarieven te ontvangen, heeft het account (of de genoemde klant) ten minste één geconfigureerd vervoerdersaccount voor labels nodig.
- **Testgegevens.** Gebruik een adres binnen het bezorggebied van het bedrijf voor `self_delivery`-tarieven, en testkenmerken zoals `WEB-10045` die achteraf kunnen worden geannuleerd.
- **Tokens.** Vraag het toegangstoken aan vanaf uw server en bewaar het daar. Stuur het nooit naar een browser of een mobiele app.
- **Plaatshouders.** Vervang `YOUR_HOST` door de API-host van uw omgeving en `ACCESS_TOKEN` door het token uit stap 4.

## 4. Authenticeren

Elke Uniorder-aanroep wordt gedaan namens een account. Meld u eenmaal aan vanaf uw server, bewaar het teruggegeven token en stuur het mee bij elk verzoek.

**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":"orders@fleursduplateau.example","password":"your_password"}'
```

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

- `access_token`: plaats dit in de header van elk volgend verzoek:

```
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. Elke dienst offreren

De offerte toont elke manier waarop het pakket kan worden verzonden, met voor elke manier een prijs en een `rate_id`. De checkout toont de tarieven als opties; er wordt niets aangemaakt of geboekt.

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

De afzender en de ontvanger zijn volledige adressen; alleen `from_address_2` en `to_address_2` zijn optioneel. Elk pakket vereist `weight`, `length`, `width` en `height`. Zet `quote_labels` op `true` om de labeldiensten van vervoerders toe te voegen; de namen en telefoonnummers van beide kanten zijn dan verplicht. Een klant- of medewerkersaccount kan voor een van zijn klanten offreren met `customer_id` of `customer_code`. Een bezorgvenster (`time_window_start`, `time_window_end`, notatie `YYYY-MM-DD HH:MM:SS`) wordt meegenomen wanneer de prijs ervan afhangt.

```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`: bezorging door het bedrijf. Maximaal één per offerte.
- `type` `label_service`: één per dienst van elk labelaccount. Toon `service_name`, `shipping_price` en `transit_days` aan de klant.
- `errors` vermeldt wat niet kon worden geoffreerd, met het `type`. Een adres buiten het bezorggebied is een fout van het type `self_delivery` met code `OUT_OF_DELIVERY_AREA`; toon dan alleen de labeldiensten.
- `rate_id` is 30 minuten geldig en alleen voor het account dat de offerte heeft aangevraagd. Bewaar deze bij de checkoutsessie.
- `result` is `true` wanneer ten minste één tarief is gevonden.

**GraphQL:** `uniorderRate` ([GraphQL-handboek](/api/graphql/documentation#/orders/uniorderRate)). Het antwoord is een JSON-scalar, dus de operatie heeft geen 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
  )
}
```

Variabelen:

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

**Verificatie:** `rates` bevat een `self_delivery`-tarief voor een adres binnen het gebied en, met `quote_labels`, één `label_service`-tarief per dienst van een vervoerder. Er wordt niets aangemaakt.

## 6. De order aanmaken tegen het gekozen tarief

Wanneer de klant betaalt, maakt de winkel de order aan met de `rate_id` van de gekozen optie en dezelfde zending. De `rate_id` bepaalt de dienst; niets anders in het verzoek selecteert deze.

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

Stuur bij elke aanmaakaanroep een `Idempotency-Key`-header mee, uniek per order. Een nieuwe poging met dezelfde sleutel en dezelfde body geeft het eerste antwoord terug met `replayed` `true` en maakt geen tweede order aan.

```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
    }]
  }'
```

Een `self_delivery`-tarief maakt een bezorgorder aan. Bij `type` `D` is de ontvanger de stop; zet `need_pick_up` op `1` om het pakket bij de afzender te laten ophalen. Bij `type` `P` is de afzender de stop.

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

Een `label_service`-tarief maakt een labelorder aan en koopt het label bij de geoffreerde dienst van de vervoerder. `type` moet `D` zijn, en `from_name`, `from_telephone`, `to_name` en `to_telephone` zijn verplicht. Als de klant UPS STANDARD had gekozen, zou het antwoord als volgt zijn:

```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`: bewaar deze bij de webwinkelorder; elke latere aanroep gebruikt deze.
- `tracking_numbers`: de eigen trackingnummers van de zending, één per pakket.
- `shipping_price`: de in rekening gebrachte prijs. De order wordt geprijsd bij het aanmaken; `quoted_price` is de prijs in de offerte. De twee kunnen verschillen.
- `label.main_tracking_number` en `label.shipping_label` (alleen labelorder): het trackingnummer van de vervoerder en de label-PDF in base64.
- `result` `false` met code `LABEL_PURCHASE_FAILED` (alleen labelorder): de order bestaat, maar heeft geen label. Bewaar de `id` en ga verder met stap 11.

**GraphQL:** `uniorderCreate` ([GraphQL-handboek](/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
  )
}
```

Variabelen:

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

**Verificatie:** `result` is `true` en `id` is ingevuld. Een `rate_id` die is verlopen of bij een ander account hoort, geeft `400` met code `RATE_ID_INVALID` terug, en er wordt niets aangemaakt.

## 7. Het label printen

Het inpakstation print het label zodra de order bestaat. Dezelfde aanroep geeft voor een bezorgorder het eigen label van het bedrijf terug en voor een labelorder het gekochte vervoerderslabel.

**REST:** `GET /api/v1/uniorder/{orderId}/label` — [REST-handboek](/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`: de label-PDF in base64. Decodeer deze en stuur het bestand naar de printer.
- `hide_sender_address`, `hide_receiver_address` (`1` om te verbergen): gelden voor het eigen label van het bedrijf bij een bezorgorder.
- `label_status` (labelorder): `ready` wanneer het bestand wordt teruggegeven. Wanneer de vervoerder het bestand nog niet heeft aangemaakt, is het antwoord `200` met `result` `false` en `label_status` `pending`; vraag het label later opnieuw op.
- Deze aanroep koopt nooit een label: een label dat niet is gekocht, geeft `409` met code `LABEL_PURCHASE_FAILED` terug. Koop het via stap 11.

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

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

**Verificatie:** `result` is `true` en de gedecodeerde `pdf_data` opent als een PDF met het trackingnummer van de order.

## 8. De order lezen

De winkel leest de order om de status, adressen en pakketten te tonen op de orderpagina of in een klantenservicescherm.

**REST:** `GET /api/v1/uniorder/{orderId}` — [REST-handboek](/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` of `label_service`; de overige velden hebben voor beide dezelfde vorm.
- `status`: `pending`, `in_transit`, `out_for_pickup`, `out_for_delivery`, `ready_for_self_pickup`, `delivered`, `exception` of `cancelled` voor een bezorgorder, en `label_pending`, `label_purchased` of `cancelled` voor een labelorder.
- `label` (alleen labelorder): de vervoerder, de dienst, `carrier_tracking_numbers` en `label_status` (`not_purchased`, `pending`, `ready` of `failed`).

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

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

**Verificatie:** de order geeft de `status` en `packages` terug, en `ref` komt overeen met de webwinkelorder.

## 9. De order volgen

De orderpagina toont de tijdlijn van de zending. Lees deze wanneer de klant de pagina opent, of houd deze actueel via webhooks.

**REST:** `GET /api/v1/uniorder/{orderId}/tracking` — [REST-handboek](/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`: de tijdlijn, nieuwste eerst, elk met `code`, `description`, `location` en tijdstip.
- `proofs`: bestanden met het afleverbewijs. Toon deze zodra `status` gelijk is aan `delivered`.
- `carrier` (alleen labelorder): de naam van de vervoerder, het trackingnummer en de trackinglink (`tracking_url`).

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

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

**Verificatie:** de trackingaanroep geeft `result` `true`, de `status` van de order en de `events` terug.

## 10. De order annuleren

Wanneer de klant de webwinkelorder annuleert, annuleert de winkel de zending met dezelfde aanroep voor een bezorgorder en een labelorder. Een label wordt eerst bij de vervoerder ongeldig gemaakt.

**REST:** `POST /api/v1/uniorder/{orderId}/cancel` — [REST-handboek](/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` wanneer de order vóór deze aanroep al was geannuleerd. Behandel dit als geslaagd.
- Wanneer de order niet wordt geannuleerd, is het antwoord `409` en blijft de order ongewijzigd: `ORDER_STATUS_NOT_CANCELLABLE` (te laat om te annuleren), `ORDER_CANCEL_REFUSED` (kan nu niet worden geannuleerd) of `LABEL_CANCEL_FAILED` (de vervoerder heeft het label niet ongeldig gemaakt). Laat de webwinkelorder open en handel de zending handmatig af.

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

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

**Verificatie:** `result` is `true`. Dezelfde order opnieuw annuleren geeft `already_cancelled` `true` terug.

## 11. Een label later kopen (alleen na LABEL_PURCHASE_FAILED)

Deze stap geldt alleen voor een labelorder waarvan het aanmaken `LABEL_PURCHASE_FAILED` als antwoord gaf. Het antwoord was `200` met `result` `false`, de code `LABEL_PURCHASE_FAILED` en de order-`id`: de order is zonder label bewaard. Dien de order niet opnieuw in; koop het label voor die order.

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

Het aanmaakverzoek waarbij het label niet kon worden gekocht, gaf als antwoord:

```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."
}
```

Koop het label voor order `123458`:

```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 '{}'
```

Het label wordt gekocht bij de dienst die bij het aanmaken van de order is gekozen. Om bij een andere dienst van hetzelfde account te kopen, stuurt u in de body een nieuwe `label_service`-`rate_id` uit stap 5 (`{"rate_id": "eyJpdiI6IlpxR0..."}`). Een label dat al is gekocht, wordt teruggegeven en niet opnieuw gekocht.

```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`: de label-PDF in base64; print deze zoals in stap 7.
- `result` `false` met opnieuw `LABEL_PURCHASE_FAILED`: de vervoerder weigert nog steeds. Probeer het later opnieuw of koop bij een andere dienst met een nieuwe `rate_id`.

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

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

**Verificatie:** `result` is `true` en `label.shipping_label` bevat de PDF, of `label.label_status` is `pending` terwijl de vervoerder het bestand aanmaakt.

## 12. Batches

Met batches worden veel zendingen in één aanroep geofferd of aangemaakt, bijvoorbeeld de groothandelsorders van het ERP. Elke rij doorloopt de enkele aanroep en geeft terug wat die aanroep zou teruggeven; een rij die mislukt, houdt de andere rijen niet tegen.

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

Maximaal 20 rijen per aanroep, beantwoord in dezelfde response: `shipments` voor de offertebatch, `orders` voor de aanmaakbatch. Elke rij heeft dezelfde velden als de enkele aanroep, plus een optionele `reference` die met het resultaat wordt teruggegeven.

```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`: één per rij, met de `index` van de rij, de `reference`, en de `status` en `body` die de enkele aanroep zou teruggeven. Koppel elk resultaat via `reference` aan de orderregel.

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

Maximaal 500 rijen, in de wachtrij geplaatst als één taak. De aanroep geeft een `job_id` terug; lees de taak tot `status` gelijk is aan `done` en lees daarna `results`. Dezelfde batch die opnieuw wordt verzonden terwijl de eerste nog in de wachtrij staat, geeft de eerste taak terug met `duplicate` `true`.

```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`, of `failed` met een `message` wanneer de taak niet kon worden verwerkt.
- Een taak wordt één keer uitgevoerd en niet opnieuw geprobeerd. Een `rate_id` die verloopt voordat de rij wordt uitgevoerd, geeft voor die rij `RATE_ID_INVALID` terug; verstuur de aanmaaktaak kort nadat de offertetaak is voltooid.

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

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

**Verificatie:** een batch geeft één resultaat per rij terug; een asynchrone taak bereikt `status` `done`.

## 13. Fouten afhandelen

| Situatie | HTTP-status | Code | Wat de integratie doet |
|---|---|---|---|
| Een verplicht veld ontbreekt of is ongeldig | 400 | `VALIDATION_FAILED` | Corrigeer het veld dat in `message` wordt genoemd en verstuur het verzoek opnieuw. |
| De ontvanger ligt buiten het bezorggebied (offerte) | 200 | `OUT_OF_DELIVERY_AREA` in `errors` | Bied alleen de `label_service`-tarieven aan. |
| De `rate_id` is verlopen, ongeldig of hoort bij een ander account | 400 | `RATE_ID_INVALID` | Vraag een nieuwe offerte aan en maak de order aan met de `rate_id` daarvan. Er is niets aangemaakt. |
| De labelorder is aangemaakt, maar het label is niet gekocht | 200 (`result` `false`) | `LABEL_PURCHASE_FAILED` | Bewaar de `id`; koop het label met `POST /api/v1/uniorder/{orderId}/label`. Maak de order nooit opnieuw aan. |
| Het label wordt opgevraagd voordat het is gekocht | 409 | `LABEL_PURCHASE_FAILED` | Koop het label met `POST /api/v1/uniorder/{orderId}/label`. |
| De order is te ver gevorderd om te annuleren | 409 | `ORDER_STATUS_NOT_CANCELLABLE` | Laat de order ongewijzigd; handel de retour afzonderlijk af. |
| De order kan nu niet worden geannuleerd | 409 | `ORDER_CANCEL_REFUSED` | Laat de order ongewijzigd; probeer het later opnieuw of neem contact op met het bedrijf. |
| De vervoerder heeft het label niet ongeldig gemaakt | 409 | `LABEL_CANCEL_FAILED` | De order is ongewijzigd; probeer later opnieuw te annuleren. |
| De order of taak bestaat niet of hoort bij een ander account | 404 | `ORDER_NOT_FOUND` | Controleer de `id` die bij de webwinkelorder is opgeslagen. |
| Een `Idempotency-Key` wordt hergebruikt met een andere body | 409 | `IDEMPOTENCY_CONFLICT` | Gebruik een nieuwe sleutel voor een ander verzoek. |
| Het token ontbreekt of is verlopen, of het account mag geen orders plaatsen | 401 | — | Meld u opnieuw aan; controleer de rechten van het account. |

## Testlijst

Gebruik een test-`ref` zoals `WEB-10045`:

- [ ] De offerte geeft een `self_delivery`-tarief terug voor een adres binnen het gebied.
- [ ] Met `quote_labels` geeft de offerte `label_service`-tarieven terug, elk met een `rate_id`.
- [ ] Bestellen met een `self_delivery`-`rate_id` geeft `id` en `tracking_numbers` terug.
- [ ] Bestellen met een `label_service`-`rate_id` geeft het label van de geoffreerde dienst terug.
- [ ] Dezelfde `Idempotency-Key` maakt geen tweede order aan.
- [ ] Een `rate_id` die ouder is dan 30 minuten geeft `RATE_ID_INVALID` terug.
- [ ] Het label van elke order decodeert tot een printbare PDF.
- [ ] De order, het label en de tracking kunnen worden gelezen met de `id` uit het aanmaken.
- [ ] Het annuleren van een testorder geeft `result: true` terug; opnieuw annuleren geeft `already_cancelled: true` terug.
- [ ] Na `LABEL_PURCHASE_FAILED` koopt `POST /api/v1/uniorder/{orderId}/label` het label voor dezelfde order.
- [ ] Een batch van twee rijen geeft twee resultaten terug met de bijbehorende `reference`.
