# Afhaling en bezorging

Afhaling en last mile met eigen vloot gebruiken dezelfde API’s. Een afhaal- of bezorgorder aanmaken → het lokale label printen → volgen → abonneren op gebeurtenismeldingen → een testorder annuleren.

`type` selecteert de stop: `D` bezorging, `P` afhaling. Voer de stappen in volgorde uit. Een tariefopvraag is optioneel en niet vereist vóór het aanmaken.

Vervang `YOUR_HOST` en `ACCESS_TOKEN` door de waarden van uw omgeving.

## 1. Aanmelden

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

Plaats het teruggegeven `access_token` in de requestheader:

```
Authorization: Bearer ACCESS_TOKEN
```

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

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

**Verificatie:** login geeft `access_token` terug. Latere verzoeken zonder dit token geven `401`.

## 2. Tarief opvragen (optioneel)

Deze stap is optioneel. Ze geeft alleen een tarief terug; er wordt geen order aangemaakt. Aanmaken vereist geen voorafgaand tarief. Zet `type` op `D` (bezorging) of `P` (afhaling).

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

```bash
curl -X POST https://YOUR_HOST/api/v1/orders/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "D",
    "from_postcode": "H9S2H9",
    "from_country": "CA",
    "to_postcode": "H4B2T5",
    "to_country": "CA",
    "packages": [{
      "weight": 1,
      "weight_unit": 2,
      "length": 30,
      "width": 20,
      "height": 10,
      "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,
  "shipping_price": "6.99",
  "currency": "CAD",
  "price_details": {
    "shipping_fee": "6.99",
    "tax_details": [{ "tax_name": "HST", "tax_rate": "13.00", "tax": "0.91" }]
  }
}
```

