# Preuzimanje i dostava (sopstvena flota)

Ovaj vodič opisuje API lokalne dostave poslovnog naloga: porudžbine koje sopstveni vozači firme dostavljaju primaocu (`type` `D`) ili preuzimaju od pošiljaoca (`type` `P`). Jedan skup endpoint-a daje ponudu, kreira, štampa nalepnicu, prati i otkazuje obe vrste stanica, a webhook-ovi javljaju svaku promenu vašem sistemu. Namenjen je programerima sistema za upravljanje porudžbinama, ERP sistema i onlajn prodavnica koji dodeljuju posao sopstvenoj floti firme.

## 1. Šta možete da izgradite

Primeri u nastavku prate jednu firmu: **Farine & Fils**, dobavljača za pekare sa skladištem na adresi 2200 Rue Cohen, Saint-Laurent, QC (H4R 2N6), koji dostavlja veleprodajne porudžbine na celom ostrvu Montreal i preuzima prazne gajbe za hleb koje mu kupci vraćaju. Tipična dostava je jedan složaj gajbi od 12 kg, dimenzija 60 × 40 × 30 cm, za Café Lumière, 5400 Avenue du Parc, Montréal (H2V 4G7), po veleprodajnoj porudžbini `WHS-20931`. Tipično preuzimanje je jedan složaj praznih gajbi od 4 kg iz Épicerie Wellington, 4100 Rue Wellington, Verdun (H4G 1V5), pod referencom `CRT-20931`.

- **Veleprodajne porudžbine koje ERP šalje dispečerskoj službi.** Svaka potvrđena veleprodajna porudžbina postaje porudžbina dostave sa jutarnjim vremenskim prozorom dostave kafića, a ERP čuva vraćeni broj za praćenje na stavci porudžbine.
- **Preuzimanja vraćenih gajbi.** Kada kupac prijavi prazne gajbe, ERP kreira porudžbinu preuzimanja za adresu kupca, a vozač preuzima gajbe na sledećoj ruti.
- **Štampa nalepnica u skladištu.** ERP preuzima PDF nalepnice svake porudžbine i štampa ga na utovarnoj rampi, tako da svaki složaj gajbi nosi svoj barkod za praćenje.
- **Portal za kupce sa statusom u realnom vremenu.** Svaki kafić vidi status svojih dostava i preuzimanja, uz dokaz o dostavi, na osnovu webhook-ova umesto periodičnog upita.

## 2. Šta ovaj vodič obuhvata

Koristite ovaj vodič kada porudžbinu prevoze sopstveni vozači firme: dostave iz skladišta i preuzimanja sa adrese kupca, kreirane pojedinačno ili paketno preko endpoint-a `/api/v1/client/...` i `/api/v1/orders/...`.

Za nove integracije, Uniorder (`/api/v1/uniorder/...`) je preporučena jedinstvena ulazna tačka: nudi iste dostave sopstvenom flotom kroz jedan API, zajedno sa nalepnicama prevoznika, iz jedne ponude. Pogledajte **Uniorder: jedan API za svaku pošiljku** za pregled i **Ponuda i porudžbina u jednom toku** za zahteve korak po korak. Endpoint-i u ovom vodiču ostaju dostupni i nepromenjeni za integracije koje su na njima izgrađene.

Koristite **Nalepnice prevoznika** kada paket šalje spoljni prevoznik sa nalepnicom kupljenom preko platforme. Koristite **Usluge slanja** za porudžbine koje nalog kupca rezerviše za usluge firme, a **Skladištenje i izlaz** za robu koja se čuva u skladištu i šalje na zahtev; Uniorder se ne primenjuje na ta dva slučaja.

## 3. Pre nego što počnete

