# Skladování a výdej

API skladování a výdeje umožňuje zákaznickému účtu skladové firmy přihlásit zboží k uskladnění, zaplatit období skladování a později odeslat uskladněné balíky vlastním kupujícím. Je určeno pro obchodníky a platformy, které drží zásoby ve skladu třetí strany (3PL) a potřebují ze svých systémů automatizovat rezervaci skladování, přehled zásob a odchozí zásilky. Všechna volání se provádějí jako zákaznický účet, nikdy jako skladová firma.

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

Příklady v této příručce sledují jeden scénář. **Northwind Outdoor**, sezónní internetový prodejce zimního vybavení, skladuje své zimní zásoby ve skladu **Toronto Hub** (sklad `7`) svého poskytovatele 3PL od 1. listopadu 2026 do 31. března 2027. Když kupující objedná karton zateplených bund, Northwind odešle tento karton ze zásob kupujícímu v Ottawě.

- **Rezervace sezónního skladování.** Administrativa prodejce získá cenovou nabídku a rezervuje období skladování pro každý přicházející karton ještě předtím, než zboží opustí dodavatele, a poplatek za skladování zaplatí ze zůstatku svého účtu.
- **Aktuální přehled zásob.** Obchod nebo ERP prodejce uvádí balíky, které sklad skutečně přijal a které jsou ještě dostupné k odeslání, takže k vyřízení se nabízejí pouze skutečné zásoby.
- **Vyřizování objednávek ze zásob.** Když kupující zadá objednávku, systém prodejce ocení odchozí zásilku, vytvoří požadavek na výdej uskladněných balíků, zaplatí ho a zaznamená sledovací číslo pro kupujícího.
- **Sledování stavu a opravy.** Systém prodejce načítá stav každé objednávky skladování a každého výdeje, sleduje zásilku přes veřejné sledování a ruší výdej, který již není potřeba, dokud je to ještě povoleno.

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

Tuto příručku použijte, když je zboží již uskladněno nebo bude uskladněno ve skladu firmy a zásilka začíná z těchto zásob. Tok je: přihlášení → načtení konfigurace skladování → cenová nabídka skladování → vytvoření objednávky skladování → platba → seznam balíků na skladě → seznam služeb a odhad výdeje → vytvoření výdeje → platba → čtení a sledování → webhooky → zrušení.

Jiné případy pokrývají jiné příručky:

- **Uniorder: jedno API pro každou zásilku** — doporučený jediný vstupní bod (`/api/v1/uniorder/...`) pro nové integrace, které rezervují místní doručení nebo štítky dopravce. Uniorder **nepokrývá** skladování a výdej; objednávky skladování a výdeje se vytvářejí pouze přes zákaznické endpointy v této příručce.
- **Přepravní služby** — zákazník odesílá zboží, které není uskladněno, s použitím přepravních služeb firmy.
- **Štítky dopravce** — firma kupuje štítky dopravců přímo pro vlastní balíky.
- **Vyzvednutí a doručení (vlastní flotila)** — firma rezervuje vyzvednutí a doručení vlastní flotilou.

## 3. Než začnete

- **Typ účtu.** **Zákaznický** účet skladové firmy (poskytovatelem služby je firma, která provozuje sklad). Token firemního (klientského) účtu na endpointech `/api/v1/customer/...` nefunguje.
- **Oprávnění.** Zákaznický účet potřebuje přístup k API. Endpointy skladování navíc vyžadují funkci skladování; endpointy výdeje vyžadují, aby firma pro tohoto zákazníka zapnula výdej (nebo konsolidaci), jinak odpovědí `403`.
- **Zůstatek.** Platby za skladování a výdej se strhávají ze zůstatku zákaznického účtu. Pro test požádejte firmu, aby připsala prostředky na zůstatek testovacího zákazníka.
- **Testovací data.** `id` skladu, alespoň jedno `id` balení, pokud nejsou povoleny vlastní balíky, a alespoň jedna aktivní přepravní služba dostupná z tohoto skladu. Výdej funguje až poté, co sklad uskladněné balíky **přijal**; při testu požádejte personál skladu, aby testovací objednávku skladování přijal.
- **Zacházení s tokenem.** Přihlašujte se ze svého serveru, token uchovávejte na serveru a nikdy ho nevkládejte do kódu prohlížeče ani mobilní aplikace.
- **Zástupné hodnoty.** Nahraďte `YOUR_HOST` hostitelem vaší platformy a `ACCESS_TOKEN` tokenem z kroku 4.

