# Skladištenje i izlaz

API za skladištenje i izlaz omogućava nalogu kupca skladišne firme da prijavi robu za skladištenje, plati period skladištenja i kasnije pošalje uskladištene pakete sopstvenim kupcima. Namenjen je trgovcima i platformama koji drže zalihe u skladištu treće strane (3PL) i žele da iz sopstvenih sistema automatizuju rezervaciju skladištenja, pregled zaliha i izlazne pošiljke. Svi pozivi se izvršavaju kao nalog kupca, nikada kao skladišna firma.

## 1. Šta možete da izgradite

Primeri u ovom vodiču prate jedan scenario. **Northwind Outdoor**, sezonski onlajn prodavac zimske opreme, čuva svoje zimske zalihe u skladištu **Toronto Hub** (skladište `7`) svog 3PL partnera od 1. novembra 2026. do 31. marta 2027. Kada kupac poruči karton izolovanih jakni, Northwind šalje taj karton sa zaliha kupcu u Otavi.

- **Rezervacija sezonskog skladištenja.** Pozadinski sistem prodavca dobija ponudu i rezerviše period skladištenja za svaki ulazni karton pre nego što roba napusti dobavljača i plaća naknadu za skladištenje sa stanja naloga.
- **Pregled zaliha u realnom vremenu.** Prodavnica ili ERP prodavca prikazuje pakete koje je skladište zaista primilo i koji su još dostupni za slanje, tako da se za ispunjenje nude samo stvarne zalihe.
- **Ispunjenje porudžbina sa zaliha.** Kada kupac postavi porudžbinu, sistem prodavca izračunava cenu izlazne pošiljke, kreira zahtev za izlaz za uskladištene pakete, plaća ga i beleži broj za praćenje za kupca.
- **Praćenje statusa i ispravke.** Sistem prodavca čita stanje svake porudžbine skladištenja i svakog izlaza, prati pošiljku preko javnog praćenja i otkazuje izlaz koji više nije potreban dok je to još dozvoljeno.

## 2. Šta ovaj vodič obuhvata

Koristite ovaj vodič kada je roba već uskladištena, ili će biti uskladištena, u skladištu firme i kada pošiljka polazi sa tih zaliha. Tok je: prijava → čitanje konfiguracije skladištenja → ponuda skladištenja → kreiranje porudžbine skladištenja → plaćanje → lista paketa na zalihi → lista usluga i procena izlaza → kreiranje izlaza → plaćanje → čitanje i praćenje → webhook-ovi → otkazivanje.

Drugi vodiči odgovaraju drugim slučajevima:

- **Uniorder: jedan API za svaku pošiljku** — preporučena jedinstvena ulazna tačka (`/api/v1/uniorder/...`) za nove integracije koje rezervišu lokalnu dostavu ili nalepnice prevoznika. Uniorder **ne** obuhvata skladištenje i izlaz; porudžbine skladištenja i izlazi kreiraju se isključivo preko endpoint-a za kupce iz ovog vodiča.
- **Usluge slanja** — kupac šalje robu koja nije uskladištena, koristeći usluge slanja firme.
- **Nalepnice prevoznika** — firma direktno kupuje nalepnice prevoznika za sopstvene pakete.
- **Preuzimanje i dostava (sopstvena flota)** — firma rezerviše preuzimanja i dostave sopstvenom flotom.

## 3. Pre nego što počnete

- **Vrsta naloga.** Nalog **kupca** skladišne firme (firma koja upravlja skladištem je pružalac usluge). Token poslovnog (klijentskog) naloga ne radi na endpoint-ima `/api/v1/customer/...`.
- **Dozvole.** Nalog kupca mora imati API pristup. Endpoint-i skladištenja dodatno zahtevaju mogućnost skladištenja; endpoint-i izlaza zahtevaju da je firma za ovog kupca uključila izlaz (ili konsolidaciju), inače odgovaraju sa `403`.
- **Stanje.** Plaćanja skladištenja i izlaza naplaćuju se sa stanja naloga kupca. Za test zatražite od firme da odobri iznos na stanje test kupca.
- **Test podaci.** `id` skladišta, najmanje jedan `id` pakovanja ako prilagođeni paketi nisu dozvoljeni i najmanje jedna aktivna usluga slanja dostupna iz tog skladišta. Izlaz radi tek nakon što skladište **primi** uskladištene pakete; u testu zatražite od osoblja skladišta da primi test porudžbinu skladištenja.
- **Rukovanje tokenom.** Prijavite se sa svog servera, čuvajte token na serveru i nikada ga ne stavljajte u kod pregledača ili mobilne aplikacije.
- **Zamenske vrednosti.** Zamenite `YOUR_HOST` hostom vaše platforme, a `ACCESS_TOKEN` tokenom iz koraka 4.