**GraphQL** (JSON-scalar — geen selection set) (`ordersRate` ([GraphQL-handboek](/api/graphql/documentation#/orders/ordersRate))):

```graphql
mutation {
  ordersRate(
    type: "D"
    from_postcode: "H9S2H9"
    from_country: "CA"
    to_postcode: "H4B2T5"
    to_country: "CA"
    packages: [{ weight: 1, weight_unit: 2, length: 30, width: 20, height: 10, dimension_unit: 2 }]
  )
}
```

**Verificatie:** als u deze aanroep uitvoert, is `result` true en `shipping_price` een getal. Herhaal met `type` `P` om een afhaaltarief op te vragen. Geen prijs betekent dat de postcode niet in een actief gebied ligt. Aanmaken hangt niet van deze stap af.

## 3. De order aanmaken

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

Het verzoek moet `Idempotency-Key` bevatten, zodat een retry geen tweede order kan aanmaken.

### Bezorging (`type` `D`)

```bash
curl -X POST https://YOUR_HOST/api/v1/client/orderCreate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-local-001" \
  -d '{
    "type": "D",
    "need_pick_up": 0,
    "ref": "DEV-LOCAL-001",
    "name": "Jane Recipient",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "2667 boul des sources",
    "city": "Pointe-Claire",
    "province": "QC",
    "postcode": "H9S2H9",
    "country": "Canada",
    "packages": 1,
    "packagesDetail": [{
      "ref": "PKG-001",
      "weight": 1,
      "weight_unit": 2,
      "length": 30,
      "width": 20,
      "height": 10,
      "dimension_unit": 2
    }],
    "delivery_instruction": "Leave at the side door"
  }'
```

### Afhaling (`type` `P`)

Hetzelfde endpoint. Het adres is de afhaalstop.

```bash
curl -X POST https://YOUR_HOST/api/v1/client/orderCreate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-pickup-001" \
  -d '{
    "type": "P",
    "need_pick_up": 1,
    "ref": "DEV-PICKUP-001",
    "name": "Jane Sender",
    "telephone": "5555555555",
    "email": "jane@example.com",
    "address_1": "2667 boul des sources",
    "city": "Pointe-Claire",
    "province": "QC",
    "postcode": "H9S2H9",
    "country": "Canada",
    "packages": 1,
    "packagesDetail": [{
      "ref": "PKG-P-001",
      "weight": 1,
      "weight_unit": 2,
      "length": 30,
      "width": 20,
      "height": 10,
      "dimension_unit": 2
    }],
    "pickup_instruction": "Ring the side bell"
  }'
```

| Veld | Betekenis |
|---|---|
| `type` | `D` bezorging, of `P` afhaling |
| `need_pick_up` | `0` — ligt al in het magazijn. `1` — een chauffeur moet het pakket ophalen |
| `ref` | Extern kenmerk voor opzoeken en afstemming |
| `name` / address | Bezorging: ontvanger. Afhaling: afhaalstop |
| `auto_deduplication` | `1` weigert een tweede pakket met dezelfde pakket-`ref` |

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

```json
{ "result": true, "id": 12345, "tracking_number": "SR123456789012", "orders_status_id": 2 }
```

`orders_status_id` `2` is Nieuw. Bewaar `id`, `tracking_number` en `ref`.

**Verificatie:** stuur het **zelfde** body met dezelfde `Idempotency-Key` opnieuw. U moet dezelfde `id` krijgen en mag geen tweede order aanmaken.

De order wordt **niet** aangemaakt wanneer:

| `code` | Wat te doen |
|---|---|
| `INSUFFICIENT_BALANCE` | `insufficient_balance` bevat required / available / shortfall. Opwaarderen, daarna opnieuw. |
| `OUT_OF_DELIVERY_AREA` | Het adres ligt buiten het servicegebied en het bedrijf verwijdert die orders. Dien een adres binnen het servicegebied in. |
| `IDEMPOTENCY_CONFLICT` | Dezelfde idempotency-sleutel is hergebruikt met een andere requestbody. Geef een nieuwe sleutel uit. |

Een bewaarde order buiten het gebied kan toch `result: true` teruggeven met `shipping_price: null` en een `warning`. Lees dat veld.

## 4. De order ophalen

```bash
curl "https://YOUR_HOST/api/v1/orders/list?page=1&per_page=50" \
  -H "Authorization: Bearer ACCESS_TOKEN"

curl https://YOUR_HOST/api/v1/orders/12345 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

**REST:** `GET /api/v1/orders/list` — alle orders van het account, de nieuwste eerst, elk met de pakketten en artikelregels. Stuur `page` en `per_page` samen om te pagineren (`per_page` maximaal 1000); zonder deze krijgt u de nieuwste 1000 orders en een `truncated`-markering. [REST-handboek](/api/documentation#/paths/v1-orders-list/get)

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

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

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

**Verificatie:** de order hoort bij het geverifieerde account. `ref` komt overeen met de waarde die u hebt aangemaakt. `tracking_number` komt overeen met stap 3.

## 5. Het lokale label printen

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

```bash
curl -X POST https://YOUR_HOST/api/v1/shipping/getShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "SR123456789012",
    "type": "TRACKING_NUMBER",
    "base64": 1,
    "hide_sender_address": 0,
    "hide_receiver_address": 0
  }'
```

`type`: `TRACKING_NUMBER`, `ORDER_ID` of `REF`. `base64: 0` (standaard) streamt een PDF. Bij `base64: 1` is de volledige responsbody een JSON-tekenreeks op het hoogste niveau met de base64-PDF, geen object met een veld `pdf_data`. Roep in plaats daarvan `POST /api/v2/shipping/getShippingLabel` aan als u het label in een gewoon JSON-object wilt ontvangen.

**GraphQL:** `shippingGetShippingLabel` ([GraphQL-handboek](/api/graphql/documentation#/shipping/shippingGetShippingLabel)). `shippingGetShippingLabelV2` ([GraphQL-handboek](/api/graphql/documentation#/shipping/shippingGetShippingLabelV2)) geeft altijd JSON (`pdf_data`) terug.

**Verificatie:** de PDF opent. Een bezorglabel toont de ontvanger; een afhaallabel toont het afhaaladres. Een verborgen adresveld wordt leeg weergegeven op het label.

## 6. Volgen

Openbaar trackingendpoint; er is geen toegangstoken vereist.

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

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

Dezelfde URL accepteert uw `ref` wanneer die als extern nummer is opgeslagen.

**GraphQL** (getypeerd — heeft een selection set nodig):

```graphql
query {
  trackingPublic(trackingNumber: "SR123456789012") {
    result
    deliveried
    returntosender
    rejectedbyrecipient
    postcode
    data {
      tracking_event_status_id
      tracking_event_key
      description
      updated_at_localized
    }
    proofs { file_id type full_url signed_url }
  }
}
```

Vertak op `tracking_event_status_id`, niet op `description` (die string volgt `Accept-Language`):

| `tracking_event_status_id` | Kant | Betekenis |
|---|---|---|
| `100` | beide | Order ontvangen |
| `300` / `301` | bezorging | In de vestiging |
| `450` | bezorging | Onderweg voor bezorging |
| `500` | bezorging | Bezorgd |
| `501` | bezorging | Bezorging mislukt, nieuw plan nodig |
| `460` | afhaling | Onderweg voor ophalen |
| `510` | afhaling | Opgehaald |
| `512` | afhaling | Ophalen mislukt, later opnieuw proberen |
| `513` | afhaling | Probleem bij ophalen |

`data` is nieuwste eerst. Behandel de eerste rij als huidig. `deliveried: true` na `500`.

Op `500` of `510` kan `proofs[]` `type` `1` (handtekening) of `2` (foto) bevatten, plus `file_id` en `signed_url`. Een foto die na dat event is geüpload, staat niet in die payload — abonneer u in stap 7 op `pod.files_updated`.

**Verificatie:** vlak na aanmaken is het nieuwste event `100` en `deliveried` is false. Een onbekend nummer is `result: false` / 404 — toon een niet-gevondenstatus; verzin geen trackingevents.

## 7. Gebeurtenismeldingen configureren

Configureer de callback-URL’s die dit pad vereist:

| Instelling | Event | Wanneer |
|---|---|---|
| `order_create_webhook_url` | `order.created` | `id` en `tracking_number` bewaren |
| `order_status_change_webhook_url` | `order.status_change` | Status zichtbaar voor de klant |
| `tracking_event_webhook_url` | `tracking.event` | Tijdlijn van afhaling of bezorging |
| `pod_files_webhook_url` | `pod.files_updated` | Foto/handtekening na ophalen of bezorging |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | Een annulering die u stuurde is geweigerd |

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

**GraphQL:** `webhookSettingsUpdate` ([GraphQL-handboek](/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",
    "order_status_change_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
  }'
```

Verifieer **v2** over de ruwe body: `HMAC_SHA256(timestamp + "." + raw_body, secret)` tegen `X-Webhook-Signature-V2`. Dedupliceer op `X-Webhook-Event-Id`. Antwoord **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;
}
```

**Verificatie:** maak één testorder en zie `order.created` met dezelfde `id` / `tracking_number`. Een ongeldige handtekening moet door de ontvanger met `401` worden geweigerd. Een tweede levering van dezelfde `X-Webhook-Event-Id` mag niet twee keer worden verwerkt.

## 8. Een testorder annuleren

**REST:** `POST /api/v1/orders/cancel` — [REST-handboek](/api/documentation#/paths/v1-orders-cancel/post) — precies één van `order_id`, `tracking_number`, `external_tracking_number`.

```bash
curl -X POST https://YOUR_HOST/api/v1/orders/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tracking_number":"SR123456789012"}'
```

Al geannuleerd: `200` met `already_cancelled: true`. **GraphQL:** `ordersCancel` ([GraphQL-handboek](/api/graphql/documentation#/orders/ordersCancel)).

**Verificatie:** publieke tracking behandelt de zending niet meer als actief. Als annuleren wordt geweigerd (`409`, bijvoorbeeld `ORDER_ALREADY_IN_DELIVERY`), vuurt `order.cancel_failed`.

## 9. Reeks aanmaken (optioneel)

- `POST /api/v1/client/batchOrderCreate` — [REST-handboek](/api/documentation#/paths/v1-client-batchOrderCreate/post) — wacht tot elke rij is verwerkt.
- `POST /api/v1/client/batchOrderCreateAsync` — [REST-handboek](/api/documentation#/paths/v1-client-batchOrderCreateAsync/post) — geeft onmiddellijk een job-id terug; poll `GET /api/v1/client/async/{id}` — [REST-handboek](/api/documentation#/paths/v1-client-async-id/get) of neem `order.create_async`.

Dezelfde velden als stap 3, als array van orders. Elke rij mag `type` `D` of `P` zijn. **GraphQL:** `clientBatchOrderCreate` ([GraphQL-handboek](/api/graphql/documentation#/client/clientBatchOrderCreate)) / `clientBatchOrderCreateAsync` ([GraphQL-handboek](/api/graphql/documentation#/client/clientBatchOrderCreateAsync)).

**Verificatie:** elke rij heeft zijn eigen `result`. Gebruik het asynchrone endpoint wanneer u meer dan ongeveer 100 rijen indient.

## Testlijst

Gebruik een test-`ref` zoals `DEV-LOCAL-001` / `DEV-PICKUP-001`:

- [ ] (Optioneel) Tarief geeft een prijs terug voor een postcode in het gebied met `type` `D`.
- [ ] (Optioneel) Tarief geeft een prijs terug voor een postcode in het gebied met `type` `P`.
- [ ] Aanmaken van een bezorging geeft `id` + `tracking_number`; dezelfde `Idempotency-Key` maakt geen tweede order.
- [ ] Aanmaken van een afhaling geeft `id` + `tracking_number`; `need_pick_up` is `1`.
- [ ] Lijst / detail toont de order onder dit account.
- [ ] De lokale-label-PDF opent en toont de ontvanger of het afhaaladres.
- [ ] Publieke tracking geeft de tijdlijn zonder token; nieuwste event is `100`.
- [ ] `order.created` komt binnen; v2-handtekening verifieert.
- [ ] Annuleren geeft `result: true` (of `already_cancelled: true` bij retry).
