# Vyzvednutí a doručení (vlastní flotila)

Tato příručka popisuje API místního doručení firemního účtu: objednávky, které vlastní řidiči firmy doručí příjemci (`type` `D`) nebo vyzvednou u odesílatele (`type` `P`). Jedna sada endpointů oceňuje, vytváří, opatřuje štítky, sleduje a ruší oba druhy zastávek a webhooky hlásí každou změnu vašemu systému. Je určena vývojářům systémů pro správu objednávek, ERP a internetových obchodů, které předávají práci vlastní flotile firmy.

## 1. Co můžete vytvořit

Následující příklady sledují jednu firmu: **Farine & Fils**, dodavatele pro pekárny se skladem na adrese 2200 Rue Cohen, Saint-Laurent, QC (H4R 2N6), který doručuje velkoobchodní objednávky po celém ostrově Montreal a vyzvedává prázdné přepravky na chléb, které jeho zákazníci vracejí. Typickým doručením je jeden stoh přepravek o hmotnosti 12 kg a rozměrech 60 × 40 × 30 cm pro Café Lumière, 5400 Avenue du Parc, Montréal (H2V 4G7), pod velkoobchodní objednávkou `WHS-20931`. Typickým vyzvednutím je jeden stoh prázdných přepravek o hmotnosti 4 kg z Épicerie Wellington, 4100 Rue Wellington, Verdun (H4G 1V5), pod referencí `CRT-20931`.

- **Velkoobchodní objednávky odeslané na dispečink z ERP.** Každá potvrzená velkoobchodní objednávka se stane objednávkou doručení s ranním časovým oknem doručení kavárny a ERP uloží vrácené sledovací číslo k řádku objednávky.
- **Vyzvednutí vrácených přepravek.** Když zákazník nahlásí prázdné přepravky, ERP vytvoří objednávku vyzvednutí na adresu zákazníka a řidič přepravky vyzvedne na následující trase.
- **Tisk štítků ve skladu.** ERP stáhne PDF štítku každé objednávky a vytiskne ho u nakládací rampy, takže každý stoh přepravek nese svůj sledovací čárový kód.
- **Zákaznický portál s aktuálním stavem.** Každá kavárna vidí stav svých doručení a vyzvednutí včetně dokladu o doručení, přičemž údaje přicházejí přes webhooky místo opakovaného dotazování.

## 2. Co tato příručka pokrývá

Tuto příručku použijte, když objednávku přepravují vlastní řidiči firmy: doručení ze skladu a vyzvednutí z adresy zákazníka, vytvářená jednotlivě nebo v dávkách přes endpointy `/api/v1/client/...` a `/api/v1/orders/...`.

Pro nové integrace je doporučeným jediným vstupním bodem Uniorder (`/api/v1/uniorder/...`): nabízí stejná doručení vlastní flotilou přes jedno API spolu se štítky dopravce z jedné cenové nabídky. Přehled najdete v příručce **Uniorder: jedno API pro každou zásilku** a požadavky krok za krokem v příručce **Nabídka a objednávka v jednom toku**. Endpointy v této příručce zůstávají dostupné a nezměněné pro integrace, které jsou na nich postavené.

Příručku **Štítky dopravce** použijte, když balík přepravuje externí dopravce se štítkem koupeným přes platformu. Příručku **Přepravní služby** použijte pro objednávky, které zákaznický účet rezervuje u služeb firmy, a příručku **Skladování a výdej** pro zboží uložené ve skladu a odesílané na požádání; na tyto dva případy se Uniorder nevztahuje.

## 3. Než začnete

- **Účet.** Použijte firemní (klientský) účet nebo zaměstnanecký účet firmy s oprávněním k API. Vytváření objednávek navíc vyžaduje oprávnění k zadávání objednávek; bez něj `POST /api/v1/client/orderCreate` vrátí `401`.
- **Oblast služby.** Adresa doručení nebo vyzvednutí musí být v aktivní oblasti firmy. Pro testy použijte adresy v oblasti, například adresy z této příručky.
- **Testovací data.** Použijte testovací reference, například `WHS-20931` a `CRT-20931`, a testovací objednávky na konci zrušte (krok 12).
- **Tokeny.** Přístupový token si vyžádejte ze svého serveru a uchovávejte ho tam. Nikdy ho neposílejte do prohlížeče ani do mobilní aplikace.
- **Zástupné hodnoty.** Nahraďte `YOUR_HOST` hostitelem API vašeho prostředí a `ACCESS_TOKEN` tokenem z kroku 4.
- **Jednotky.** `weight_unit`: `1` g, `2` kg, `3` oz, `4` lb. `dimension_unit`: `1` mm, `2` cm, `3` m, `4` in. Výchozí hodnota obou je `1`.

