# Usluge slanja

API usluga slanja omogućava nalogu kupca da rezerviše usluge slanja koje je njegov logistički partner podesio i dodelio mu. Sopstveni sistem kupca prikazuje listu usluga koje sme da koristi, učitava pravila jedne usluge, izračunava cenu pošiljke, kreira porudžbinu slanja, plaća je sa stanja naloga i prati pošiljku do dostave. Ovaj vodič je namenjen programerima koji povezuju sistem uvoznika, trgovca ili veletrgovca sa logističkim partnerom koji ga opslužuje.

## 1. Šta možete da izgradite

Svi primeri u ovom vodiču koriste jedan scenario. **Harbourline Imports Inc.**, uvoznik čaja iz Toronta, ima nalog kupca kod svog logističkog partnera. Partner nudi uslugu `intl_express` (International Express) iz svog skladišta Toronto Hub (ID skladišta `7`). Harbourline predaje dva kartona uzoraka čaja u Toronto Hub za distributera u Sijetlu, po svojoj nabavnoj porudžbini `HLI-PO-1058`.

- **Rezervacija iz sistema nabavnih porudžbina.** Kada se nabavna porudžbina odobri, Harbourline-ov sistem izračunava cenu pošiljke za `intl_express`, kreira porudžbinu slanja sa brojem nabavne porudžbine kao referencom i plaća je sa unapred uplaćenog stanja naloga, a da niko ne otvara portal partnera.
- **Provera cene pre obavezivanja.** Nabavljač u Harbourline-u vidi vozarinu, doplate, porez i ukupan iznos za dva kartona pre nego što se pošiljka rezerviše, a pošiljka za koju usluga ne može da izračuna cenu zaustavlja se pre nego što porudžbina postoji.
- **Status pošiljke u ERP-u.** Broj za praćenje svakog kartona čuva se uz nabavnu porudžbinu; webhook-ovi prenose status porudžbine i vremensku liniju praćenja u ERP, a noćni posao vrši usaglašavanje sa listom porudžbina.
- **Kontrolisane izmene.** Neplaćena rezervacija se ispravlja na licu mesta, a rezervacija koja više nije potrebna otkazuje se uz povraćaj plaćenog iznosa na kredit naloga.

## 2. Šta ovaj vodič obuhvata

Koristite ovu grupu kada je pozivalac **kupac** logističke firme i rezerviše jednu od sopstvenih usluga slanja te firme: firma određuje cenovni plan, skladišta, doplate i pakovanje i dodeljuje usluge kupcu. Kupac vidi i rezerviše samo usluge koje su mu dodeljene.

U sledećim slučajevima koristite drugu grupu:

- Pozivalac je sama logistička firma (poslovni/klijentski nalog) i rezerviše preuzimanja i dostave istog dana ili lokalna preuzimanja i dostave sopstvenom flotom: pročitajte **Preuzimanje i dostava (sopstvena flota)**.
- Pozivalac kupuje nalepnice prevoznika (na primer UPS ili FedEx) po ugovorenim tarifama naloga: pročitajte **Nalepnice prevoznika**.
- Kupac čuva robu u skladištu partnera i šalje je sa zaliha: pročitajte **Skladištenje i izlaz**.

**Uniorder: jedan API za svaku pošiljku** (`/api/v1/uniorder/...`) je preporučena jedinstvena ulazna tačka za nove integracije lokalne dostave i nalepnica prevoznika. Uniorder ne obuhvata usluge slanja: porudžbine usluga slanja kreiraju se i vode isključivo preko endpoint-a `/api/v1/customer/shipping-orders/...` opisanih ovde.

## 3. Pre nego što počnete