## 4. Přihlášení jako zákazník

Každé další volání se autorizuje zákaznickým tokenem typu bearer. Vaše integrace se přihlásí jednou, uloží token na straně serveru a obnoví ho před `expires_at`.

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

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

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

- `access_token` — posílejte ho u každého požadavku v hlavičce uvedené níže.
- `expires_at` / `expires_timestamp` — před tímto časem se přihlaste znovu.

```
Authorization: Bearer ACCESS_TOKEN
```

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

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

## 5. Načtení konfigurace skladování

Balík konfigurace uvádí sklady, které může zákazník používat, katalog balení, jednotky a příplatky. Vaše integrace ho načte jednou za relaci, aby vybrala sklad a sestavila platné řádky balíků.

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

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

```json
{
  "result": true,
  "data": {
    "warehouses": [
      { "id": 7, "name": "Toronto Hub", "address": "10 Main St", "city": "Toronto", "province": "ON", "postcode": "M5V 2T6" }
    ],
    "packagings": [
      { "id": 1, "name": "Large Carton", "type": "Box", "length": 60, "width": 40, "height": 40, "dimension_unit": 2 }
    ],
    "dimension_units": { "1": { "name": "dimension_mm" }, "2": { "name": "dimension_cm" }, "3": { "name": "dimension_m" }, "4": { "name": "dimension_inch" } },
    "weight_units": { "1": { "name": "weight_g" }, "2": { "name": "weight_kg" }, "3": { "name": "weight_oz" }, "4": { "name": "weight_lb" } },
    "allow_custom_package": true,
    "surcharges": [],
    "form_bindings": []
  }
}
```

- `warehouses[].id` — `warehouse_id` pro každé další volání.
- `allow_custom_package` — když je `false`, každá položka skladování musí nést `packaging_id` z `packagings[]`; když je `true`, položky lze popsat pouze rozměry.
- `dimension_units` / `weight_units` — celočíselné kódy používané v řádcích balíků (`2` = cm, `2` = kg).
- `form_bindings` — formuláře, které firma vyžaduje u objednávky skladování; jejich odpovědi pošlete jako `form_data` v kroku 7.

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

```graphql
query {
  customerStorageOrderConfig
}
```

**Ověření:** zaznamenali jste `id` skladu a, pokud katalog není prázdný, `id` balení.

## 6. Cenová nabídka období skladování

Cenová nabídka ocení období skladování pro plánované balíky ještě před jakoukoli rezervací. Vaše integrace tuto cenu zobrazí nebo zkontroluje a poté vytvoří objednávku se stejnými vstupy.

**REST:** `POST /api/v1/customer/storage-orders/calculate-price` — [Příručka REST](/api/documentation#/paths/v1-customer-storage-orders-calculate-price/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders/calculate-price \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": 7,
    "start_date": "2026-11-01",
    "end_date": "2027-03-31",
    "items": [{
      "qty": 2,
      "length": 60,
      "width": 40,
      "height": 40,
      "dimension_unit": 2,
      "weight": 14,
      "weight_unit": 2
    }]
  }'
```

```json
{
  "success": true,
  "price": {
    "total_price": "186.45",
    "currency": "CAD",
    "shipping_price": 186.45
  }
}
```

- `success` — `true`, když byla cena vypočtena.
- `price.total_price` / `price.currency` — cena skladování za období včetně daně.
- `promotion` — přítomné pouze tehdy, když se uplatní akce.

**Ověření:** `success` nebo `result` je true a máte cenu. Chybějící `warehouse_id` / data vrátí `400`.

## 7. Vytvoření objednávky skladování

Objednávka skladování ohlásí skladu přicházející balíky a stanoví období skladování. Vaše integrace uloží vrácené id; je potřeba pro platbu, načtení objednávky a její zrušení.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: northwind-winter-2026-po-4471" \
  -d '{
    "warehouse_id": 7,
    "start_date": "2026-11-01",
    "end_date": "2027-03-31",
    "notes": "Winter 2026 stock, PO 4471",
    "items": [{
      "description": "Insulated jackets, carton of 12",
      "qty": 2,
      "length": 60,
      "width": 40,
      "height": 40,
      "dimension_unit": 2,
      "weight": 14,
      "weight_unit": 2,
      "value": 1800
    }]
  }'
```