## 4. Přihlášení

Každé volání v této příručce kromě veřejného sledování se provádí jménem firemního účtu. Přihlaste se jednou ze svého serveru, uložte vrácený token a posílejte ho při každém požadavku.

**REST:** `POST /api/v1/user/login` — [Příručka REST](/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":"dispatch@farineetfils.example","password":"your_password"}'
```

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

- `access_token`: vložte ho do hlavičky každého dalšího požadavku:

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL používá stejnou hlavičku na `POST /api/graphql`.

**GraphQL:** `userLogin` ([Příručka GraphQL](/api/graphql/documentation#/user/userLogin))

**Ověření:** přihlášení vrátí `access_token`. Následující požadavky bez tohoto tokenu vrátí `401`.

## 5. Cenová nabídka doručení nebo vyzvednutí (volitelně)

Cenová nabídka ukáže cenu zastávky ještě před existencí objednávky, například pro zobrazení poplatku za doručení na velkoobchodní faktuře. Nic nevytváří a vytvoření objednávky nevyžaduje předchozí cenovou nabídku. Nastavte `type` na `D` (doručení) nebo `P` (vyzvednutí); `to_postcode` je PSČ zastávky.

**REST:** `POST /api/v1/orders/rate` — [Příručka REST](/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": "H4R2N6",
    "from_country": "CA",
    "to_postcode": "H2V4G7",
    "to_country": "CA",
    "packages": [{
      "weight": 12,
      "weight_unit": 2,
      "length": 60,
      "width": 40,
      "height": 30,
      "dimension_unit": 2
    }]
  }'
```

```json
{
  "result": true,
  "shipping_price": "18.50",
  "currency": "CAD",
  "price_details": {
    "shipping_fee": "18.50",
    "tax_details": [
      { "tax_name": "GST", "tax_rate": "5.00", "tax": "0.93" },
      { "tax_name": "QST", "tax_rate": "9.975", "tax": "1.85" }
    ]
  }
}
```

- `shipping_price`: cena zastávky bez daně. Prázdná cena znamená, že PSČ není v aktivní oblasti nebo pro něj ceník nemá řádek.
- `price_details.tax_details`: daně, které bude objednávka nést; zobrazte je na řádku faktury.
- `currency`: měna všech částek v odpovědi.

Pro cenovou nabídku vyzvednutí přepravek pošlete stejný požadavek s `"type": "P"`, `"to_postcode": "H4G1V5"` a s hmotností a rozměry stohu přepravek.

**GraphQL:** `ordersRate` ([Příručka GraphQL](/api/graphql/documentation#/orders/ordersRate)). Výsledkem je JSON skalár a nepřijímá selection set.

```graphql
mutation {
  ordersRate(
    type: "P"
    from_postcode: "H4R2N6"
    from_country: "CA"
    to_postcode: "H4G1V5"
    to_country: "CA"
    packages: [{ weight: 4, weight_unit: 2, length: 60, width: 40, height: 30, dimension_unit: 2 }]
  )
}
```

**Ověření:** `result` je `true` a `shipping_price` je číslo pro `type` `D` i `type` `P`. Vytvoření objednávky na tomto kroku nezávisí.

## 6. Vytvoření objednávky doručení

Každá potvrzená velkoobchodní objednávka se stane jednou objednávkou doručení. ERP uloží vrácené `id` a `tracking_number` ke svému řádku objednávky; každé další volání používá jedno z nich.

**REST:** `POST /api/v1/client/orderCreate` — [Příručka REST](/api/documentation#/paths/v1-client-orderCreate/post)

Pošlete hlavičku `Idempotency-Key`, jedinečnou pro každou velkoobchodní objednávku, aby opakování po vypršení časového limitu nemohlo vytvořit druhou objednávku.

```bash
curl -X POST https://YOUR_HOST/api/v1/client/orderCreate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: whs-20931-delivery" \
  -d '{
    "type": "D",
    "need_pick_up": 0,
    "ref": "WHS-20931",
    "name": "Marie Tremblay",
    "company_name": "Café Lumière",
    "telephone": "5145550142",
    "email": "commandes@cafelumiere.example",
    "address_1": "5400 Avenue du Parc",
    "city": "Montréal",
    "province": "QC",
    "postcode": "H2V4G7",
    "country": "Canada",
    "schedule_date": "2026-10-02",
    "time_window_start": "2026-10-02 06:00:00",
    "time_window_end": "2026-10-02 09:00:00",
    "packages": 1,
    "packagesDetail": [{
      "ref": "WHS-20931-1",
      "weight": 12,
      "weight_unit": 2,
      "length": 60,
      "width": 40,
      "height": 30,
      "dimension_unit": 2
    }],
    "delivery_instruction": "Deliver to the back door on Rue Saint-Viateur"
  }'
```

```json
{
  "result": true,
  "id": 12345,
  "ref": "WHS-20931",
  "shipping_price": "18.50",
  "currency": "CAD",
  "price_details": { "shipping_fee": "18.50" },
  "tracking_number": ["SR123456789012"],
  "packages": [
    { "id": 67890, "ref": "WHS-20931-1", "tracking_number": "SR123456789012", "external_tracking_number": "" }
  ]
}
```

| Pole | Význam |
|---|---|
| `type` | `D` doručení nebo `P` vyzvednutí |
| `need_pick_up` | `0` — zboží je již ve skladu. `1` — řidič musí balík vyzvednout |
| `ref` | Externí reference pro vyhledávání a odsouhlasení |
| `name` / adresa | Doručení: příjemce. Vyzvednutí: zastávka vyzvednutí |
| `schedule_date`, `time_window_start`, `time_window_end` | Datum doručení (`Y-m-d`) a časové okno, ve kterém musí být zastávka obsloužena (`Y-m-d H:i:s`) |
| `packagesDetail` | Jedna položka na balík; `ref` identifikuje balík ve vašem systému |
| `auto_deduplication` | `1` odmítne druhý balík se stejným `ref` balíku |

V odpovědi:

- `id`: id objednávky; uložte ho pro detail objednávky a volání zrušení.
- `tracking_number`: jedno sledovací číslo na balík; podle nich tiskněte a sledujte.
- `warning`: přítomné, když byla objednávka vytvořena s upozorněním, například u adresy mimo oblast doručení, kterou firma ponechává nebo pozastavuje. Ponechaná objednávka mimo oblast může vrátit `shipping_price: null`.

**GraphQL:** `clientOrderCreate` ([Příručka GraphQL](/api/graphql/documentation#/client/clientOrderCreate)). Výsledkem je JSON skalár se stejným tělem jako odpověď REST.

```graphql
mutation {
  clientOrderCreate(
    type: "D"
    need_pick_up: 0
    ref: "WHS-20931"
    name: "Marie Tremblay"
    company_name: "Café Lumière"
    telephone: "5145550142"
    address_1: "5400 Avenue du Parc"
    city: "Montréal"
    province: "QC"
    postcode: "H2V4G7"
    country: "Canada"
    packages: 1
    packagesDetail: [{ ref: "WHS-20931-1", weight: 12, weight_unit: 2, length: 60, width: 40, height: 30, dimension_unit: 2 }]
  )
}
```

**Ověření:** pošlete stejné tělo se stejným `Idempotency-Key` znovu. Odpověď nese stejné `id` a druhá objednávka se nevytvoří.

## 7. Vytvoření objednávky vyzvednutí

Objednávka vyzvednutí pošle řidiče vyzvednout zboží na adrese; v tomto případě prázdné přepravky v Épicerie Wellington. Používá stejný endpoint jako doručení: adresa je zastávka vyzvednutí, `type` je `P` a `need_pick_up` je `1`.

**REST:** `POST /api/v1/client/orderCreate` — [Příručka REST](/api/documentation#/paths/v1-client-orderCreate/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/client/orderCreate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crt-20931-pickup" \
  -d '{
    "type": "P",
    "need_pick_up": 1,
    "ref": "CRT-20931",
    "name": "Luc Gagnon",
    "company_name": "Épicerie Wellington",
    "telephone": "5145550187",
    "email": "luc@epiceriewellington.example",
    "address_1": "4100 Rue Wellington",
    "city": "Verdun",
    "province": "QC",
    "postcode": "H4G1V5",
    "country": "Canada",
    "packages": 1,
    "packagesDetail": [{
      "ref": "CRT-20931-1",
      "weight": 4,
      "weight_unit": 2,
      "length": 60,
      "width": 40,
      "height": 30,
      "dimension_unit": 2
    }],
    "pickup_instruction": "Empty crates are stacked at the loading door"
  }'
```

```json
{
  "result": true,
  "id": 12346,
  "ref": "CRT-20931",
  "shipping_price": "12.00",
  "currency": "CAD",
  "tracking_number": ["SR123456789029"],
  "packages": [
    { "id": 67891, "ref": "CRT-20931-1", "tracking_number": "SR123456789029", "external_tracking_number": "" }
  ]
}
```

- `id` a `tracking_number`: uložte je k vrácení přepravek, stejně jako u doručení.
- `pickup_instruction`: zobrazí se řidiči na zastávce vyzvednutí; `delivery_instruction` je jeho obdoba u doručení.

**Ověření:** detail objednávky (krok 8) ukazuje pro tuto objednávku `type` `P` a `need_pickup` `1`.

## 8. Načtení objednávky

Detail objednávky potvrdí, co bylo uloženo, a vrátí aktuální stav; endpoint seznamu umožňuje ERP odsouhlasit vlastní záznamy s platformou.

**REST:** `GET /api/v1/orders/{orderId}` — [Příručka REST](/api/documentation#/paths/v1-orders-orderId/get)

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

```json
{
  "business_name": "Farine & Fils",
  "order": {
    "id": 12345,
    "ref": "WHS-20931",
    "type": "D",
    "need_pickup": 0,
    "orders_status_id": 2,
    "name": "Marie Tremblay",
    "address_1": "5400 Avenue du Parc",
    "postcode": "H2V4G7",
    "time_window_start": "2026-10-02 06:00:00",
    "time_window_end": "2026-10-02 09:00:00",
    "shipping_price": "18.50"
  },
  "tracking_numbers": ["SR123456789012"],
  "packages": [
    { "id": 67890, "ref": "WHS-20931-1", "tracking_number": "SR123456789012" }
  ]
}
```

- `order.orders_status_id`: stav objednávky; `2` je Nová, `12` je Zrušená.
- `order.type` a `order.need_pickup`: potvrzují, zda byla zastávka uložena jako doručení nebo vyzvednutí.
- `tracking_numbers`: sledovací čísla balíků objednávky.

**REST:** `GET /api/v1/orders/list` — [Příručka REST](/api/documentation#/paths/v1-orders-list/get)

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

Seznam vrátí všechny objednávky účtu od nejnovější, každou s jejími balíky a položkami. Pro stránkování pošlete `page` a `per_page` společně (`per_page` nejvýše 1000); bez nich se vrátí nejnovějších 1000 objednávek s příznakem `truncated`.

**GraphQL:** `orders` ([Příručka GraphQL](/api/graphql/documentation#/orders/orders)) pro jednu objednávku a `ordersList` ([Příručka GraphQL](/api/graphql/documentation#/orders/ordersList)) pro seznam. Obě vracejí JSON skalár.

```graphql
query {
  orders(orderId: "12345")
}
```

**Ověření:** objednávka patří k přihlášenému účtu, `ref` odpovídá hodnotě odeslané při vytvoření a `tracking_numbers` odpovídá odpovědi na vytvoření.

## 9. Tisk místního štítku

Štítek nese sledovací čárový kód, který řidič naskenuje ve skladu a na zastávce. Vytiskněte jeden štítek na balík a připevněte ho na stoh přepravek.

**REST:** `POST /api/v1/shipping/getShippingLabel` — [Příručka REST](/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
  }'
```

```json
"JVBERi0xLjcKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZwo..."
```

- `type`: jak se čte `id`: `TRACKING_NUMBER` (výchozí), `ORDER_ID` nebo `REF`.
- `base64`: `0` (výchozí) odešle PDF jako proud. `1` změní celé tělo odpovědi na JSON řetězec nejvyšší úrovně obsahující PDF v base64, nikoli na objekt s polem `pdf_data`. Chcete-li dostat štítek v běžném JSON objektu, zavolejte místo toho `POST /api/v2/shipping/getShippingLabel` — [Příručka REST](/api/documentation#/paths/v2-shipping-getShippingLabel/post).
- `packages`: volitelné; počet štítků k tisku. Hodnota odlišná od počtu balíků objednávky objednávku aktualizuje.
- `hide_sender_address` / `hide_receiver_address`: `1` ponechá danou adresu na štítku prázdnou.

**GraphQL:** `shippingGetShippingLabel` ([Příručka GraphQL](/api/graphql/documentation#/shipping/shippingGetShippingLabel)). `shippingGetShippingLabelV2` ([Příručka GraphQL](/api/graphql/documentation#/shipping/shippingGetShippingLabelV2)) vždy vrací JSON (`pdf_data`).

**Ověření:** dekódované PDF se otevře. Štítek doručení ukazuje adresu Café Lumière; štítek vyzvednutí ukazuje adresu Épicerie Wellington. Skrytá adresa je na štítku prázdná.

## 10. Sledování objednávky

Veřejné sledování vrátí časovou osu událostí balíku. Nevyžaduje přístupový token, takže ho zákaznický portál může zobrazit přímo; spolu s ním přichází doklad o doručení nebo vyzvednutí.

**REST:** `GET /api/v1/tracking/{trackingNumber}` — [Příručka REST](/api/documentation#/paths/v1-tracking-trackingNumber/get)

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

```json
{
  "result": true,
  "postcode": "H2V4G7",
  "deliveried": false,
  "returntosender": false,
  "rejectedbyrecipient": false,
  "data": [
    {
      "tracking_event_status_id": 100,
      "description": "Order information submitted",
      "updated_at_localized": "2026-10-01 16:42:10"
    }
  ],
  "proofs": []
}
```

Stejná URL přijímá i váš `ref`, pokud byl uložen jako externí číslo.

Rozhodujte podle `tracking_event_status_id`, nikoli podle `description`; tento řetězec se řídí hlavičkou `Accept-Language`.

| `tracking_event_status_id` | Strana | Význam |
|---|---|---|
| `100` | obě | Objednávka přijata |
| `300` / `301` | doručení | V provozovně |
| `450` | doručení | Na cestě k příjemci |
| `500` | doručení | Doručeno |
| `501` | doručení | Doručení selhalo, je třeba nový plán |
| `460` | vyzvednutí | Na cestě k vyzvednutí |
| `510` | vyzvednutí | Vyzvednuto |
| `512` | vyzvednutí | Vyzvednutí selhalo, zkusit později |
| `513` | vyzvednutí | Problém s vyzvednutím |

- `data`: od nejnovější; první řádek je aktuální stav.
- `deliveried`: `true` po `500`.
- `proofs[]`: u `500` nebo `510` může obsahovat `type` `1` (podpis) nebo `2` (fotografie) s `file_id` a `signed_url`. Fotografie nahraná po této události v tomto obsahu není; přihlaste se k odběru `pod.files_updated` (krok 11).

**GraphQL:** `trackingPublic` ([Příručka GraphQL](/api/graphql/documentation#/tracking/trackingPublic)). Výsledek je typovaný a vyžaduje selection set.

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

**Ověření:** hned po vytvoření je nejnovější událost `100` a `deliveried` je `false`. Neznámé číslo vrátí `result: false` s `404`; zobrazte stav nenalezeno a nevytvářejte umělé sledovací události.

## 11. Příjem webhooků

Webhooky posílají každou změnu na váš server, takže ERP a zákaznický portál zůstávají aktuální bez opakovaného dotazování. Zaregistrujte URL zpětných volání, která tento tok potřebuje:

| Nastavení | Událost | Použití |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Uložení `id` a `tracking_number` |
| `order_status_change_webhook_url` | `order.status_change` | Stav pro zákazníka |
| `tracking_event_webhook_url` | `tracking.event` | Časová osa vyzvednutí nebo doručení |
| `pod_files_webhook_url` | `pod.files_updated` | Fotografie nebo podpis po vyzvednutí nebo doručení |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | Vámi odeslané zrušení bylo odmítnuto |
| `order_create_async_postback_url` | `order.create_async` | Výsledek asynchronní dávky (krok 13) |

**REST:** `PUT /api/v1/webhook-settings` — [Příručka REST](/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 '{
    "order_create_webhook_url": "https://erp.farineetfils.example/hooks/superroute",
    "order_status_change_webhook_url": "https://erp.farineetfils.example/hooks/superroute",
    "tracking_event_webhook_url": "https://erp.farineetfils.example/hooks/superroute",
    "pod_files_webhook_url": "https://erp.farineetfils.example/hooks/superroute",
    "order_cancel_failed_webhook_url": "https://erp.farineetfils.example/hooks/superroute",
    "webhook_sign_secret": "YOUR_LONG_RANDOM_SECRET",
    "webhook_verify_ssl": 1
  }'
```

```json
{
  "result": true,
  "changed_keys": [
    "order_create_webhook_url",
    "order_status_change_webhook_url",
    "tracking_event_webhook_url",
    "pod_files_webhook_url",
    "order_cancel_failed_webhook_url",
    "webhook_sign_secret"
  ],
  "settings": {
    "webhook_sign_secret": "************CRET",
    "tracking_event_webhook_url": "https://erp.farineetfils.example/hooks/superroute",
    "webhook_verify_ssl": 1
  }
}
```

- `changed_keys`: nastavení, která toto volání změnilo.
- `settings.webhook_sign_secret`: vrací se zamaskované; úplnou hodnotu uchovávejte pouze na svém serveru.

**GraphQL:** `webhookSettingsUpdate` ([Příručka GraphQL](/api/graphql/documentation#/webhooks/webhookSettingsUpdate)).

Na straně příjemce ověřte podpis **v2** nad nezpracovaným tělem: `HMAC_SHA256(timestamp + "." + raw_body, secret)` porovnaný s `X-Webhook-Signature-V2`, kde časová značka je `X-Webhook-Timestamp`. Duplicity odstraňujte podle `X-Webhook-Event-Id`. Odpovězte **2xx do 3 sekund** a událost zpracujte až poté.

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

**Ověření:** vytvořte jednu testovací objednávku a přijměte `order.created` se stejným `id` a `tracking_number`. Příjemce odmítne neplatný podpis kódem `401` a druhé doručení stejného `X-Webhook-Event-Id` se nezpracuje dvakrát.

## 12. Zrušení objednávky

Objednávku zrušte, když je velkoobchodní objednávka stažena nebo vyzvednutí přepravek již není potřeba. Volání je idempotentní: zrušení již zrušené objednávky opět uspěje.

**REST:** `POST /api/v1/orders/cancel` — [Příručka REST](/api/documentation#/paths/v1-orders-cancel/post) — pošlete právě jedno z `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"}'
```

```json
{
  "result": true,
  "id": 12345,
  "message": "Order 12345 has been cancelled successful.",
  "already_cancelled": false
}
```

- `result`: `true`, když je objednávka zrušena.
- `already_cancelled`: `true`, když byla objednávka zrušena před tímto voláním; považujte to za úspěch.
- `code`: přítomné, když je zrušení odmítnuto; viz krok 14.

**GraphQL:** `ordersCancel` ([Příručka GraphQL](/api/graphql/documentation#/orders/ordersCancel)). Výsledek je typovaný a vyžaduje selection set.

```graphql
query {
  ordersCancel(tracking_number: "SR123456789012") {
    result
    id
    message
    already_cancelled
    code
  }
}
```

**Ověření:** detail objednávky ukazuje `orders_status_id` `12` a stejné zrušení vrátí `already_cancelled: true`. Když je zrušení odmítnuto, na `order_cancel_failed_webhook_url` se odešle `order.cancel_failed`.

## 13. Dávkové vytvoření objednávek (volitelně)

ERP může odeslat velkoobchodní objednávky a vyzvednutí přepravek za celý den v jednom požadavku. Každý řádek přijímá stejná pole jako kroky 6 a 7 a může mít `type` `D` nebo `P`.

**REST:** `POST /api/v1/client/batchOrderCreate` — [Příručka REST](/api/documentation#/paths/v1-client-batchOrderCreate/post) — odpoví, když jsou zpracovány všechny řádky.

```bash
curl -X POST https://YOUR_HOST/api/v1/client/batchOrderCreate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: batch-2026-10-01" \
  -d '{
    "per_order_transaction": 1,
    "orders": [
      {
        "type": "D",
        "need_pick_up": 0,
        "ref": "WHS-20932",
        "name": "Sophie Roy",
        "company_name": "Boulangerie du Marché",
        "telephone": "5145550163",
        "address_1": "7070 Avenue Henri-Julien",
        "city": "Montréal",
        "province": "QC",
        "postcode": "H2S3S3",
        "country": "Canada",
        "packages": 1,
        "packagesDetail": [{ "ref": "WHS-20932-1", "weight": 10, "weight_unit": 2, "length": 60, "width": 40, "height": 30, "dimension_unit": 2 }]
      },
      {
        "type": "P",
        "need_pick_up": 1,
        "ref": "CRT-20932",
        "name": "Luc Gagnon",
        "company_name": "Épicerie Wellington",
        "telephone": "5145550187",
        "address_1": "4100 Rue Wellington",
        "city": "Verdun",
        "province": "QC",
        "postcode": "H4G1V5",
        "country": "Canada",
        "packages": 1,
        "packagesDetail": [{ "ref": "CRT-20932-1", "weight": 4, "weight_unit": 2, "length": 60, "width": 40, "height": 30, "dimension_unit": 2 }]
      }
    ]
  }'
```

```json
[
  { "result": true, "id": 12347, "ref": "WHS-20932", "tracking_number": ["SR123456789036"], "packages": [{ "id": 67892, "ref": "WHS-20932-1", "tracking_number": "SR123456789036", "external_tracking_number": "" }] },
  { "result": true, "id": 12348, "ref": "CRT-20932", "tracking_number": ["SR123456789043"], "packages": [{ "id": 67893, "ref": "CRT-20932-1", "tracking_number": "SR123456789043", "external_tracking_number": "" }] }
]
```

- Každý řádek má vlastní `result`; přiřaďte ho ke svému řádku objednávky podle `ref`. Odmítnutý řádek nese `message` a `skipped_ref` a může nést `code` (například `INSUFFICIENT_BALANCE` nebo `OUT_OF_DELIVERY_AREA`).
- `per_order_transaction`: `1` potvrdí každý řádek samostatně, takže jeden neúspěšný řádek nemůže vrátit zpět ostatní.
- Dávky s více než 100 objednávkami dostanou hlavičku odpovědi `X-Batch-Size-Warning`; posílejte je na asynchronní endpoint.

**REST:** `POST /api/v1/client/batchOrderCreateAsync` — [Příručka REST](/api/documentation#/paths/v1-client-batchOrderCreateAsync/post) — přijímá stejné tělo a okamžitě vrátí identifikátor úlohy:

```json
{ "message": "Order batch created in async,please check later.", "asyncId": 28 }
```

Dotazujte se na `GET /api/v1/client/async/{id}` — [Příručka REST](/api/documentation#/paths/v1-client-async-id/get) — s `asyncId` nebo přijměte `order.create_async` na `order_create_async_postback_url`. Výsledkem úlohy je stejný seznam po řádcích jako u synchronního endpointu.

```bash
curl https://YOUR_HOST/api/v1/client/async/28 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

**GraphQL:** `clientBatchOrderCreate` ([Příručka GraphQL](/api/graphql/documentation#/client/clientBatchOrderCreate)), `clientBatchOrderCreateAsync` ([Příručka GraphQL](/api/graphql/documentation#/client/clientBatchOrderCreateAsync)) a `clientAsync` ([Příručka GraphQL](/api/graphql/documentation#/client/clientAsync)).

**Ověření:** dávka se dvěma řádky vrátí dva výsledky, každý s jeho `ref`. Asynchronní úloha po spuštění vrátí stejné řádky.

## 14. Zpracování chyb

| Situace | Stav HTTP | Kód | Co integrace udělá |
|---|---|---|---|
| Povinné pole chybí nebo má nesprávný formát (vytvoření) | 400 | `VALIDATION_FAILED` | Opravte pole uvedené v `message` a požadavek odešlete znovu. |
| Zůstatek účtu nepokrývá objednávku | 400 | `INSUFFICIENT_BALANCE` | Přečtěte `insufficient_balance` (požadováno, dostupné, chybí); dobijte zůstatek a poté opakujte. Žádná objednávka nebyla vytvořena. |
| Adresa je mimo oblast služby a firma takové objednávky odstraňuje | 400 | `OUT_OF_DELIVERY_AREA` | Zadejte adresu v oblasti služby. Žádná objednávka nebyla vytvořena. |
| `ref` balíku nebo externí sledovací číslo již existuje (se zapnutou deduplikací) | 200 (`result` `false`), nebo 409 se `strict_duplicate_check` `1` | `DUPLICATE_TRACKING_NUMBER` | Přečtěte `exist_package_ref` a místo vytvoření nové objednávky propojte existující. |
| `Idempotency-Key` je použit znovu s jiným tělem | 409 | `IDEMPOTENCY_CONFLICT` | Pro jiný požadavek použijte nový klíč. |
| Požadavek se stejným `Idempotency-Key` ještě probíhá | 409 | `IDEMPOTENCY_IN_PROGRESS` | Počkejte a poté opakujte se stejným klíčem. |
| Zrušení bez identifikátoru objednávky | 400 | `MISSING_IDENTIFIER` | Pošlete jedno z `order_id`, `tracking_number`, `external_tracking_number`. |
| Zrušení objednávky, která neexistuje | 400 | `ORDER_NOT_FOUND` | Zkontrolujte uložené `id` nebo sledovací číslo. |
| Číslo odpovídá více než jedné aktivní objednávce | 409 | `MULTIPLE_ORDERS_MATCHED` | Zrušte podle `order_id` s použitím jednoho z `matched_order_ids`. |
| Objednávka patří jinému účtu | 401 | `ORDER_CANCEL_UNAUTHORIZED` | Zrušte účtem, který objednávku vytvořil. |
| Stav objednávky již zrušení nepovoluje | 401 | `ORDER_STATUS_NOT_CANCELLABLE` | Objednávku ponechte beze změny; vrácení řešte samostatně. |
| Objednávku drží externí dopravce, který ji nemůže zrušit | 409 | `ORDER_CANCEL_BLOCKED_THIRD_PARTY`, `THIRD_PARTY_CANCEL_NOT_SUPPORTED` nebo `THIRD_PARTY_CANCEL_FAILED` | Objednávka je nezměněná; kontaktujte firmu. |
| Token chybí nebo vypršel, nebo účet nesmí zadávat objednávky | 401 | — | Přihlaste se znovu; zkontrolujte oprávnění účtu. |

## Seznam testů

Použijte testovací reference, například `WHS-20931` a `CRT-20931`:

- [ ] (Volitelně) Cenová nabídka vrátí cenu pro PSČ v oblasti s `type` `D`.
- [ ] (Volitelně) Cenová nabídka vrátí cenu pro PSČ v oblasti s `type` `P`.
- [ ] Vytvoření doručení vrátí `id` + `tracking_number`; stejný `Idempotency-Key` nevytvoří druhou objednávku.
- [ ] Vytvoření vyzvednutí vrátí `id` + `tracking_number`; detail objednávky ukazuje `type` `P` a `need_pickup` `1`.
- [ ] Detail objednávky i seznam ukazují obě objednávky pod tímto účtem.
- [ ] PDF místního štítku se otevře a ukazuje příjemce nebo adresu vyzvednutí.
- [ ] Veřejné sledování vrátí časovou osu bez tokenu; nejnovější událost je `100`.
- [ ] Přijde `order.created` a jeho podpis v2 se ověří.
- [ ] Zrušení vrátí `result: true` a druhé zrušení vrátí `already_cancelled: true`.
- [ ] Dávka s jedním doručením a jedním vyzvednutím vrátí dva výsledky, každý s jeho `ref`.