- **Vrsta naloga.** Nalog **kupca** logističke firme, sa **API dozvolom** koju je uključila firma. Poslovni/klijentski nalog ili nalog zaposlenog ne može da se prijavi preko prijave kupca navedene ispod.
- **Dodela usluga.** Firma mora kupcu da dodeli najmanje jednu aktivnu uslugu slanja. Kupac bez dodeljene usluge dobija praznu listu usluga.
- **Test podaci.** Dogovorite sa firmom test kod usluge, test skladište i malo unapred uplaćeno stanje na test nalogu. Koristite referencu kao što je `HLI-PO-1058` ili `DEV-SHIP-001`, kako bi se test porudžbine lako pronašle i otkazale.
- **Rukovanje tokenom.** API pozivajte isključivo sa svog servera. Lozinku i pristupni token ne stavljajte u pregledače i mobilne klijente. Token ističe nedelju dana posle prijave (`expires_at`); ponovo se prijavite pre isteka.
- **Zamenske vrednosti.** Zamenite `YOUR_HOST` nazivom hosta logističke firme, a `ACCESS_TOKEN` tokenom koji vraća korak prijave.
- **JSON greške.** Uz svaki zahtev šaljite `Accept: application/json`, kako bi greške validacije vraćale JSON umesto preusmeravanja.

## 4. Prijava kao kupac

Prijava menja e-poštu i lozinku kupca za bearer token. Svaki naredni poziv u ovom vodiču šalje taj token.

**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" \
  -H "Accept: application/json" \
  -d '{"email":"logistics@harbourline-imports.example","password":"your_password"}'
```

```json
{
  "result": true,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
  "token_type": "Bearer",
  "expires_at": "2026-10-05 09:15:00",
  "expires_timestamp": 1791206100,
  "name": "Harbourline Imports Inc."
}
```

- `access_token`: šaljite ga uz svaki zahtev kao `Authorization: Bearer ACCESS_TOKEN`. GraphQL koristi isto zaglavlje na `POST /api/graphql`.
- `expires_at` / `expires_timestamp`: zakažite novu prijavu pre ovog trenutka.

**Verifikacija:** odgovor sadrži `result: true` i `access_token`. Zahtev bez tokena vraća `401`; prijava nalogom koji nije nalog kupca ili koji nema API dozvolu takođe vraća `401`.

## 5. Lista usluga dodeljenih kupcu

Lista usluga pokazuje integraciji koje kodove usluga sme da rezerviše i da li svaka usluga prihvata predaju u skladištu, preuzimanje ili oboje. Sačuvajte `service_code`; svaki naredni poziv usluge ga koristi.

**REST:** `GET /api/v1/customer/shipping-orders/services` — [REST priručnik](/api/documentation#/paths/v1-customer-shipping-orders-services/get)

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/services \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "data": {
    "services": [
      {
        "id": 12,
        "service_code": "intl_express",
        "name": { "en": "International Express" },
        "offer_pickup": true,
        "allow_warehouse_delivery": true,
        "support_multi_package": true,
        "allow_special_requirements": false,
        "allow_purchase_supplies": true,
        "send_confirmation_email": true,
        "warehouses": [{ "id": 7, "name": "Toronto Hub" }]
      }
    ]
  }
}
```

- `service_code`: parametar putanje svakog narednog poziva usluge.
- `offer_pickup` / `allow_warehouse_delivery`: dozvoljene vrednosti za `origin_type` (`pickup` / `warehouse`).
- `support_multi_package`: da li jedna porudžbina može da sadrži više od jednog reda paketa.
- Prazan niz `services` znači da ovom kupcu nije dodeljena nijedna usluga.

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

```graphql
query CustomerShippingOrderServices {
  customerShippingOrderServices
}
```

**Verifikacija:** lista sadrži najmanje jednu uslugu i sačuvali ste njen `service_code` (u ovom vodiču: `intl_express`).

## 6. Učitavanje konfiguracije usluge

Konfiguracija vraća sve što je potrebno obrascu porudžbine jedne usluge: skladišta koja prihvataju predaju, doplate koje mogu da se izaberu, katalog pakovanja i potrošnog materijala, jedinice i zemlje iz kojih usluga može da preuzima i u koje može da dostavlja. Proverite podatke porudžbine prema njoj pre nego što izračunate cenu ili bilo šta kreirate.