- **Nalog.** Koristite poslovni (klijentski) nalog ili nalog zaposlenog u firmi, sa API dozvolom. Kreiranje porudžbina dodatno zahteva dozvolu za postavljanje porudžbina; bez nje `POST /api/v1/client/orderCreate` vraća `401`.
- **Područje usluge.** Adresa dostave ili preuzimanja mora biti unutar aktivnog regiona firme. Za testove koristite adrese unutar područja, kao što su one u ovom vodiču.
- **Test podaci.** Koristite test reference kao što su `WHS-20931` i `CRT-20931`, a test porudžbine na kraju otkažite (korak 12).
- **Tokeni.** Pristupni token tražite sa svog servera i čuvajte ga tamo. Nikada ga ne šaljite pregledaču ili mobilnoj aplikaciji.
- **Zamenske vrednosti.** Zamenite `YOUR_HOST` API hostom vašeg okruženja, a `ACCESS_TOKEN` tokenom iz koraka 4.
- **Jedinice.** `weight_unit`: `1` g, `2` kg, `3` oz, `4` lb. `dimension_unit`: `1` mm, `2` cm, `3` m, `4` in. Obe podrazumevano imaju vrednost `1`.

## 4. Prijava

Svaki poziv u ovom vodiču, osim javnog praćenja, izvršava se u ime poslovnog naloga. Prijavite se jednom sa svog servera, sačuvajte vraćeni token i šaljite ga uz svaki zahtev.