```json
{
  "result": true,
  "message": "Storage order created",
  "data": { "id": 1024, "status": "pending payment" }
}
```

- `data.id` — id objednávky skladování. Uložte ho ke své nákupní objednávce.
- `data.status` — `pending payment`, dokud objednávka není zaplacena.
- `Idempotency-Key` — odvoďte ho od vlastního stabilního id. Opakovaný klíč se stejným tělem vrátí první odpověď (`replayed: true`); stejný klíč s jiným tělem se odmítne s `409 IDEMPOTENCY_CONFLICT`.
- Povinná pole: `warehouse_id`, `start_date`, `end_date` (po `start_date`) a `items[]` s `qty`, `length`, `width`, `height`, `dimension_unit`. Když je `allow_custom_package` `false`, přidejte `items[].packaging_id`.

**Ověření:** odpověď obsahuje `data.id`. Toto id objednávky skladování uložte.

## 8. Platba za skladování

Platba potvrdí objednávku skladování. Vaše integrace může nejprve načíst splatnou částku a poté zaplatit ze zůstatku zákazníka.

**REST:** `GET /api/v1/customer/storage-orders/{id}/payment-info` — [Příručka REST](/api/documentation#/paths/v1-customer-storage-orders-id--payment-info/get) (volitelné)

```bash
curl https://YOUR_HOST/api/v1/customer/storage-orders/1024/payment-info \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "success": true,
  "result": true,
  "data": {
    "order_id": 1024,
    "currency": "CAD",
    "total_price": "186.45",
    "paid_amount": "0.00",
    "remaining_balance": "186.45",
    "user_balance": "500.00",
    "has_sufficient_balance": true,
    "minimum_payment": "186.45"
  }
}
```

**REST:** `POST /api/v1/customer/storage-orders/{id}/pay` — [Příručka REST](/api/documentation#/paths/v1-customer-storage-orders-id--pay/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders/1024/pay \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"payment_type": "full_balance"}'
```

```json
{
  "success": true,
  "result": true,
  "message": "Payment of $186.45 processed successfully. Your storage order is now confirmed.",
  "new_balance": 313.55,
  "paid_amount": 186.45,
  "charge_amount": 186.45,
  "is_fully_paid": true
}
```

- `payment_type` — `full_balance` (výchozí, zaplatí zbývající částku), `minimum_payment` (zaplatí minimum, které firma vyžaduje) nebo `custom` spolu s `custom_amount`.
- `is_fully_paid` — `true`, když nezbývá nic k zaplacení.
- Nedostatečný zůstatek vrátí `400` s `customer_balance`; dobijte zůstatek a poté opakujte.

Načtěte objednávku, abyste potvrdili její stav a později také to, které balíky sklad přijal.

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

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

```json
{
  "result": true,
  "data": {
    "id": 1024,
    "status": "confirmed",
    "can_cancel": true,
    "store_from": "2026-11-01",
    "store_to": "2027-03-31",
    "warehouse_id": 7,
    "packages": [
      { "id": 5001, "description": "Insulated jackets, carton of 12", "quantity": 1, "received": false },
      { "id": 5002, "description": "Insulated jackets, carton of 12", "quantity": 1, "received": false }
    ],
    "total_price": 186.45,
    "currency": "CAD"
  }
}
```

- `status` — `confirmed` po platbě; později `partial received` / `storage in progress` podle příchodu zboží.
- `packages[].received` — `true`, když sklad daný balík přijal.
- `can_cancel` — zda lze objednávku skladování ještě zrušit.

**GraphQL:** `customerStorageOrderShow` ([Příručka GraphQL](/api/graphql/documentation#/customer/customerStorageOrderShow)); seznam všech objednávek skladování je `customerStorageOrders` ([Příručka GraphQL](/api/graphql/documentation#/customer/customerStorageOrders)).

```graphql
query {
  customerStorageOrderShow(id: 1024) {
    result
    data {
      id
      status
      can_cancel
      packages { id description received }
    }
  }
}
```

**Ověření:** objednávka skladování je zaplacena / potvrzena. `400` s `customer_balance` znamená, že je třeba dobít zůstatek a poté opakovat.

Výdej uvedený níže funguje až poté, co jsou balíky ve skladu **přijaty**. Při testu počkejte, dokud je personál (nebo testovací příjem) neoznačí jako přijaté, a poté pokračujte.

## 9. Seznam položek, které jsou ještě na skladě

Tento seznam představuje zásoby, které může vaše integrace odeslat. Obsahuje pouze balíky, které sklad přijal a které ještě nejsou vázány na jiný výdej.

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

```bash
curl "https://YOUR_HOST/api/v1/customer/shipout-orders/available-items?warehouse_id=7" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "data": {
    "warehouses": [{ "id": 7, "name": "Toronto Hub", "available_count": 2 }],
    "storage_orders": [{
      "id": 1024,
      "warehouse_id": 7,
      "packages": [
        { "id": 5001, "description": "Insulated jackets, carton of 12", "weight": 14, "weight_unit": 2, "length": 60, "width": 40, "height": 40, "dimension_unit": 2, "value": 1800 },
        { "id": 5002, "description": "Insulated jackets, carton of 12", "weight": 14, "weight_unit": 2, "length": 60, "width": 40, "height": 40, "dimension_unit": 2, "value": 1800 }
      ]
    }]
  }
}
```

- `storage_orders[].packages[].id` — `storage_package_ids` k výdeji v kroku 10.
- `warehouses[].available_count` — počet dostupných balíků v každém skladu.

**GraphQL:** `customerShipoutAvailableItems` ([Příručka GraphQL](/api/graphql/documentation#/storage-shipout/customerShipoutAvailableItems))

```graphql
query {
  customerShipoutAvailableItems(warehouse_id: 7) {
    result
    data {
      warehouses { id name available_count }
      storage_orders { id warehouse_id packages { id description weight length width height } }
    }
  }
}
```

**Ověření:** zaznamenali jste jedno nebo více `storage_package_ids` (příklad `5001`). Prázdný seznam znamená, že zatím nebylo nic přijato — výdej nevytvářejte. `403` znamená, že výdej je pro tohoto zákazníka vypnutý.

## 10. Odhad a vytvoření výdeje

Výdej se oceňuje přepravní službou firmy. Vaše integrace načte služby dostupné ze skladu, odhadne cenu pro cíl kupujícího a poté vytvoří výdej pro vybrané balíky.

**REST:** `GET /api/v1/customer/shipout-orders/services?warehouse_id=7` — [Příručka REST](/api/documentation#/paths/v1-customer-shipout-orders-services/get)

```bash
curl "https://YOUR_HOST/api/v1/customer/shipout-orders/services?warehouse_id=7" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "data": {
    "services": [
      { "id": 5, "service_code": "intl_express", "name": { "en": "Express" }, "pricing_method": 1, "pricing_method_name": "Shipping Price Plan", "support_multi_package": true }
    ]
  }
}
```

Zaznamenejte `service_code`.

**REST:** `POST /api/v1/customer/shipout-orders/services/{serviceCode}/estimate` — [Příručka REST](/api/documentation#/paths/v1-customer-shipout-orders-services-serviceCode--estimate/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/services/intl_express/estimate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": 7,
    "delivery_postcode": "K2P1L4",
    "delivery_country": "CA",
    "packages": [{
      "weight": 14,
      "length": 60,
      "width": 40,
      "height": 40,
      "weight_unit": 2,
      "dimension_unit": 2
    }]
  }'
```