**REST:** `GET /api/v1/customer/shipping-orders/services/{serviceCode}/config` — [REST priručnik](/api/documentation#/paths/v1-customer-shipping-orders-services-serviceCode--config/get)

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/config \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "data": {
    "service": {
      "service_code": "intl_express",
      "offer_pickup": true,
      "allow_warehouse_delivery": true,
      "allow_special_requirements": false,
      "allow_purchase_supplies": true,
      "weight_mode": 2,
      "volumetric_factor": 5000
    },
    "warehouses": [
      {
        "id": 7,
        "name": "Toronto Hub",
        "address_1": "250 Dundas St W",
        "city": "Toronto",
        "province": "ON",
        "country": "CA",
        "postcode": "M5T 2Z5",
        "telephone": "4165550100"
      }
    ],
    "surcharges": [],
    "has_surcharges": false,
    "packagings": [],
    "products": [],
    "weight_units": { "2": { "name": "weight_kg", "accuracy": 3 } },
    "dimension_units": { "2": { "name": "dimension_cm", "accuracy": 1 } },
    "delivery_allowed_countries": ["CA", "US"],
    "pickup_allowed_countries": ["CA"]
  }
}
```

- `warehouses[].id`: `warehouse_id` koji se šalje kada je `origin_type` `warehouse`. ID koji nije na ovoj listi odbija se pri kreiranju.
- `service.weight_mode`: koja polja paketa zahteva cenovni plan: `0` stvarna težina (težina), `1` zapreminska težina (dužina, širina i visina), `2` obračunska težina (oboje). `null` znači da se cena usluge određuje ručno. Pošaljite težinu i sve tri dimenzije da biste zadovoljili svaki režim.
- `delivery_allowed_countries` / `pickup_allowed_countries`: odbijte zemlju odredišta ili preuzimanja van ovih lista pre poziva procene.
- `surcharges[].id`, `packagings[].id`, `products[].id`: ID-jevi za opcione doplate, pakovanje i kupovinu potrošnog materijala.
- `weight_units` / `dimension_units`: jedinice paketa šalju se kao brojevi. Pošaljite `weight_unit: 2` (kg) i `dimension_unit: 2` (cm), kao u svim primerima ovog vodiča; obe su i podrazumevane vrednosti kada su polja izostavljena.

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

```graphql
query CustomerShippingOrderServiceConfig {
  customerShippingOrderServiceConfig(serviceCode: "intl_express")
}
```

**Verifikacija:** `result` je `true` i, za predaju u skladištu, `warehouses` sadrži skladište koje nameravate da koristite. `403` znači da usluga nije dodeljena ovom kupcu; `404` znači da kod usluge ne postoji ili je neaktivan.

## 7. Procena cene

Procena izračunava cenu pošiljke prema cenovnom planu usluge, bez upisivanja bilo čega. Prikažite ukupan iznos nabavljaču i ne kreirajte porudžbinu kada procena javi odbijanje.

**REST:** `POST /api/v1/customer/shipping-orders/services/{serviceCode}/estimate-price` — [REST priručnik](/api/documentation#/paths/v1-customer-shipping-orders-services-serviceCode--estimate-price/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/estimate-price \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "delivery_postcode": "98104",
    "delivery_country": "US",
    "packages": [{
      "weight": 12,
      "length": 50,
      "width": 40,
      "height": 35,
      "weight_unit": 2,
      "dimension_unit": 2,
      "quantity": 2
    }]
  }'
```

```json
{
  "result": true,
  "data": {
    "needs_manual_quote": false,
    "shipping_fee": 118.40,
    "shipping_fee_total": 131.20,
    "fuel_surcharge": 12.80,
    "pickup_fee": null,
    "surcharges_total": 0,
    "sub_total": 131.20,
    "tax": { "total_tax": 17.06 },
    "total": 148.26,
    "currency": "CAD",
    "all_fees_calculated": true,
    "has_items_needing_quote": false,
    "unpriced_items": [],
    "refused": false,
    "refusal_message": null
  }
}
```