**REST:** `POST /api/v1/user/login` — [REST priručnik](/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`: stavite ga u zaglavlje svakog narednog zahteva:

```
Authorization: Bearer ACCESS_TOKEN
```

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

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

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

## 5. Ponuda za dostavu ili preuzimanje (opciono)

Ponuda prikazuje cenu stanice pre nego što porudžbina postoji, na primer da bi se trošak dostave prikazao na veleprodajnoj fakturi. Ponuda ništa ne kreira, a kreiranje porudžbine ne zahteva prethodnu ponudu. Postavite `type` na `D` (dostava) ili `P` (preuzimanje); `to_postcode` je poštanski broj stanice.

**REST:** `POST /api/v1/orders/rate` — [REST priručnik](/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 stanice bez poreza. Prazna cena znači da poštanski broj nije u aktivnom regionu ili da cenovnik nema red za njega.
- `price_details.tax_details`: porezi koje će porudžbina nositi; prikažite ih na stavci fakture.
- `currency`: valuta svakog iznosa u odgovoru.

Da biste dobili ponudu za preuzimanje gajbi, pošaljite isti zahtev sa `"type": "P"`, `"to_postcode": "H4G1V5"` i težinom i dimenzijama složaja gajbi.

**GraphQL:** `ordersRate` ([GraphQL priručnik](/api/graphql/documentation#/orders/ordersRate)). Rezultat je JSON skalar i ne prima 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 }]
  )
}
```

**Verifikacija:** `result` je `true`, a `shipping_price` je broj i za `type` `D` i za `type` `P`. Kreiranje porudžbine ne zavisi od ovog koraka.

## 6. Kreiranje porudžbine dostave

Svaka potvrđena veleprodajna porudžbina postaje jedna porudžbina dostave. ERP čuva vraćene `id` i `tracking_number` na svojoj stavci porudžbine; svaki kasniji poziv koristi jedan od njih.

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

Pošaljite zaglavlje `Idempotency-Key`, jedinstveno za svaku veleprodajnu porudžbinu, kako ponovni pokušaj posle isteka vremena ne bi kreirao drugu porudžbinu.

```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": "" }
  ]
}
```

| Polje | Značenje |
|---|---|
| `type` | `D` dostava ili `P` preuzimanje |
| `need_pick_up` | `0` — roba je već u skladištu. `1` — vozač mora da preuzme paket |
| `ref` | Spoljna referenca za pretragu i usaglašavanje |
| `name` / adresa | Dostava: primalac. Preuzimanje: stanica preuzimanja |
| `schedule_date`, `time_window_start`, `time_window_end` | Datum dostave (`Y-m-d`) i vremenski prozor u kojem stanica mora biti uslužena (`Y-m-d H:i:s`) |
| `packagesDetail` | Jedan unos po paketu; `ref` identifikuje paket u vašem sistemu |
| `auto_deduplication` | `1` odbija drugi paket sa istim `ref` paketa |

U odgovoru:

- `id`: ID porudžbine; sačuvajte ga za detalj porudžbine i poziv otkazivanja.
- `tracking_number`: jedan broj za praćenje po paketu; sa njima štampate i pratite.
- `warning`: prisutno kada je porudžbina kreirana uz napomenu, na primer za adresu van područja dostave koju firma zadržava ili stavlja na čekanje. Zadržana porudžbina van područja može da vrati `shipping_price: null`.

**GraphQL:** `clientOrderCreate` ([GraphQL priručnik](/api/graphql/documentation#/client/clientOrderCreate)). Rezultat je JSON skalar sa istim telom kao REST odgovor.

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

**Verifikacija:** ponovo pošaljite isto telo sa istim `Idempotency-Key`. Odgovor nosi isti `id` i druga porudžbina se ne kreira.

## 7. Kreiranje porudžbine preuzimanja

Porudžbina preuzimanja šalje vozača da preuzme robu na adresi; ovde su to prazne gajbe u Épicerie Wellington. Koristi isti endpoint kao dostava: adresa je stanica preuzimanja, `type` je `P`, a `need_pick_up` je `1`.

**REST:** `POST /api/v1/client/orderCreate` — [REST priručnik](/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` i `tracking_number`: sačuvajte ih uz povraćaj gajbi, kao kod dostave.
- `pickup_instruction`: prikazuje se vozaču na stanici preuzimanja; `delivery_instruction` je odgovarajuće polje kod dostave.

**Verifikacija:** detalj porudžbine (korak 8) pokazuje `type` `P` i `need_pickup` `1` za ovu porudžbinu.

## 8. Čitanje porudžbine

Detalj porudžbine potvrđuje šta je sačuvano i vraća trenutni status; endpoint liste omogućava ERP-u da usaglasi sopstvene zapise sa platformom.

**REST:** `GET /api/v1/orders/{orderId}` — [REST priručnik](/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`: status porudžbine; `2` je Nova, `12` je Otkazana.
- `order.type` i `order.need_pickup`: potvrđuju da je stanica sačuvana kao dostava ili kao preuzimanje.
- `tracking_numbers`: brojevi za praćenje paketa porudžbine.

**REST:** `GET /api/v1/orders/list` — [REST priručnik](/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"
```

Lista vraća sve porudžbine naloga, od najnovije, svaku sa njenim paketima i stavkama. Za podelu na stranice prosledite `page` i `per_page` zajedno (`per_page` najviše 1000); bez njih se vraća najnovijih 1000 porudžbina sa oznakom `truncated`.

**GraphQL:** `orders` ([GraphQL priručnik](/api/graphql/documentation#/orders/orders)) za jednu porudžbinu i `ordersList` ([GraphQL priručnik](/api/graphql/documentation#/orders/ordersList)) za listu. Oba vraćaju JSON skalar.

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

**Verifikacija:** porudžbina pripada autentifikovanom nalogu, `ref` se poklapa sa vrednošću poslatom pri kreiranju, a `tracking_numbers` se poklapa sa odgovorom kreiranja.

## 9. Štampa lokalne nalepnice

Nalepnica nosi barkod za praćenje koji vozač skenira u skladištu i na stanici. Odštampajte jednu nalepnicu po paketu i pričvrstite je na složaj gajbi.

**REST:** `POST /api/v1/shipping/getShippingLabel` — [REST priručnik](/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`: kako se čita `id`: `TRACKING_NUMBER` (podrazumevano), `ORDER_ID` ili `REF`.
- `base64`: `0` (podrazumevano) šalje PDF kao tok. `1` čini celo telo odgovora JSON stringom najvišeg nivoa koji sadrži base64 PDF, a ne objektom sa poljem `pdf_data`. Umesto toga pozovite `POST /api/v2/shipping/getShippingLabel` — [REST priručnik](/api/documentation#/paths/v2-shipping-getShippingLabel/post) da biste nalepnicu dobili unutar običnog JSON objekta.
- `packages`: opciono; broj nalepnica za štampu. Vrednost različita od broja paketa porudžbine ažurira porudžbinu.
- `hide_sender_address` / `hide_receiver_address`: `1` ostavlja tu adresu praznom na nalepnici.

**GraphQL:** `shippingGetShippingLabel` ([GraphQL priručnik](/api/graphql/documentation#/shipping/shippingGetShippingLabel)). `shippingGetShippingLabelV2` ([GraphQL priručnik](/api/graphql/documentation#/shipping/shippingGetShippingLabelV2)) uvek vraća JSON (`pdf_data`).

**Verifikacija:** dekodirani PDF se otvara. Nalepnica dostave pokazuje adresu kafića Café Lumière; nalepnica preuzimanja pokazuje adresu Épicerie Wellington. Skrivena adresa je prazna na nalepnici.

## 10. Praćenje porudžbine

Javno praćenje vraća vremensku liniju događaja paketa. Ne zahteva pristupni token, pa ga portal za kupce može direktno prikazati; uz njega dolazi i dokaz o dostavi ili preuzimanju.

**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,
  "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": []
}
```

Isti URL prihvata i vaš `ref` kada je sačuvan kao spoljni broj.

Grananje zasnivajte na `tracking_event_status_id`, ne na `description`; taj string prati `Accept-Language`.

| `tracking_event_status_id` | Strana | Značenje |
|---|---|---|
| `100` | obe | Porudžbina primljena |
| `300` / `301` | dostava | U objektu |
| `450` | dostava | Na dostavi |
| `500` | dostava | Dostavljeno |
| `501` | dostava | Dostava neuspešna, potreban je novi plan |
| `460` | preuzimanje | Na putu ka preuzimanju |
| `510` | preuzimanje | Preuzeto |
| `512` | preuzimanje | Preuzimanje neuspešno, pokušati kasnije |
| `513` | preuzimanje | Problem sa preuzimanjem |

- `data`: od najnovijeg; prvi red je trenutno stanje.
- `deliveried`: `true` posle `500`.
- `proofs[]`: kod `500` ili `510` može da sadrži `type` `1` (potpis) ili `2` (fotografija), sa `file_id` i `signed_url`. Fotografija otpremljena posle tog događaja nije u ovom sadržaju; pretplatite se na `pod.files_updated` (korak 11).

**GraphQL:** `trackingPublic` ([GraphQL priručnik](/api/graphql/documentation#/tracking/trackingPublic)). Rezultat je tipiziran i zahteva 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 }
  }
}
```

**Verifikacija:** odmah posle kreiranja najnoviji događaj je `100`, a `deliveried` je `false`. Nepoznat broj vraća `result: false` sa `404`; prikažite stanje „nije pronađeno” i ne izmišljajte događaje praćenja.

## 11. Prijem webhook-ova

Webhook-ovi šalju svaku promenu na vaš server, tako da ERP i portal za kupce ostaju ažurni bez periodičnog upita. Registrujte URL-ove povratnog poziva koje ovaj tok zahteva:

| Podešavanje | Događaj | Upotreba |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Sačuvati `id` i `tracking_number` |
| `order_status_change_webhook_url` | `order.status_change` | Status koji vidi kupac |
| `tracking_event_webhook_url` | `tracking.event` | Vremenska linija preuzimanja ili dostave |
| `pod_files_webhook_url` | `pod.files_updated` | Fotografija ili potpis posle preuzimanja ili dostave |
| `order_cancel_failed_webhook_url` | `order.cancel_failed` | Otkazivanje koje ste poslali je odbijeno |
| `order_create_async_postback_url` | `order.create_async` | Rezultat asinhronog paketa (korak 13) |

**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 '{
    "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`: podešavanja koja je ovaj poziv promenio.
- `settings.webhook_sign_secret`: vraća se maskirano; punu vrednost čuvajte samo na svom serveru.

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

Na strani prijema proverite **v2** potpis nad sirovim telom: `HMAC_SHA256(timestamp + "." + raw_body, secret)` uporedite sa `X-Webhook-Signature-V2`, gde je vremenska oznaka `X-Webhook-Timestamp`. Duplikate uklanjajte prema `X-Webhook-Event-Id`. Odgovorite sa **2xx za manje od 3 sekunde** i događaj obradite nakon toga.

```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:** kreirajte jednu test porudžbinu i primite `order.created` sa istim `id` i `tracking_number`. Primalac odbija nevažeći potpis sa `401`, a druga isporuka istog `X-Webhook-Event-Id` ne obrađuje se dva puta.

## 12. Otkazivanje porudžbine

Otkažite porudžbinu kada je veleprodajna porudžbina povučena ili preuzimanje gajbi više nije potrebno. Poziv je idempotentan: otkazivanje već otkazane porudžbine ponovo uspeva.

**REST:** `POST /api/v1/orders/cancel` — [REST priručnik](/api/documentation#/paths/v1-orders-cancel/post) — pošaljite tačno jedno od `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` kada je porudžbina otkazana.
- `already_cancelled`: `true` kada je porudžbina otkazana pre ovog poziva; tretirajte to kao uspeh.
- `code`: prisutno kada je otkazivanje odbijeno; pogledajte korak 14.

**GraphQL:** `ordersCancel` ([GraphQL priručnik](/api/graphql/documentation#/orders/ordersCancel)). Rezultat je tipiziran i zahteva selection set.

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

**Verifikacija:** detalj porudžbine pokazuje `orders_status_id` `12`, a isto otkazivanje vraća `already_cancelled: true`. Kada je otkazivanje odbijeno, `order.cancel_failed` se šalje na `order_cancel_failed_webhook_url`.

## 13. Paketno kreiranje porudžbina (opciono)

ERP može da pošalje veleprodajne porudžbine i preuzimanja gajbi za taj dan u jednom zahtevu. Svaki red prima ista polja kao u koracima 6 i 7 i može biti `type` `D` ili `P`.

**REST:** `POST /api/v1/client/batchOrderCreate` — [REST priručnik](/api/documentation#/paths/v1-client-batchOrderCreate/post) — odgovara kada su svi redovi obrađeni.

```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": "" }] }
]
```

- Svaki red ima sopstveni `result`; povežite ga sa svojom stavkom porudžbine preko `ref`. Odbijeni red nosi `message` i `skipped_ref`, a može da nosi i `code` (na primer `INSUFFICIENT_BALANCE` ili `OUT_OF_DELIVERY_AREA`).
- `per_order_transaction`: `1` potvrđuje svaki red zasebno, tako da jedan neuspešan red ne može da poništi ostale.
- Paketi sa više od 100 porudžbina dobijaju zaglavlje odgovora `X-Batch-Size-Warning`; takve pakete šaljite asinhronom endpoint-u.

**REST:** `POST /api/v1/client/batchOrderCreateAsync` — [REST priručnik](/api/documentation#/paths/v1-client-batchOrderCreateAsync/post) — prima isto telo i odmah vraća identifikator posla:

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

Proveravajte `GET /api/v1/client/async/{id}` — [REST priručnik](/api/documentation#/paths/v1-client-async-id/get) — sa `asyncId`, ili primite `order.create_async` na `order_create_async_postback_url`. Rezultat posla je ista lista po redovima kao kod sinhronog endpoint-a.

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

**GraphQL:** `clientBatchOrderCreate` ([GraphQL priručnik](/api/graphql/documentation#/client/clientBatchOrderCreate)), `clientBatchOrderCreateAsync` ([GraphQL priručnik](/api/graphql/documentation#/client/clientBatchOrderCreateAsync)) i `clientAsync` ([GraphQL priručnik](/api/graphql/documentation#/client/clientAsync)).

**Verifikacija:** paket od dva reda vraća dva rezultata, svaki sa svojim `ref`. Asinhroni posao vraća iste redove kada se izvrši.

## 14. Obrada grešaka

| Situacija | HTTP status | Kod | Šta integracija radi |
|---|---|---|---|
| Obavezno polje nedostaje ili je neispravno (kreiranje) | 400 | `VALIDATION_FAILED` | Ispravite polje navedeno u `message` i ponovo pošaljite zahtev. |
| Stanje naloga ne pokriva porudžbinu | 400 | `INSUFFICIENT_BALANCE` | Pročitajte `insufficient_balance` (potrebno, raspoloživo, manjak); dopunite stanje, zatim pokušajte ponovo. Porudžbina nije kreirana. |
| Adresa je van područja usluge, a firma briše takve porudžbine | 400 | `OUT_OF_DELIVERY_AREA` | Pošaljite adresu unutar područja usluge. Porudžbina nije kreirana. |
| `ref` paketa ili spoljni broj za praćenje već postoji (uz uključeno uklanjanje duplikata) | 200 (`result` `false`), ili 409 uz `strict_duplicate_check` `1` | `DUPLICATE_TRACKING_NUMBER` | Pročitajte `exist_package_ref` i povežite postojeću porudžbinu umesto kreiranja nove. |
| `Idempotency-Key` je ponovo upotrebljen sa drugačijim telom | 409 | `IDEMPOTENCY_CONFLICT` | Za drugačiji zahtev koristite novi ključ. |
| Zahtev sa istim `Idempotency-Key` se još izvršava | 409 | `IDEMPOTENCY_IN_PROGRESS` | Sačekajte, zatim pokušajte ponovo sa istim ključem. |
| Otkazivanje bez identifikatora porudžbine | 400 | `MISSING_IDENTIFIER` | Pošaljite jedno od `order_id`, `tracking_number`, `external_tracking_number`. |
| Otkazivanje porudžbine koja ne postoji | 400 | `ORDER_NOT_FOUND` | Proverite sačuvani `id` ili broj za praćenje. |
| Broj odgovara više od jedne aktivne porudžbine | 409 | `MULTIPLE_ORDERS_MATCHED` | Otkažite preko `order_id`, koristeći jedan od `matched_order_ids`. |
| Porudžbina pripada drugom nalogu | 401 | `ORDER_CANCEL_UNAUTHORIZED` | Otkažite nalogom koji je kreirao porudžbinu. |
| Status porudžbine više ne dozvoljava otkazivanje | 401 | `ORDER_STATUS_NOT_CANCELLABLE` | Ostavite porudžbinu nepromenjenom; povraćaj obradite zasebno. |
| Porudžbinu drži spoljni prevoznik koji ne može da je otkaže | 409 | `ORDER_CANCEL_BLOCKED_THIRD_PARTY`, `THIRD_PARTY_CANCEL_NOT_SUPPORTED` ili `THIRD_PARTY_CANCEL_FAILED` | Porudžbina je nepromenjena; obratite se firmi. |
| Token nedostaje ili je istekao, ili nalog ne sme da postavlja porudžbine | 401 | — | Ponovo se prijavite; proverite dozvole naloga. |

## Lista provera

Koristite test reference kao što su `WHS-20931` i `CRT-20931`:

- [ ] (Opciono) Ponuda vraća cenu za poštanski broj u području sa `type` `D`.
- [ ] (Opciono) Ponuda vraća cenu za poštanski broj u području sa `type` `P`.
- [ ] Kreiranje dostave vraća `id` + `tracking_number`; isti `Idempotency-Key` ne kreira drugu porudžbinu.
- [ ] Kreiranje preuzimanja vraća `id` + `tracking_number`; detalj porudžbine pokazuje `type` `P` i `need_pickup` `1`.
- [ ] Detalj porudžbine i lista pokazuju obe porudžbine na ovom nalogu.
- [ ] PDF lokalne nalepnice se otvara i pokazuje primaoca ili adresu preuzimanja.
- [ ] Javno praćenje vraća vremensku liniju bez tokena; najnoviji događaj je `100`.
- [ ] Stiže `order.created` i njegov v2 potpis se uspešno proverava.
- [ ] Otkazivanje vraća `result: true`, a drugo otkazivanje vraća `already_cancelled: true`.
- [ ] Paket od jedne dostave i jednog preuzimanja vraća dva rezultata, svaki sa svojim `ref`.