```json
{
  "result": true,
  "data": {
    "shipping_fee": 38.5,
    "fuel_surcharge": 4.2,
    "handling_fee": 0,
    "sub_total": 42.7,
    "tax": 5.55,
    "total": 48.25,
    "currency": "CAD",
    "has_items_needing_quote": false,
    "refused": false
  }
}
```

- `total` / `currency` — odhadovaná cena pro tento cíl.
- `has_items_needing_quote` — `true`, když se služba oceňuje ručně; sklad stanoví cenu po vytvoření výdeje a platba na ni čeká.
- `refused` / `refusal_message` — služba tuto zásilku nepřijme, protože ji nedokáže ocenit.

**REST:** `POST /api/v1/customer/shipout-orders/services/{serviceCode}/orders` — [Příručka REST](/api/documentation#/paths/v1-customer-shipout-orders-services-serviceCode--orders/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/services/intl_express/orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: northwind-order-NW-20931" \
  -d '{
    "warehouse_id": 7,
    "storage_package_ids": [5001],
    "delivery_name": "Maya Chen",
    "delivery_telephone": "6135550142",
    "delivery_email": "maya.chen@example.com",
    "delivery_address_1": "150 Elgin St",
    "delivery_city": "Ottawa",
    "delivery_province": "ON",
    "delivery_country": "CA",
    "delivery_postcode": "K2P1L4",
    "note": "Web order NW-20931"
  }'