- `origin_type`: `warehouse` (kupac predaje robu u skladištu; pošaljite `warehouse_id`) ili `pickup` (partner preuzima; pošaljite `pickup_postcode` i `pickup_country`). Koristite samo vrednost koju korak 5 dozvoljava.
- `packages`: jedan red po grupi identičnih paketa; `quantity` množi red.
- `total` i `currency`: iznos za prikaz. `total` je `null` dok bilo koja naknada nije izračunata.
- `needs_manual_quote` / `has_items_needing_quote`: firma ručno određuje cenu porudžbine; porudžbina može da se kreira i plaća se kada firma odredi cenu.
- `refused` / `refusal_message`: usluga odbija pošiljke za koje ne može da izračuna cenu. Ne kreirajte porudžbinu; umesto toga prikažite `refusal_message`.
- Opcioni ulazi: `surcharges`, `products` (mapa ID-ja proizvoda na količinu, uzima se u obzir samo kada je `allow_purchase_supplies` true), `has_special_requirements`, `coupon_code`.

**Verifikacija:** `result` je `true`, `refused` je `false`, a `total` ima vrednost ili je `needs_manual_quote` `true`.

## 8. Kreiranje porudžbine slanja

Poziv kreiranja rezerviše pošiljku na usluzi. Integracija čuva vraćeni `id` uz sopstvenu nabavnu porudžbinu; svaki naredni poziv koristi ovaj ID.

**REST:** `POST /api/v1/customer/shipping-orders/services/{serviceCode}/orders` — [REST priručnik](/api/documentation#/paths/v1-customer-shipping-orders-services-serviceCode--orders/post)

Pošaljite `Idempotency-Key` izveden iz sopstvenog stabilnog ID-ja (ovde broj nabavne porudžbine). Ponovni pokušaj sa istim ključem i istim telom vraća prvi odgovor sa `"replayed": true` i zaglavljem `Idempotency-Replayed: true` i ne kreira drugu porudžbinu.

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: hli-po-1058" \
  -d '{
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "reference": "HLI-PO-1058",
    "delivery_name": "Cascade Tea Distributors",
    "delivery_telephone": "2065550143",
    "delivery_email": "receiving@cascadetea.example",
    "delivery_address_1": "300 5th Ave S",
    "delivery_city": "Seattle",
    "delivery_province": "WA",
    "delivery_country": "US",
    "delivery_postcode": "98104",
    "note": "Two cartons of sample tea, dock door B",
    "package": [{
      "description": "Sample tea carton",
      "weight": 12,
      "length": 50,
      "width": 40,
      "height": 35,
      "weight_unit": 2,
      "dimension_unit": 2,
      "value": 380,
      "quantity": 2
    }]
  }'