## 4. Prijava kao kupac

Svaki naredni poziv autorizuje se bearer tokenom kupca. Vaša integracija se prijavljuje jednom, čuva token na serveru i obnavlja ga pre `expires_at`.

**REST:** `POST /api/v1/user/customer/login` — [REST priručnik](/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` — šaljite ga uz svaki zahtev kao zaglavlje ispod.
- `expires_at` / `expires_timestamp` — ponovo se prijavite pre ovog trenutka.

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL koristi isto zaglavlje na `POST /api/graphql`.

**Verifikacija:** prijava vraća `access_token`. Naredni zahtevi bez ovog tokena vraćaju `401`.

## 5. Čitanje konfiguracije skladištenja

Skup konfiguracije navodi skladišta koja kupac sme da koristi, katalog pakovanja, jedinice i doplate. Vaša integracija ga čita jednom po sesiji da bi izabrala skladište i sastavila ispravne redove paketa.

**REST:** `GET /api/v1/customer/storage-orders/config` — [REST priručnik](/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` za svaki naredni poziv.
- `allow_custom_package` — kada je `false`, svaka stavka skladištenja mora da nosi `packaging_id` iz `packagings[]`; kada je `true`, stavke mogu da se opišu samo dimenzijama.
- `dimension_units` / `weight_units` — celobrojni kodovi koji se koriste u redovima paketa (`2` = cm, `2` = kg).
- `form_bindings` — obrasci koje firma zahteva uz porudžbinu skladištenja; odgovore pošaljite kao `form_data` u koraku 7.

**GraphQL:** `customerStorageOrderConfig` ([GraphQL priručnik](/api/graphql/documentation#/customer/customerStorageOrderConfig))

```graphql
query {
  customerStorageOrderConfig
}
```

**Verifikacija:** sačuvali ste `id` skladišta i, ako katalog nije prazan, `id` pakovanja.

## 6. Ponuda za period skladištenja

Ponuda izračunava cenu perioda skladištenja za planirane pakete pre nego što se išta rezerviše. Vaša integracija prikazuje ili proverava ovu cenu, a zatim kreira porudžbinu sa istim ulaznim podacima.

**REST:** `POST /api/v1/customer/storage-orders/calculate-price` — [REST priručnik](/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` kada je cena izračunata.
- `price.total_price` / `price.currency` — cena skladištenja sa porezom za taj period.
- `promotion` — prisutno samo kada se primenjuje promocija.

**Verifikacija:** `success` ili `result` je true i imate cenu. Nedostajući `warehouse_id` / datumi daju `400`.

## 7. Kreiranje porudžbine skladištenja

Porudžbina skladištenja najavljuje skladištu ulazne pakete i utvrđuje period skladištenja. Vaša integracija čuva vraćeni ID; potreban je za plaćanje, čitanje i otkazivanje porudžbine.

**REST:** `POST /api/v1/customer/storage-orders` — [REST priručnik](/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 porudžbine skladištenja. Sačuvajte ga uz svoju nabavnu porudžbinu.
- `data.status` — `pending payment` dok porudžbina nije plaćena.
- `Idempotency-Key` — izvedite ga iz sopstvenog stabilnog ID-ja. Ponovljeni ključ sa istim telom ponavlja prvi odgovor (`replayed: true`); isti ključ sa drugačijim telom odbija se sa `409 IDEMPOTENCY_CONFLICT`.
- Obavezna polja: `warehouse_id`, `start_date`, `end_date` (posle `start_date`) i `items[]` sa `qty`, `length`, `width`, `height`, `dimension_unit`. Dodajte `items[].packaging_id` kada je `allow_custom_package` `false`.

**Verifikacija:** odgovor sadrži `data.id`. Sačuvajte taj ID porudžbine skladištenja.

## 8. Plaćanje skladištenja

Plaćanje potvrđuje porudžbinu skladištenja. Vaša integracija može prvo da pročita iznos za plaćanje, a zatim plaća sa stanja kupca.

**REST:** `GET /api/v1/customer/storage-orders/{id}/payment-info` — [REST priručnik](/api/documentation#/paths/v1-customer-storage-orders-id--payment-info/get) (opciono)

```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` — [REST priručnik](/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` (podrazumevano, plaća preostali iznos), `minimum_payment` (plaća minimum koji firma zahteva) ili `custom` zajedno sa `custom_amount`.
- `is_fully_paid` — `true` kada ništa ne ostaje za plaćanje.
- Nedovoljno stanje daje odgovor `400` sa `customer_balance`; dopunite stanje, zatim pokušajte ponovo.

Pročitajte porudžbinu da biste potvrdili njen status i, kasnije, koje je pakete skladište primilo.

**REST:** `GET /api/v1/customer/storage-orders/{id}` — [REST priručnik](/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` posle plaćanja; kasnije `partial received` / `storage in progress` kako roba stiže.
- `packages[].received` — `true` kada skladište primi taj paket.
- `can_cancel` — da li porudžbina skladištenja još može da se otkaže.

**GraphQL:** `customerStorageOrderShow` ([GraphQL priručnik](/api/graphql/documentation#/customer/customerStorageOrderShow)); lista svih porudžbina skladištenja je `customerStorageOrders` ([GraphQL priručnik](/api/graphql/documentation#/customer/customerStorageOrders)).

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

**Verifikacija:** porudžbina skladištenja je plaćena / potvrđena. `400` sa `customer_balance` znači da treba dopuniti stanje, a zatim pokušati ponovo.

Izlaz opisan u nastavku radi tek nakon što su paketi **primljeni** u skladište. Za test sačekajte da ih osoblje (ili test prijem) označi kao primljene, a zatim nastavite.

## 9. Lista stavki još na zalihi

Ova lista predstavlja zalihe koje vaša integracija sme da pošalje. Sadrži samo pakete koje je skladište primilo i koji nisu već zaključani za drugi izlaz.

**REST:** `GET /api/v1/customer/shipout-orders/available-items` — [REST priručnik](/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` za izlaz u koraku 10.
- `warehouses[].available_count` — broj dostupnih paketa po skladištu.

**GraphQL:** `customerShipoutAvailableItems` ([GraphQL priručnik](/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 } }
    }
  }
}
```

**Verifikacija:** sačuvali ste jedan ili više `storage_package_ids` (primer `5001`). Prazna lista znači da još ništa nije primljeno — ne kreirajte izlaz. `403` znači da je izlaz isključen za ovog kupca.

## 10. Procena i kreiranje izlaza

Cenu izlaza određuje usluga slanja firme. Vaša integracija prikazuje listu usluga dostupnih iz skladišta, procenjuje cenu za odredište kupca, a zatim kreira izlaz za izabrane pakete.

**REST:** `GET /api/v1/customer/shipout-orders/services?warehouse_id=7` — [REST priručnik](/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 }
    ]
  }
}
```

Sačuvajte `service_code`.

**REST:** `POST /api/v1/customer/shipout-orders/services/{serviceCode}/estimate` — [REST priručnik](/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` — procenjena cena za ovo odredište.
- `has_items_needing_quote` — `true` kada se cena usluge određuje ručno; skladište određuje cenu nakon što se izlaz kreira, a plaćanje čeka na nju.
- `refused` / `refusal_message` — usluga neće prihvatiti ovu pošiljku jer ne može da izračuna njenu cenu.

**REST:** `POST /api/v1/customer/shipout-orders/services/{serviceCode}/orders` — [REST priručnik](/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 izlaza. Sačuvajte ga uz porudžbinu kupca.
- `data.status` — `0` = na čekanju (čeka plaćanje), `1` = potvrđeno, `2` = u tranzitu, `3` = poslato, `4` = otkazano, `5` = neuspešno.
- `storage_package_ids` — ovi paketi su sada zaključani za ovaj izlaz i više se ne pojavljuju u koraku 9.
- Obavezna polja: `warehouse_id`, `storage_package_ids`, `delivery_name`, `delivery_telephone`, `delivery_address_1`, `delivery_city`, `delivery_province`, `delivery_country`, `delivery_postcode`. Svi paketi moraju biti iz istog skladišta.

**GraphQL:** `customerCreateShipoutOrder` ([GraphQL priručnik](/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 }
  }
}
```

**Verifikacija:** odgovor sadrži `id` izlaza. Izabrani uskladišteni paketi su zaključani za ovaj zahtev.

## 11. Plaćanje izlaza

Skladište obrađuje izlaz kada je plaćen. Vaša integracija plaća preostali iznos sa naloga kupca; izostavite `amount` da biste platili u celosti.

**REST:** `POST /api/v1/customer/shipout-orders/{id}/pay` — [REST priručnik](/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` (zahtev, opciono) — delimičan iznos; podrazumevano ceo preostali iznos.
- `order_status` — `1` (potvrđeno) posle plaćanja u celosti.
- `remaining_balance` — `0` kada je plaćeno u celosti.

**GraphQL:** `customerPayShipout` ([GraphQL priručnik](/api/graphql/documentation#/storage-shipout/customerPayShipout))

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

**Verifikacija:** plaćanje beleži iznos (ili vraća `402` / `422` sa jasnim razlogom). `402` znači da stanje nije dovoljno; `422` znači da porudžbina još ne može da se plati (na primer, još čeka ručnu ponudu) ili da je iznos nevažeći.

## 12. Čitanje i praćenje izlaza

Vaša integracija čita izlaz da bi pratila njegov status i, kada ga skladište pošalje, prati pošiljku po njenom broju za praćenje.

**REST:** `GET /api/v1/customer/shipout-orders/{id}` — [REST priručnik](/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` — trenutno stanje izlaza.
- `can_be_paid` / `can_be_cancelled` — da li je korak 11 ili korak 14 trenutno dozvoljen.

Kada broj za praćenje postoji:

**REST:** `GET /api/v1/tracking/{trackingNumber}` — [REST priručnik](/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[]` — događaji praćenja hronološkim redosledom.
- `deliveried` — `true` kada je pošiljka dostavljena.

**GraphQL:** `trackingPublic` ([GraphQL priručnik](/api/graphql/documentation#/tracking/trackingPublic))

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

**Verifikacija:** čitanje izlaza vraća očekivani `status`. Javno praćenje pronalazi pošiljku čim postoji broj.

## 13. Pretplata na webhook-ove

Webhook-ovi šalju promene praćenja i statusa na vaš server umesto periodičnog upita. Nalog kupca podešava sopstvene webhook URL-ove i tajnu za potpisivanje; podešavanja se čuvaju na nalogu kupca, ne na firmi.

**REST:** `PUT /api/v1/webhook-settings` — [REST priručnik](/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"
  }
}
```

- Menjaju se samo poslati ključevi; nepoznat ključ ili nevažeći URL daju odgovor `400`.
- `recipient_type` — `customer` potvrđuje da podešavanja pripadaju nalogu kupca.
- `webhook_sign_secret` — od 16 do 255 znakova; čuvajte je na svom serveru radi provere potpisa.

**GraphQL:** `webhookSettingsUpdate` ([GraphQL priručnik](/api/graphql/documentation#/webhooks/webhookSettingsUpdate)).

Proverite **v2**: `HMAC_SHA256(timestamp + "." + raw_body, secret)` u odnosu na `X-Webhook-Signature-V2`. Duplikate uklanjajte prema `X-Webhook-Event-Id`. Odgovorite sa **2xx za manje od 3 sekunde**.

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

**Verifikacija:** ažuriranje vraća `changed_keys` sa poslatim ključevima, a test događaj primljen na vašem URL-u prolazi gornju proveru potpisa.

## 14. Otkazivanje izlaza ili porudžbine skladištenja

Otkazivanje oslobađa ono što je rezervisano. Otkazivanje izlaza vraća njegove pakete na zalihu; otkazivanje porudžbine skladištenja zaustavlja rezervaciju čija roba još nije primljena.

**REST:** `POST /api/v1/customer/shipout-orders/{id}/cancel` — [REST priručnik](/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` (opciono) — beleži se uz otkazivanje.
- Izlaz može da se otkaže samo dok je na čekanju (`0`) ili potvrđen (`1`).

**GraphQL:** `customerCancelShipout` ([GraphQL priručnik](/api/graphql/documentation#/storage-shipout/customerCancelShipout))

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

Ovo oslobađa zaključavanje uskladištenih paketa. Samo skladištenje se otkazuje pomoću `POST /api/v1/customer/storage-orders/{id}/cancel` dok je to još dozvoljeno (status `pending payment`, `confirmed`, `waiting for pickup` ili `awaiting dropoff`; [REST priručnik](/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" } }
```

- Iznos već plaćen za porudžbinu skladištenja vraća se na stanje kupca.
- Porudžbina skladištenja u bilo kom drugom statusu daje odgovor `403`.

**Verifikacija:** `422` znači da ovaj status ne može da se otkaže. Posle uspešnog otkazivanja izlaza, korak 9 ponovo prikazuje pakete.

## 15. Obrada grešaka

| Situacija | HTTP status | Kod | Šta integracija radi |
|---|---|---|---|
| Token nedostaje ili je istekao, ili je vrsta naloga pogrešna | 401 | — | Ponovo se prijavite kao kupac (korak 4). |
| Ponuda skladištenja bez `warehouse_id` ili datuma | 400 | — | Pošaljite `warehouse_id`, `start_date` i `end_date`. |
| Validacija porudžbine skladištenja nije uspela (nedostaju dimenzije stavke, `end_date` nije posle `start_date`, nedostaje `packaging_id`) | 422 | — | Pročitajte `errors`, ispravite polja i pošaljite ponovo. |
| Plaćanje skladištenja sa nedovoljnim stanjem ili porudžbina već plaćena u celosti | 400 | — | Dopunite stanje (odgovor nosi `customer_balance`) ili prekinite ako je već plaćeno. |
| Plaćanje skladištenja sa `custom_amount` van dozvoljenog opsega | 422 | — | Platite iznos između minimuma i preostalog iznosa. |
| Porudžbina skladištenja ne može da se otkaže u trenutnom statusu | 403 | — | Zatražite od skladišta da obradi porudžbinu; ne pokušavajte ponovo. |
| Izlaz je isključen za ovog kupca | 403 | — | Zatražite od firme da uključi izlaz za nalog kupca. |
| Nepoznat kod usluge ili izlaz / porudžbina skladištenja nije pronađena | 404 | — | Ponovo pročitajte listu usluga ili proverite sačuvani ID. |
| Paket nije dostupan, paketi su iz različitih skladišta ili se usluga ne nudi iz skladišta | 422 | — | Ponovo pročitajte korak 9 i izaberite dostupne pakete iz jednog skladišta. |
| Usluga slanja ne može da izračuna cenu pošiljke i odbija je | 422 | `unpriced_refused` | Izaberite drugu uslugu ili odredište; ništa nije kreirano. |
| Plaćanje izlaza sa nedovoljnim stanjem | 402 | — | Dopunite stanje, zatim ponovite korak 11. |
| Izlaz još ne može da se plati (čeka ručnu ponudu) ili je iznos nevažeći | 422 | — | Sačekajte cenu, ponovo pročitajte izlaz, zatim platite. |
| Izlaz ne može da se otkaže u trenutnom statusu | 422 | — | Pošiljka je već u toku; ne pokušavajte ponovo. |
| Isti `Idempotency-Key` poslat sa drugačijim telom | 409 | `IDEMPOTENCY_CONFLICT` | Za drugačiji zahtev koristite novi ključ. |
| Prvobitni zahtev sa istim `Idempotency-Key` se još obrađuje | 409 | `IDEMPOTENCY_IN_PROGRESS` | Sačekajte `Retry-After` sekundi i ponovo pošaljite isti zahtev. |

## Lista provera

- [ ] Konfiguracija skladištenja vraća `id` skladišta.
- [ ] Ponuda skladištenja vraća cenu, a kreiranje skladištenja vraća `data.id`.
- [ ] Plaćanje skladištenja uspeva, **ili** ste potvrdili da novčanik mora da se dopuni.
- [ ] Dostupne stavke navode primljene pakete (`storage_package_ids`).
- [ ] Procena izlaza vraća cenu ili `has_items_needing_quote`, a kreiranje izlaza vraća `id` i zaključava te pakete.
- [ ] Plaćanje izlaza uspeva (ili su `402` / `422` razumljivi).
- [ ] Javno praćenje pronalazi pošiljku čim postoji broj za praćenje.
- [ ] Otkazivanje izlaza oslobađa pakete, **ili** ovaj status ne može da se otkaže.
- [ ] Ponavljanje kreiranja sa istim `Idempotency-Key` i telom vraća `replayed: true` i ne kreira drugu porudžbinu.
- [ ] Podešavanja webhook-ova vraćaju `recipient_type: customer`, a primljeni događaj prolazi proveru v2 potpisa.