```

```json
{
  "result": true,
  "data": {
    "id": 8001,
    "status": 0,
    "is_storage_shipout": true,
    "total_price": "48.25",
    "price_breakdown": { "total": 48.25, "has_items_needing_quote": false },
    "has_items_needing_quote": false,
    "storage_package_ids": [5001]
  }
}
```

- `data.id` — id výdeje. Uložte ho k objednávce kupujícího.
- `data.status` — `0` = čekající (čeká na platbu), `1` = potvrzený, `2` = na cestě, `3` = odeslaný, `4` = zrušený, `5` = neúspěšný.
- `storage_package_ids` — tyto balíky jsou nyní vázány na tento výdej a v kroku 9 se již nezobrazují.
- Povinná pole: `warehouse_id`, `storage_package_ids`, `delivery_name`, `delivery_telephone`, `delivery_address_1`, `delivery_city`, `delivery_province`, `delivery_country`, `delivery_postcode`. Všechny balíky musí pocházet ze stejného skladu.

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

```graphql
mutation {
  customerCreateShipoutOrder(
    service_code: "intl_express"
    warehouse_id: 7
    storage_package_ids: [5001]
    delivery_name: "Maya Chen"
    delivery_telephone: "6135550142"
    delivery_email: "maya.chen@example.com"
    delivery_address_1: "150 Elgin St"
    delivery_city: "Ottawa"
    delivery_province: "ON"
    delivery_country: "CA"
    delivery_postcode: "K2P1L4"
    note: "Web order NW-20931"
  ) {
    result
    message
    data { id status total_price has_items_needing_quote storage_package_ids }
  }
}
```

**Ověření:** odpověď obsahuje `id` výdeje. Vybrané uskladněné balíky jsou vázány na tento požadavek.

## 11. Platba za výdej

Sklad zpracuje výdej po jeho zaplacení. Vaše integrace zaplatí zbývající částku z účtu zákazníka; chcete-li zaplatit celou částku, vynechte `amount`.

**REST:** `POST /api/v1/customer/shipout-orders/{id}/pay` — [Příručka REST](/api/documentation#/paths/v1-customer-shipout-orders-id--pay/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/8001/pay \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{
  "result": true,
  "data": {
    "transaction_id": 9001,
    "amount": "48.25",
    "order_status": 1,
    "paid_amount": 48.25,
    "remaining_balance": 0
  }
}
```

- `amount` (požadavek, volitelné) — částečná částka; ve výchozím stavu celá zbývající částka.
- `order_status` — `1` (potvrzený) po úplné platbě.
- `remaining_balance` — `0` při úplném zaplacení.