```

```json
{
  "result": true,
  "data": {
    "id": 9001,
    "status": 0,
    "total_price": 148.26,
    "price_breakdown": { "total": 148.26, "currency": "CAD" },
    "promotion_id": null,
    "tracking_number": null
  }
}
```

- Ključ tela za pakete pri kreiranju je `package` (kod procene je `packages`). Svaki red sa `quantity` N postaje N paketa, a svaki paket dobija sopstveni broj za praćenje.
- Obavezna polja: `delivery_name`, `delivery_telephone`, `delivery_address_1`, `delivery_city`, `delivery_province`, `delivery_country`, `delivery_postcode`, `origin_type`, `package[].weight`; plus `warehouse_id` za `warehouse`, ili `pickup_name`, `pickup_telephone`, `pickup_address_1`, `pickup_city`, `pickup_province`, `pickup_country`, `pickup_postcode` za `pickup`.
- Opciona polja: `reference` (čuva se kao `reference_number` porudžbine), `delivery_email`, `delivery_address_2`, `scheduled_date`, `time_window`, `note`, `special_requirements` (niz tekstualnih redova, uzima se u obzir samo kada ih usluga dozvoljava), `products`, `surcharges`, `coupon_code`.
- `id`: sačuvajte ga. `status` `0` je Na čekanju (čeka plaćanje).
- `total_price`: iznos koji naplaćuje korak 9. Iznosi `0` dok porudžbina čeka ručnu ponudu.
- `tracking_number` na nivou porudžbine je `null`; brojevi za praćenje nalaze se na paketima i čitaju se u koraku 10.
- Endpoint odgovara sa HTTP `201` za novu porudžbinu.

**Verifikacija:** odgovor sadrži `result: true` i `id`. Ponavljanje istog zahteva sa istim `Idempotency-Key` vraća isti `id` sa `"replayed": true`.

## 9. Plaćanje porudžbine sa stanja naloga

Porudžbine slanja plaćaju se u celosti sa stanja naloga kupca. Plaćena porudžbina prelazi iz statusa Na čekanju u Potvrđeno, a partner počinje da je obrađuje.

Prvo pročitajte iznos:

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

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/9001/payment-info \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "data": {
    "order_id": 9001,
    "currency": "CAD",
    "total_price": "148.26",
    "paid_amount": "0.00",
    "remaining_balance": "148.26",
    "user_balance": "500.00",
    "has_sufficient_balance": true,
    "shortfall": 0,
    "payment_options": [
      { "type": "remaining_balance", "amount": 148.26 }
    ]
  }
}
```

Zatim platite:

**REST:** `POST /api/v1/customer/shipping-orders/{id}/pay` — [REST priručnik](/api/documentation#/paths/v1-customer-shipping-orders-id--pay/post)

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

```json
{
  "result": true,
  "message": "Payment of $148.26 processed successfully.",
  "data": {
    "order_id": 9001,
    "status": 1,
    "amount_paid": "148.26"
  }
}
```

- `has_sufficient_balance` / `shortfall`: kada stanje ne pokriva `remaining_balance`, dopunite nalog pre plaćanja.
- `payment_type`: podržano je samo `remaining_balance`; uvek se naplaćuje ceo preostali iznos.
- `data.status` `1` je Potvrđeno.

**Verifikacija:** poziv plaćanja vraća `result: true` i `status` `1`, a drugi poziv `payment-info` vraća `400` jer je porudžbina plaćena u celosti. Poziv plaćanja bez dovoljnog stanja vraća `422` i ništa ne naplaćuje.

## 10. Čitanje porudžbine i praćenje paketa

Poziv detalja vraća trenutni status i broj za praćenje svakog paketa. Sačuvajte brojeve za praćenje paketa uz nabavnu porudžbinu; javno praćenje prihvata svaki od njih.

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

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/9001 \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "data": {
    "id": 9001,
    "status": 1,
    "status_name": "Confirmed",
    "can_edit": false,
    "can_cancel": true,
    "shipping_code": "K7RW2Q",
    "tracking_number": null,
    "reference_number": "HLI-PO-1058",
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "delivery_address": { "name": "Cascade Tea Distributors", "city": "Seattle", "country": "US" },
    "package_count": 2,
    "packages": [
      { "id": 55101, "description": "Sample tea carton", "tracking_number": "SR123456789012", "weight": 12, "weight_unit": 2 },
      { "id": 55102, "description": "Sample tea carton", "tracking_number": "SR123456789013", "weight": 12, "weight_unit": 2 }
    ],
    "total_price": 148.26
  }
}
```

- `status`: `0` Na čekanju, `1` Potvrđeno, `2` U tranzitu, `3` Poslato, `4` Otkazano, `5` Neuspešno, `6` Delimično preuzeto, `7` Preuzeto, `8` U obradi.
- `can_edit` / `can_cancel`: da li je korak 12 trenutno dozvoljen.
- `packages[].tracking_number`: brojevi koje treba sačuvati i pratiti.
- `shipping_code`: kod koji prihvataju ekrani za predaju u skladištu; odštampajte ga na dokumentaciji za predaju.

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

```graphql
query CustomerShippingOrderShow {
  customerShippingOrderShow(id: 9001) {
    result
    message
    data {
      id
      status
      status_name
      can_cancel
      reference_number
      packages {
        id
        tracking_number
        weight
      }
      total_price
    }
  }
}
```

Da biste usaglasili sve porudžbine jedne usluge, na primer u noćnom poslu, prikažite ih u listi sa filterom. Filter `id` odgovara ID-ju porudžbine, broju za praćenje ili referenci.

**REST:** `GET /api/v1/customer/shipping-orders/services/{serviceCode}/orders` — [REST priručnik](/api/documentation#/paths/v1-customer-shipping-orders-services-serviceCode--orders/get)

```bash
curl "https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/orders?id=HLI-PO-1058&created_at_from=2026-09-01&per_page=20" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Accept: application/json"
```

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

Javno praćenje ne zahteva token i vraća vremensku liniju događaja jednog paketa:

**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 \
  -H "Accept: application/json"
```

```json
{
  "result": true,
  "deliveried": false,
  "data": [
    {
      "tracking_event_status_id": 1,
      "otep_status": "received",
      "description": "Received at warehouse",
      "location_city": "Toronto",
      "updated_at": "2026-09-29 10:42:00"
    }
  ]
}
```

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

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

**Verifikacija:** detalj vraća porudžbinu ovog kupca sa jednim brojem za praćenje po paketu, a javno praćenje vraća `result: true` za broj za praćenje paketa. ID porudžbine drugog kupca vraća `404`.

## 11. Prijem webhook-ova

Webhook-ovi dostavljaju kreiranje porudžbine, promene statusa i događaje praćenja vašem serveru, tako da integracija ne mora periodično da proverava. Nalog kupca podešava sopstvene webhook URL-ove i tajnu za potpisivanje.

Kada se kreira porudžbina slanja, partner kreira i povezanu porudžbinu preuzimanja za svoj dispečerski tim. Webhook-ovi se šalju za tu povezanu porudžbinu: njen `ref` je `Shipping-Pickup-{shipping order id}` (na primer `Shipping-Pickup-9001`), a svaki njen paket nosi broj za praćenje paketa slanja u `external_tracking_number`. Dolazne događaje povezujte prema ova dva polja.

| Podešavanje | Događaj | Šta integracija radi |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Povezuje događaj sa porudžbinom slanja preko `ref` i `packages[].external_tracking_number` |
| `tracking_event_webhook_url` | `tracking.event` | Dodaje događaj u vremensku liniju paketa |
| `order_status_change_webhook_url` | `order.status_change` | Ažurira status prikazan u vašem sistemu |

**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" \
  -H "Accept: application/json" \
  -d '{
    "order_create_webhook_url": "https://erp.harbourline-imports.example/hooks/order-created",
    "tracking_event_webhook_url": "https://erp.harbourline-imports.example/hooks/tracking",
    "order_status_change_webhook_url": "https://erp.harbourline-imports.example/hooks/status",
    "webhook_sign_secret": "hli-webhook-secret-7f2c9a1e5b"
  }'