**GraphQL:** `customerPayShipout` ([Příručka GraphQL](/api/graphql/documentation#/storage-shipout/customerPayShipout))

```graphql
mutation {
  customerPayShipout(id: 8001) {
    result
    message
    data { transaction_id amount order_status paid_amount remaining_balance }
  }
}
```

**Ověření:** platba zaznamená částku (nebo vrátí `402` / `422` s jasným důvodem). `402` znamená nedostatečný zůstatek; `422` znamená, že objednávku ještě nelze zaplatit (například stále čeká na ruční cenovou nabídku) nebo je částka neplatná.

## 12. Čtení a sledování výdeje

Vaše integrace načítá výdej, aby sledovala jeho stav, a po odeslání ze skladu sleduje zásilku podle jejího sledovacího čísla.

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

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

```json
{
  "result": true,
  "data": {
    "id": 8001,
    "status": 3,
    "status_label": "Shipped",
    "warehouse": { "id": 7, "name": "Toronto Hub" },
    "shipping_service": { "id": 5, "service_code": "intl_express" },
    "total_price": "48.25",
    "paid_amount": 48.25,
    "remaining_balance": 0,
    "can_be_paid": false,
    "can_be_cancelled": false,
    "storage_packages": [{ "id": 5001, "storage_order_id": 1024, "description": "Insulated jackets, carton of 12" }]
  }
}
```

- `status` / `status_label` — aktuální stav výdeje.
- `can_be_paid` / `can_be_cancelled` — zda je krok 11 nebo krok 14 aktuálně povolen.

Když existuje sledovací číslo:

**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,
  "deliveried": false,
  "data": [
    { "tracking_event_status_id": 3, "description": "Package picked up", "location_city": "Toronto", "updated_at": "2026-12-02 14:30:00" }
  ]
}
```

- `data[]` — sledovací události v chronologickém pořadí.
- `deliveried` — `true` po doručení zásilky.

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

```graphql
query {
  trackingPublic(trackingNumber: "SR123456789012") {
    result
    deliveried
    data { tracking_event_status_id description location_city updated_at }
  }
}
```

**Ověření:** načtení výdeje vrátí očekávaný `status`. Veřejné sledování najde zásilku, jakmile existuje číslo.

## 13. Odběr webhooků

Webhooky posílají změny sledování a stavu na váš server místo opakovaného dotazování. Zákaznický účet si nastavuje vlastní URL webhooků a podpisové tajemství; nastavení se ukládají k zákaznickému účtu, nikoli k firmě.

**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 '{
    "tracking_event_webhook_url": "https://hooks.northwind-outdoor.example/tracking",
    "order_status_change_webhook_url": "https://hooks.northwind-outdoor.example/status",
    "webhook_sign_secret": "nw-webhook-secret-2026-0123456789"
  }'
```

```json
{
  "result": true,
  "changed_keys": ["tracking_event_webhook_url", "order_status_change_webhook_url", "webhook_sign_secret"],
  "recipient_type": "customer",
  "settings": {
    "webhook_sign_secret": "************6789",
    "tracking_event_webhook_url": "https://hooks.northwind-outdoor.example/tracking",
    "order_status_change_webhook_url": "https://hooks.northwind-outdoor.example/status"
  }
}
```

- Mění se pouze odeslané klíče; neznámý klíč nebo neplatná URL vrátí `400`.
- `recipient_type` — `customer` potvrzuje, že nastavení patří zákaznickému účtu.
- `webhook_sign_secret` — 16 až 255 znaků; uložte ho na svém serveru pro ověřování podpisů.

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

Ověřte **v2**: `HMAC_SHA256(timestamp + "." + raw_body, secret)` proti `X-Webhook-Signature-V2`. Duplicity odstraňujte podle `X-Webhook-Event-Id`. Odpovězte **2xx do 3 sekund**.

```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í:** aktualizace vrátí `changed_keys` s odeslanými klíči a testovací událost přijatá na vaší URL projde výše uvedenou kontrolou podpisu.

## 14. Zrušení výdeje nebo objednávky skladování

Zrušení uvolní to, co bylo rezervováno. Zrušení výdeje vrátí jeho balíky do zásob; zrušení objednávky skladování ukončí rezervaci, jejíž zboží ještě nebylo přijato.

**REST:** `POST /api/v1/customer/shipout-orders/{id}/cancel` — [Příručka REST](/api/documentation#/paths/v1-customer-shipout-orders-id--cancel/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/8001/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Buyer cancelled web order NW-20931"}'
```

```json
{ "result": true, "message": "Shipout cancelled." }
```

- `reason` (volitelné) — zaznamená se spolu se zrušením.
- Výdej lze zrušit pouze tehdy, když je čekající (`0`) nebo potvrzený (`1`).

**GraphQL:** `customerCancelShipout` ([Příručka GraphQL](/api/graphql/documentation#/storage-shipout/customerCancelShipout))

```graphql
mutation {
  customerCancelShipout(id: 8001, reason: "Buyer cancelled web order NW-20931") {
    result
    message
  }
}
```

Tím se uvolní vázání uskladněných balíků. Samotné skladování se ruší pomocí `POST /api/v1/customer/storage-orders/{id}/cancel`, dokud je to ještě povoleno (stav `pending payment`, `confirmed`, `waiting for pickup` nebo `awaiting dropoff`; [Příručka REST](/api/documentation#/paths/v1-customer-storage-orders-id--cancel/post)).

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

```json
{ "result": true, "message": "Storage order cancelled", "data": { "id": 1024, "status": "cancelled" } }
```

- Částka již zaplacená za objednávku skladování se připíše zpět na zůstatek zákazníka.
- Objednávka skladování v jakémkoli jiném stavu vrátí `403`.

**Ověření:** `422` znamená, že tento stav nelze zrušit. Po úspěšném zrušení výdeje krok 9 znovu uvádí balíky.

## 15. Zpracování chyb

| Situace | Stav HTTP | Kód | Co integrace udělá |
|---|---|---|---|
| Chybějící nebo prošlý token, nebo nesprávný typ účtu | 401 | — | Přihlaste se znovu jako zákazník (krok 4). |
| Cenová nabídka skladování bez `warehouse_id` nebo dat | 400 | — | Pošlete `warehouse_id`, `start_date` a `end_date`. |
| Validace objednávky skladování selhala (chybějící rozměry položky, `end_date` není po `start_date`, chybějící `packaging_id`) | 422 | — | Přečtěte `errors`, opravte pole a odešlete znovu. |
| Platba za skladování s nedostatečným zůstatkem nebo objednávka je již zcela zaplacena | 400 | — | Dobijte zůstatek (odpověď obsahuje `customer_balance`) nebo skončete, pokud je již zaplaceno. |
| Platba za skladování s `custom_amount` mimo povolený rozsah | 422 | — | Zaplaťte částku mezi minimem a zbývající částkou. |
| Objednávku skladování nelze v jejím aktuálním stavu zrušit | 403 | — | Požádejte sklad o vyřízení objednávky; neopakujte. |
| Výdej je pro tohoto zákazníka vypnutý | 403 | — | Požádejte firmu o zapnutí výdeje pro zákaznický účet. |
| Neznámý kód služby nebo výdej / objednávka skladování nebyla nalezena | 404 | — | Znovu načtěte seznam služeb nebo zkontrolujte uložené id. |
| Balík není dostupný, balíky jsou z různých skladů nebo se služba ze skladu nenabízí | 422 | — | Znovu načtěte krok 9 a vyberte dostupné balíky z jednoho skladu. |
| Přepravní služba nedokáže zásilku ocenit a odmítá ji | 422 | `unpriced_refused` | Zvolte jinou službu nebo cíl; nic nebylo vytvořeno. |
| Platba za výdej s nedostatečným zůstatkem | 402 | — | Dobijte zůstatek a poté opakujte krok 11. |
| Výdej ještě nelze zaplatit (čeká na ruční cenovou nabídku) nebo je částka neplatná | 422 | — | Počkejte na cenu, znovu načtěte výdej a poté zaplaťte. |
| Výdej nelze v jeho aktuálním stavu zrušit | 422 | — | Zásilka je již v procesu; neopakujte. |
| Stejný `Idempotency-Key` odeslaný s jiným tělem | 409 | `IDEMPOTENCY_CONFLICT` | Pro jiný požadavek použijte nový klíč. |
| Původní požadavek se stejným `Idempotency-Key` se ještě zpracovává | 409 | `IDEMPOTENCY_IN_PROGRESS` | Počkejte počet sekund z `Retry-After` a odešlete stejný požadavek znovu. |

## Seznam testů

- [ ] Konfigurace skladování vrátí `id` skladu.
- [ ] Cenová nabídka skladování vrátí cenu a vytvoření skladování vrátí `data.id`.
- [ ] Platba za skladování uspěje, **nebo** jste ověřili, že peněženku je třeba dobít.
- [ ] Seznam dostupných položek uvádí přijaté balíky (`storage_package_ids`).
- [ ] Odhad výdeje vrátí cenu nebo `has_items_needing_quote` a vytvoření výdeje vrátí `id` a váže tyto balíky.
- [ ] Platba za výdej uspěje (nebo rozumíte významu `402` / `422`).
- [ ] Veřejné sledování najde zásilku, jakmile existuje sledovací číslo.
- [ ] Zrušení výdeje uvolní balíky, **nebo** tento stav nelze zrušit.
- [ ] Zopakování vytvoření se stejným `Idempotency-Key` a tělem vrátí `replayed: true` a žádnou druhou objednávku.
- [ ] Nastavení webhooků vrátí `recipient_type: customer` a přijatá událost projde kontrolou podpisu v2.