```

```json
{
  "result": true,
  "changed_keys": [
    "order_create_webhook_url",
    "tracking_event_webhook_url",
    "order_status_change_webhook_url",
    "webhook_sign_secret"
  ],
  "recipient_type": "customer",
  "settings": {
    "webhook_sign_secret": "*************************1e5b",
    "tracking_event_webhook_url": "https://erp.harbourline-imports.example/hooks/tracking"
  }
}
```

- Menjaju se samo poslati ključevi; prazan string briše URL. `webhook_sign_secret` mora imati od 16 do 255 znakova, a nijedan webhook se ne šalje dok je tajna prazna.
- `recipient_type` je `customer` za nalog kupca.

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

```graphql
mutation WebhookSettingsUpdate {
  webhookSettingsUpdate(
    order_status_change_webhook_url: "https://erp.harbourline-imports.example/hooks/status"
    tracking_event_webhook_url: "https://erp.harbourline-imports.example/hooks/tracking"
  )
}
```

Proverite **v2** potpis nad sirovim telom: `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** 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:** posle poziva podešavanja, jedno test kreiranje proizvodi događaj `order.created` čiji je `ref` `Shipping-Pickup-{id}` za ID nove porudžbine slanja, a provera potpisa prolazi.

## 12. Izmena ili otkazivanje porudžbine

Porudžbina može da se ispravi dok je Na čekanju (pre plaćanja) i da se otkaže dok je Na čekanju ili Potvrđena. Otkazivanje plaćene porudžbine vraća plaćeni iznos na kredit naloga.

Za izmenu ponovo pošaljite celu porudžbinu sa istim poljima kao u koraku 8. Cena se ponovo izračunava.

**REST:** `PUT /api/v1/customer/shipping-orders/{id}` — [REST priručnik](/api/documentation#/paths/v1-customer-shipping-orders-id/put)

```bash
curl -X PUT https://YOUR_HOST/api/v1/customer/shipping-orders/9001 \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "reference": "HLI-PO-1058",
    "delivery_name": "Cascade Tea Distributors",
    "delivery_telephone": "2065550143",
    "delivery_email": "receiving@cascadetea.example",
    "delivery_address_1": "300 5th Ave S",
    "delivery_address_2": "Suite 210",
    "delivery_city": "Seattle",
    "delivery_province": "WA",
    "delivery_country": "US",
    "delivery_postcode": "98104",
    "package": [{
      "description": "Sample tea carton",
      "weight": 12,
      "length": 50,
      "width": 40,
      "height": 35,
      "weight_unit": 2,
      "dimension_unit": 2,
      "value": 380,
      "quantity": 2
    }]
  }'
```

```json
{
  "result": true,
  "data": {
    "id": 9001,
    "status": 0,
    "total_price": 148.26,
    "promotion_id": null,
    "coupon_code": null,
    "promotion_discount": null
  }
}
```

Za otkazivanje:

**REST:** `POST /api/v1/customer/shipping-orders/{id}/cancel` — [REST priručnik](/api/documentation#/paths/v1-customer-shipping-orders-id--cancel/post)

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

```json
{
  "result": true,
  "data": {
    "id": 9001,
    "status": 4,
    "refund_amount": 148.26
  },
  "message": "Order cancelled. $148.26 refunded to your credit."
}
```

- `status` `4` je Otkazano. Povezana porudžbina preuzimanja se uklanja.
- `refund_amount`: iznos vraćen na kredit naloga; `0` za neplaćenu porudžbinu.
- Pročitajte `can_edit` i `can_cancel` iz koraka 10 pre nego što ove radnje ponudite korisnicima.

**Verifikacija:** otkazivanje vraća `status` `4`, a detalj pokazuje `status_name` `Cancelled`. Drugo otkazivanje, ili otkazivanje porudžbine koja je U tranzitu ili u kasnijem statusu, vraća `403` sa porukom `This order can no longer be cancelled.`; izmena plaćene porudžbine vraća `403`.

## 13. Obrada grešaka

| Situacija | HTTP status | Kod | Šta integracija radi |
|---|---|---|---|
| Token nedostaje, istekao je ili je nevažeći; prijava nalogom koji nije nalog kupca ili bez API dozvole | `401` | — | Ponovo se prijavite; ako sama prijava ne uspe, zatražite od firme da proveri vrstu naloga i API dozvolu |
| Usluga nije dodeljena ovom kupcu | `403` | — | Ponovo pročitajte listu usluga (korak 5) i rezervišite samo dodeljene usluge |
| API se poziva iz sesije aplikacije platforme čija aplikacija ima isključene porudžbine slanja | `403` | `APP_CAPABILITY_DISABLED` | Zatražite od firme da uključi porudžbine slanja za aplikaciju |
| Nepoznat ili neaktivan kod usluge; ID porudžbine nije pronađen za ovog kupca | `404` | — | Osvežite listu usluga; proverite sačuvani ID porudžbine |
| Obavezno polje nedostaje ili je nevažeće | `422` | — | Pročitajte `errors` u telu, ispravite polja i pošaljite ponovo |
| Usluga ne nudi tu vrstu polazišta ili skladište nije na listi usluge | `422` | — | Koristite `origin_type` i `warehouse_id` iz koraka 5 i 6 |
| Usluga ne može da izračuna cenu pošiljke i odbija pošiljke bez cene | `422` | `unpriced_refused` | Ništa nije kreirano; prikažite `message` i ne pokušavajte ponovo bez izmena |
| Poručen potrošni materijal kojeg nema na zalihi | `422` | — | Pročitajte `stock_shortages`, smanjite količine i pošaljite ponovo |
| Isti `Idempotency-Key` sa drugačijim telom | `409` | `IDEMPOTENCY_CONFLICT` | Za novu porudžbinu koristite novi ključ; nikada ne koristite isti ključ za drugačiji sadržaj |
| Ponovni pokušaj dok se prvi zahtev sa tim ključem još obrađuje | `409` | `IDEMPOTENCY_IN_PROGRESS` | Sačekajte `Retry-After` sekundi, zatim pokušajte ponovo sa istim ključem i telom |
| Plaćanje bez dovoljnog stanja | `422` | — | Dopunite nalog, zatim ponovo platite |
| Informacije o plaćanju ili plaćanje porudžbine koja je plaćena u celosti | `400` | — | Tretirajte porudžbinu kao plaćenu; pročitajte detalj |
| Otkazivanje nakon što je porudžbina napustila status Na čekanju ili Potvrđeno | `403` | — | Prikažite da porudžbina više ne može da se otkaže; obratite se firmi |
| Izmena posle plaćanja | `403` | — | Otkažite i kreirajte novu porudžbinu ili se obratite firmi |
| Greška servera tokom procene, kreiranja, plaćanja ili otkazivanja | `500` | — | Pokušajte ponovo jednom; za kreiranje, pokušajte ponovo sa istim `Idempotency-Key` |

## Lista provera

Koristite test referencu kao što je `DEV-SHIP-001` ili `HLI-PO-1058`:

- [ ] Prijava kupca vraća `access_token`; zahtev bez tokena vraća `401`.
- [ ] Lista usluga nije prazna i sačuvali ste jedan `service_code`.
- [ ] Konfiguracija vraća skladišta, jedinice i dozvoljene zemlje za tu uslugu, a vaš obrazac ih koristi.
- [ ] Procena vraća `total` (ili `needs_manual_quote: true`), a odbijena pošiljka se ne kreira.
- [ ] Kreiranje vraća `id`; isti `Idempotency-Key` sa istim telom vraća isti `id` sa `"replayed": true`.
- [ ] Plaćanje uspeva i status postaje Potvrđeno, ili ste potvrdili da nedovoljno stanje vraća `422` i ništa ne naplaćuje.
- [ ] Detalj pokazuje porudžbinu ovog kupca sa jednim brojem za praćenje po paketu, a javno praćenje pronalazi svaki paket.
- [ ] Webhook-ovi su podešeni sa tajnom za potpisivanje; test kreiranje proizvodi `order.created` sa `ref` `Shipping-Pickup-{id}` i provera potpisa prolazi.
- [ ] Otkazivanje test porudžbine vraća `status` `4` i očekivani `refund_amount`; drugo otkazivanje vraća `403`.
