# Nalepnice prevoznika

Usluga nalepnica kupuje nalepnice za slanje od prevoznika povezanih sa nalogom (na primer UPS i Canada Post) i čuva svaku nalepnicu kao porudžbinu. Integracija prikazuje listu metoda slanja naloga, traži ponudu za paket, kreira porudžbinu nalepnice, kupuje nalepnicu kod izabrane usluge prevoznika, štampa PDF, prati paket i otkazuje nalepnice koje se ne koriste. Namenjena je onlajn prodavnicama, skladišnim sistemima i sistemima za upravljanje porudžbinama koji šalju pakete preko prevoznika, a ne preko sopstvenih vozača.

## 1. Šta možete da izgradite

Primeri u ovom vodiču prate jednu firmu: **Northbound Outfitters**, onlajn prodavnicu opreme za boravak u prirodi koja šalje iz svog skladišta na adresi 1200 Eglinton Ave E, Toronto. Njen nalog ima metodu Canada Post i metodu UPS. Tipična porudžbina je kutija sa šatorom od 4,2 kg, dimenzija 60 × 30 × 25 cm, sa odredištem u Kalgariju; porudžbine za Sjedinjene Države šalju se preko UPS-a.

- **Izbor prevoznika pri plaćanju.** Prodavnica traži ponudu za korpu kupca kod Canada Post-a, prikazuje usluge sa cenom i danima tranzita i šalje uslugom koju je kupac platio.
- **Štampa nalepnice jednim klikom u skladištu.** Stanica za pakovanje kreira porudžbinu nalepnice kada je kutija spakovana, kupuje nalepnicu kod izabrane usluge i štampa PDF prevoznika na termalnom štampaču.
- **Prekogranične pošiljke sa carinskim podacima.** Porudžbine za Sjedinjene Države nose stavke (opis, količina, vrednost, HS kod), tako da se UPS nalepnica izdaje sa komercijalnim podacima.
- **Automatsko ažuriranje statusa za kupca.** Prodavnica čuva broj za praćenje prevoznika, prikazuje javnu vremensku liniju praćenja na stranici porudžbine i ažurira porudžbinu kada webhook `tracking.event` javi da je paket dostavljen.

## 2. Šta ovaj vodič obuhvata

Ovaj vodič obuhvata v1 uslugu nalepnica (`/api/v1/labelservice/...`): jednu metodu slanja (jedan nalog prevoznika) po pozivu. Koristite je kada integracija već zna kojom metodom slanja šalje ili kada održava postojeću integraciju usluge nalepnica.

Za nove integracije, Uniorder (`/api/v1/uniorder/...`) je preporučena jedinstvena ulazna tačka. Vodiči za Uniorder, „Uniorder: jedan API za svaku pošiljku” i „Ponuda i porudžbina u jednom toku”, daju ponudu za sve usluge prevoznika na nalogu odjednom (zajedno sa sopstvenom dostavom firme, gde se primenjuje) i kupuju nalepnicu kod usluge izabrane vraćanjem njenog `rate_id`. Isti pozivi zatim štampaju, prate i otkazuju svaku porudžbinu.

Drugi vodiči obuhvataju ostale vrste pošiljki:

- Dostava sopstvenim vozačima firme: „Preuzimanje i dostava (sopstvena flota)”.
- Nalog kupca koji šalje preko usluga koje nudi njegova firma: „Usluge slanja”.
- Roba koja se čuva u skladištu i šalje na zahtev: „Skladištenje i izlaz”.

## 3. Pre nego što počnete

- **Nalog.** Koristite poslovni (klijentski) nalog, zaposlenog tog naloga ili nalog kupca neke firme. Poslovni nalog vidi sopstvene metode slanja. Nalog kupca vidi samo metode koje mu je firma dodelila, a svaka nalepnica koju kupi naplaćuje se sa njegovog stanja; kada je firma za tog kupca uključila Auto Pause Label Service, nalepnica se odbija dok stanje uvećano za kredit ne pokriva njenu cenu.
- **API dozvola.** Nalog mora imati uključen API pristup. Bez njega svaki poziv usluge nalepnica vraća `401` sa `Unauthorized`.
- **Metode slanja.** Na nalogu mora biti aktivna najmanje jedna metoda slanja (za kupce: dodeljena kupcu). Identifikatori metoda razlikuju se po nalogu i ne smeju biti fiksno upisani u kod; pročitajte ih u koraku 5.
- **Test podaci.** Koristite test metodu slanja ili sandbox prevoznika tamo gde je podešen (tarife tada nose `test_mode: true`) i odredište koje kontrolišete. Otkažite svaku test nalepnicu kupljenu na produkcionoj metodi.
- **Rukovanje tokenom.** Prijavite se sa svog servera i čuvajte token tamo. Ne stavljajte token ni lozinku u pregledač ili mobilnu aplikaciju.
- **Zamenske vrednosti.** Zamenite `YOUR_HOST` hostom vaše platforme, a `ACCESS_TOKEN` tokenom iz koraka 4. Zamenite vrednosti `shipping_method` ID-jevima vašeg naloga.

## 4. Prijava

Svaki poziv usluge nalepnica zahteva bearer token. Integracija se prijavljuje jednom, čuva `access_token` i `expires_at` na serveru i ponovo se prijavljuje pre isteka tokena.

**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":"shipping@northbound-outfitters.ca","password":"your_password"}'
```

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

- `access_token`: šaljite ga uz svaki naredni poziv kao zaglavlje ispod.
- `expires_at`: ponovo se prijavite pre ovog trenutka.

```
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. Lista metoda slanja

Lista metoda pokazuje integraciji sa kojim nalozima prevoznika sme da šalje i koje opcije svaki od njih prihvata. Sačuvajte `id` svake metode koju koristite; to je `shipping_method` u svakom narednom pozivu.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingMethodList \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"detail": true}'
```

```json
[
  {
    "id": 59,
    "name": "Canada Post",
    "unique_identifier": "CPC-TOR",
    "options": {
      "signature_option": true,
      "insurance_option": true,
      "insurance_value": true,
      "multi_package": true
    },
    "package_type": {
      "parcel": {
        "name": "Parcel",
        "options": { "weight_options": true, "dimension_options": true }
      }
    },
    "from_contry_limit": ["CA"],
    "isUploadMethod": false
  },
  {
    "id": 61,
    "name": "UPS",
    "unique_identifier": "UPS-TOR",
    "options": {
      "signature_option": true,
      "insurance_option": true,
      "insurance_value": true,
      "multi_package": true
    }
  }
]
```

Svaki red sadrži:

| Polje | Upotreba |
|---|---|
| `id` | `shipping_method` u svakom narednom pozivu |
| `name` | Naziv za prikaz |
| `unique_identifier` | Stabilan kod |
| `options.signature_option` | Potpis je dostupan |
| `options.insurance_option` | Osiguranje je dostupno |
| `options.multi_package` | Više od jednog komada |
| `package_type` | Prihvaćeni kodovi `package_type` i da li svaki zahteva težinu i dimenzije |
| `from_contry_limit` | Zemlje u kojima adresa pošiljaoca može da se nalazi |
| `services` | Prevoznici i usluge iza metode; kodovi mogu da ograniče ponudu pomoću `carriers` / `services` |

Pošaljite `"id": 59` da biste pročitali samo jednu metodu ili `"detail": false` da biste dobili samo `id`, `name` i `unique_identifier`.

**GraphQL:** `labelserviceGetShippingMethodList` ([GraphQL priručnik](/api/graphql/documentation#/labelservice/labelserviceGetShippingMethodList)) (JSON skalar).

**Verifikacija:** lista nije prazna. Izabrali ste jedan `id` i znate da li ta metoda dozvoljava potpis, osiguranje i više paketa. Prazna lista znači da nijedna metoda nije uključena na nalogu.

## 6. Ponuda

Ponuda traži cene od prevoznika bez kreiranja bilo čega: privremena porudžbina korišćena za zahtev se briše i ništa se ne naplaćuje. Northbound Outfitters je poziva pri plaćanju da bi prikazao usluge Canada Post-a za korpu. Telo ima isti oblik kao u koraku 7. `shipping_method` je obavezan.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "shipping_method": 59,
    "name": "Emily Tremblay",
    "telephone": "4035550182",
    "email": "emily.tremblay@example.com",
    "address_1": "1415 17 Ave SW",
    "city": "Calgary",
    "province": "AB",
    "postcode": "T2T0C8",
    "country": "CA",
    "weight": 4.2,
    "length": 60,
    "width": 30,
    "height": 25,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "NB-10482",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA"
  }'
```

```json
{
  "result": true,
  "shipping_price": "24.86",
  "price_details": { "shipping_fee": "24.86" },
  "rates": [
    {
      "carrier_name": "canadapost",
      "currency": "CAD",
      "service_code": "canadapost_expedited_parcel",
      "service_name": "CANADAPOST EXPEDITED PARCEL",
      "price": 24.86,
      "transit_days": 3,
      "test_mode": false
    },
    {
      "carrier_name": "canadapost",
      "currency": "CAD",
      "service_code": "canadapost_xpresspost",
      "service_name": "CANADAPOST XPRESSPOST",
      "price": 38.12,
      "transit_days": 2,
      "test_mode": false
    }
  ],
  "best_rate": {
    "service_code": "canadapost_expedited_parcel",
    "price": 24.86,
    "transit_days": 3
  }
}
```

- `rates[]`: jedan unos po usluzi prevoznika, sa `price`, `currency`, `transit_days` i `price_detail` (osnovna cena, doplata za gorivo, porezi). Prikažite ih kupcu.
- `best_rate` / `shipping_price`: prva tarifa koju metoda vrati.
- Ponuda ne nosi `rate_id` ni `id` porudžbine. Nalepnice se kupuju iz tarifa porudžbine kreirane u koraku 7.

`weight_unit`: `1` g, `2` kg, `3` oz, `4` lb. `dimension_unit`: `1` mm, `2` cm, `3` m, `4` in.

Za više od jednog komada pošaljite `packages: [{ ref, weight, length, width, height, weight_unit, dimension_unit }]`. `shipping_from` (ID iz adresara) ili `shipping_from_code` može da zameni blok `sender_*`. `carriers` i `services` ograničavaju ponudu na navedene kodove.

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

**Verifikacija:** `result` je true i imate cenu (i dane tranzita, kada ih prevoznik pošalje). Ako nema tarife, ispravite odredište / paket / metodu **pre** kreiranja.

## 7. Kreiranje porudžbine nalepnice

Ovaj poziv kreira porudžbinu nalepnice i traži od prevoznika tarife za tu pošiljku. Vraća `id` porudžbine i jedan `rate_id` po usluzi. Nalepnica u ovom trenutku još nije kupljena i ništa se ne naplaćuje; kupuje je korak 8. Northbound Outfitters ga poziva kada je kutija spakovana i čuva `id` uz svoju porudžbinu `NB-10482`.

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

Isto telo kao u koraku 6. Pošaljite `Idempotency-Key`: ponovni pokušaj sa istim ključem i istim telom vraća prvi odgovor umesto da kreira drugu porudžbinu.

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nb-10482-label" \
  -d '{
    "shipping_method": 59,
    "name": "Emily Tremblay",
    "telephone": "4035550182",
    "email": "emily.tremblay@example.com",
    "address_1": "1415 17 Ave SW",
    "city": "Calgary",
    "province": "AB",
    "postcode": "T2T0C8",
    "country": "CA",
    "weight": 4.2,
    "length": 60,
    "width": 30,
    "height": 25,
    "dimension_unit": 2,
    "weight_unit": 2,
    "signature_option": 0,
    "insurance_option": 0,
    "package_type": "parcel",
    "ref": "NB-10482",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA"
  }'
```

```json
{
  "result": true,
  "id": 128455,
  "shipping_price": "24.86",
  "price_details": { "shipping_fee": "24.86" },
  "rates": [
    {
      "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10",
      "carrier_name": "canadapost",
      "service_code": "canadapost_expedited_parcel",
      "service_name": "CANADAPOST EXPEDITED PARCEL",
      "price": 24.86,
      "currency": "CAD",
      "transit_days": 3
    },
    {
      "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c11",
      "carrier_name": "canadapost",
      "service_code": "canadapost_xpresspost",
      "service_name": "CANADAPOST XPRESSPOST",
      "price": 38.12,
      "currency": "CAD",
      "transit_days": 2
    }
  ],
  "best_rate": {
    "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10",
    "service_code": "canadapost_expedited_parcel",
    "price": 24.86
  }
}
```

| Polje | Upotreba |
|---|---|
| `id` | ID porudžbine u Superroute-u — kupovina, preuzimanje i otkazivanje |
| `rates[].rate_id` | Usluga koja se kupuje u koraku 8; važi samo za ovu porudžbinu |
| `rates[].price` | Cena te usluge |
| `shipping_price` | Cena za `best_rate` |

Pošiljka za Sjedinjene Države ide preko UPS metode sa stavkama potrebnim za carinu. Ovaj primer koristi oblik `packages`, koji nosi stavke po kutiji:

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitOrder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nb-10497-label" \
  -d '{
    "shipping_method": 61,
    "name": "Daniel Price",
    "telephone": "2065550117",
    "email": "daniel.price@example.com",
    "address_1": "500 Mercer St",
    "city": "Seattle",
    "province": "WA",
    "postcode": "98109",
    "country": "US",
    "package_type": "parcel",
    "paid_by": 1,
    "ref": "NB-10497",
    "sender_name": "Northbound Outfitters",
    "sender_telephone": "4165550140",
    "sender_address_1": "1200 Eglinton Ave E",
    "sender_city": "Toronto",
    "sender_province": "ON",
    "sender_postcode": "M3C1H9",
    "sender_country": "CA",
    "packages": [
      {
        "ref": "NB-10497-1",
        "weight": 2.6,
        "length": 45,
        "width": 30,
        "height": 20,
        "weight_unit": 2,
        "dimension_unit": 2,
        "items": [
          {
            "name": "Down sleeping bag",
            "description": "Down-filled sleeping bag, -7 C rating",
            "quantity": 1,
            "unit_price": 289.00,
            "currency": "CAD",
            "weight": 1.6,
            "hscode": "9404400000",
            "sku": "NB-SB-7C",
            "unit": "PCS"
          },
          {
            "name": "Camp stove",
            "description": "Canister camp stove",
            "quantity": 1,
            "unit_price": 79.00,
            "currency": "CAD",
            "weight": 1.0,
            "hscode": "7321111000",
            "sku": "NB-ST-01",
            "unit": "PCS"
          }
        ]
      }
    ]
  }'
```

**GraphQL:** `labelserviceSubmitOrder` ([GraphQL priručnik](/api/graphql/documentation#/labelservice/labelserviceSubmitOrder)). REST telo se stavlja u `input`:

```graphql
mutation {
  labelserviceSubmitOrder(input: {
    shipping_method: 59
    name: "Emily Tremblay"
    telephone: "4035550182"
    address_1: "1415 17 Ave SW"
    city: "Calgary"
    province: "AB"
    postcode: "T2T0C8"
    country: "CA"
    weight: 4.2
    length: 60
    width: 30
    height: 25
    dimension_unit: 2
    weight_unit: 2
    package_type: "parcel"
    ref: "NB-10482"
    sender_name: "Northbound Outfitters"
    sender_telephone: "4165550140"
    sender_address_1: "1200 Eglinton Ave E"
    sender_city: "Toronto"
    sender_province: "ON"
    sender_postcode: "M3C1H9"
    sender_country: "CA"
  })
}
```

**Verifikacija:** odgovor sadrži `id` i najmanje jedan `rates[].rate_id`. Sačuvajte oba. Isti `Idempotency-Key` sa istim telom vraća isti `id` i ne kreira drugu porudžbinu.

## 8. Kupovina nalepnice i čitanje detalja pošiljke

Ovaj poziv kupuje nalepnicu kod izabrane usluge, naplaćuje je i vraća brojeve za praćenje prevoznika. Kada je nalepnica već kupljena, samo čita detalje, tako da ponovljeni poziv nikada ne kupuje dva puta. Northbound Outfitters šalje `rate_id` usluge koju je kupac platio.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingDetail \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "128455",
    "type": "ORDER_ID",
    "rate_id": "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10"
  }'
```

```json
{
  "id": 128455,
  "shippingPrice": "24.86",
  "mainTrackingNumber": "7023210039414604",
  "trackingNumber": "7023210039414604",
  "needSubmitShippingInformation": false,
  "rate": {
    "carrier_name": "canadapost",
    "price": 24.86,
    "price_detail": [
      { "name": "Base charge", "amount": 18.40 },
      { "name": "Fuel surcharge", "amount": 3.60 },
      { "name": "GST", "amount": 1.10 }
    ],
    "tax_items": ["HST", "GST", "PST", "QST"]
  },
  "labelStatus": "ready",
  "shippingLabel": "JVBERi0xLjQKMS... (base64 encoded)"
}
```

| Polje | Upotreba |
|---|---|
| `mainTrackingNumber` | Broj za praćenje prevoznika za prvi paket; dajte ga kupcu |
| `trackingNumber` | Brojevi za praćenje prevoznika za sve pakete, razdvojeni zarezom |
| `shippingPrice` | Naplaćeni iznos |
| `labelStatus` | `ready`: `shippingLabel` sadrži PDF. `pending`: kupljeno i naplaćeno, prevoznik još nije napravio datoteku; pozovite ponovo kasnije. `failed`: preuzimanje u pozadini je odustalo; ponovni poziv ga pokreće iznova |
| `needSubmitShippingInformation` | `true` kada ova metoda zahteva slanje informacija o pošiljci (korak 13) |

`type` može biti `ORDER_ID` (podrazumevano), `TRACKING_NUMBER` (broj paketa u Superroute-u) ili `THIRD_PARTY_TRACKING_NUMBER` (broj prevoznika). Pošaljite `rate_id` kako bi nalepnica bila kupljena kod usluge koju ste izabrali; bez njega metoda kupuje po svojoj podrazumevanoj tarifi.

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

```graphql
mutation {
  labelserviceGetShippingDetail(
    id: "128455"
    type: "ORDER_ID"
    rate_id: "rat_5f0c2a9e41d84b7c9a3e6d21b8f47c10"
  )
}
```

**Verifikacija:** `mainTrackingNumber` nije prazan, a `labelStatus` je `ready` (ili `pending`, koji pri nekom kasnijem pozivu postaje `ready`). Drugi poziv vraća isti broj za praćenje i isti `shippingPrice`.

## 9. Preuzimanje PDF-a

Skladište štampa nalepnicu prevoznika iz ovog poziva. Ako nalepnica još nije kupljena, prvi poziv je kupuje po podrazumevanoj tarifi, kao u koraku 8; da biste odredili uslugu, prvo pozovite korak 8.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/getShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "128455",
    "type": "ORDER_ID",
    "base64": 1
  }'
```

```json
"JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURlY29kZT4+..."
```

- Uz `base64: 1` telo je PDF kao jedan base64 string; dekodirajte ga i pošaljite štampaču.
- Uz `base64: 0` odgovor je sama PDF datoteka (`application/pdf`).

`type` može biti `ORDER_ID` (podrazumevano), `TRACKING_NUMBER` ili `THIRD_PARTY_TRACKING_NUMBER` (broj prevoznika). Ovo je **zvanična nalepnica prevoznika**. Broj komada je određen rezervacijom.

**GraphQL:** `labelserviceGetShippingLabel` ([GraphQL priručnik](/api/graphql/documentation#/labelservice/labelserviceGetShippingLabel)). GraphQL uvek vraća base64 string.

**Verifikacija:** PDF se otvara i pokazuje barkod / broj za praćenje prevoznika iz koraka 8. Odštampajte jedan test primerak, a zatim ga bacite — ne predajte test nalepnicu prevozniku.

## 10. Praćenje

Prodavnica prikazuje napredak paketa na stranici porudžbine kupca. Endpoint javnog praćenja ne zahteva token i prihvata broj prevoznika iz koraka 8.

**REST:** `GET /api/v1/tracking/{trackingNumber}` — [REST priručnik](/api/documentation#/paths/v1-tracking-trackingNumber/get)

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

```json
{
  "result": true,
  "is_third_party_tracking": true,
  "deliveried": false,
  "data": [
    {
      "tracking_event_status_id": 430,
      "otep_status": "in_transit",
      "description": "Item in transit",
      "location_city": "Mississauga",
      "updated_at_localized": "2026-09-29 18:42"
    }
  ]
}
```

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

```graphql
query {
  trackingPublic(trackingNumber: "7023210039414604") {
    result
    deliveried
    is_third_party_tracking
    data {
      tracking_event_status_id
      otep_status
      description
      updated_at_localized
    }
    third_party_info { tracking_number status carrier_tracking_link }
    proofs { file_id type full_url signed_url }
  }
}
```

- `is_third_party_tracking` je true kada događaji dolaze od prevoznika.
- `data`: najnoviji događaj prvi. Grananje zasnivajte na `tracking_event_status_id` / `otep_status`, ne na `description`. Rani događaji mogu i dalje biti „informacije poslate” dok prevoznik ne skenira paket.
- `deliveried` je true, a `500` znači dostavljeno; `proofs[]` tada može da sadrži potpis (`type` `1`) ili fotografiju (`type` `2`).

**Verifikacija:** pretraga vraća pošiljku koju ste upravo kreirali. Nepoznat ili otkazan broj vraća `404` sa `result: false`.

## 11. Konfiguracija obaveštenja o događajima

Webhook-ovi zamenjuju periodični upit: server prodavnice prima svako skeniranje prevoznika i ažurira porudžbinu bez pozivanja koraka 10 po rasporedu.

| Podešavanje | Događaj | Kada |
|---|---|---|
| `tracking_event_webhook_url` | `tracking.event` | Skeniranja prevoznika, na dostavi, dostavljeno |
| `order_status_change_webhook_url` | `order.status_change` | Status u vašem sistemu |
| `order_create_webhook_url` | `order.created` | Porudžbina nalepnice je kreirana (korak 7); za porudžbine nalepnica šalje se samo kada je `order_created_webhook_all_types` `1` |

**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://shop.northbound-outfitters.ca/hooks/superroute",
    "order_create_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_created_webhook_all_types": 1,
    "webhook_sign_secret": "YOUR_LONG_RANDOM_SECRET",
    "webhook_verify_ssl": 1
  }'
```

```json
{
  "result": true,
  "changed_keys": [
    "tracking_event_webhook_url",
    "order_create_webhook_url",
    "order_created_webhook_all_types",
    "webhook_sign_secret",
    "webhook_verify_ssl"
  ],
  "recipient_type": "business",
  "settings": {
    "webhook_sign_secret": "************CRET",
    "tracking_event_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_create_webhook_url": "https://shop.northbound-outfitters.ca/hooks/superroute",
    "order_created_webhook_all_types": 1,
    "webhook_verify_ssl": 1
  }
}
```

- Menjaju se samo ključevi koje pošaljete; nepoznat ključ se odbija sa `400`.
- `changed_keys` navodi šta je sačuvano. Tajna se uvek vraća maskirano.

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

Svaki `tracking.event` nosi `order_id`, `tracking_event_status_id`, `tracking_event_key`, `tracking_number` i `external_tracking_number`; povežite ga sa svojom porudžbinom preko `order_id` (`id` iz koraka 7).

Proverite **v2** 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**.

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

`order_cancel_failed_webhook_url` (`order.cancel_failed`) se ne šalje za otkazivanja nalepnica u koraku 12; odbijeno otkazivanje nalepnice javlja se u odgovoru tog poziva.

**Verifikacija:** jedan test `submitOrder` proizvodi `order.created` sa `id` porudžbine, a prvo skeniranje prevoznika proizvodi `tracking.event`. Primalac mora da odbije nevažeći potpis sa `401`.

## 12. Otkazivanje

Nalepnica koja neće biti poslata otkazuje se kako je prevoznik ne bi naplatio; iznos se vraća na nalog. Otkazivanje je moguće samo dok ga prevoznik još dozvoljava (obično pre preuzimanja).

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/cancelShippingLabel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nb-10482-cancel" \
  -d '{"id": 128455}'
```

```json
{
  "result": true,
  "message": "Shipping Label cancelled successfully"
}
```

- Pošaljite tačno jedno od `id` (ID porudžbine) ili `tracking_number` (broj za praćenje u Superroute-u ili broj prevoznika). Slanje oba vraća `400`.
- `result: true`: prevoznik je prihvatio otkazivanje i iznos nalepnice je vraćen.

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

Prevoznik koji već ima paket odbija otkazivanje: odgovor je `400` sa `result: false` i porukom prevoznika. Porudžbina čija nalepnica nikada nije kupljena ne može se otkazati ovim pozivom.

**Verifikacija:** odgovor je `result: true`, a javno praćenje za taj broj vraća `404`. Ponovni pokušaj sa istim `Idempotency-Key` vraća sačuvani odgovor; novi zahtev za otkazivanje iste porudžbine vraća `400` `This order already cancelled`.

## 13. Slanje informacija o pošiljci i zatvaranje dana (samo ako ova metoda to zahteva)

Neki prevoznici zahtevaju da se pošiljke dana prenesu (manifest) pre preuzimanja. Korak 8 to javlja za svaku porudžbinu u `needSubmitShippingInformation`. Tokom dana prikupljajte ID-jeve tih porudžbina i pošaljite ih posle poslednje nalepnice.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/submitShippingInformation \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ids": [128455, 128461, 128470]}'
```

```json
{
  "result": true,
  "message": "Processed 3 orders. Success: 3, Failed: 0",
  "data": {
    "total_processed": 3,
    "success_count": 3,
    "failure_count": 0,
    "details": [
      { "order_id": 128455, "result": true, "message": "Successful" },
      { "order_id": 128461, "result": true, "message": "Successful" },
      { "order_id": 128470, "result": true, "message": "Successful" }
    ]
  }
}
```

- `details[]`: jedan red po porudžbini; porudžbine sa `result: false` pošaljite ponovo nakon što otklonite razlog naveden u `message`.
- `404` `No eligible orders found for shipping information submission`: nijedan od ID-jeva nema kupljenu nalepnicu za koju je slanje još potrebno.

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

Zatim zatvorite dan. Poziv nema telo i obuhvata sve porudžbine nalepnica pozivaoca.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/labelservice/endofday \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "success": 0,
  "failed": 0,
  "success_ids": [],
  "failed_ids": []
}
```

- `400` sa `There are orders need to submit shipping information`: za neke kupljene nalepnice slanje je još potrebno; pošaljite ih pomoću `submitShippingInformation` i pozovite ponovo.

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

Preskočite ovaj korak kada nijedna porudžbina tog dana nije javila `needSubmitShippingInformation: true`.

**Verifikacija:** `submitShippingInformation` javlja `failure_count: 0`, a `endofday` odgovara sa `200`. Ovo prvo pokrenite na test metodi.

## 14. Obrada grešaka

Greške usluge nalepnica nose `message`; `code` je prisutan samo tamo gde ga tabela navodi.

| Situacija | HTTP status | Kod | Šta integracija radi |
|---|---|---|---|
| Token nedostaje ili je istekao, ili API pristup nije uključen | `401` | — (`Unauthorized`) | Ponovo se prijavite; ako se greška ponavlja, zatražite od firme da uključi API pristup |
| `shipping_method` nedostaje ili nije dostupan pozivaocu | `400` | — | Ponovo učitajte listu metoda (korak 5) i koristite `id` iz nje |
| Metoda ne nudi `package_type` | `400` | — | Koristite ključ iz `package_type` iz koraka 5 |
| Neispravna adresa ili paket, ili prevoznik ne vraća tarifu | `400` | — (poruka prevoznika) | Prikažite poruku, ispravite podatke i ponovo zatražite ponudu |
| `auto_deduplication` je `1` i `ref` već postoji | `400` | — (`exist_order_ids`) | Koristite postojeću porudžbinu iz `exist_order_ids` umesto kreiranja nove |
| Isti `Idempotency-Key` sa drugačijim telom | `409` | `IDEMPOTENCY_CONFLICT` | Za drugačiji zahtev koristite novi ključ |
| Isti `Idempotency-Key` dok se prvi zahtev još izvršava | `409` | `IDEMPOTENCY_IN_PROGRESS` | Sačekajte `Retry-After` sekundi i pokušajte ponovo sa istim ključem i telom |
| Stanje kupca uvećano za kredit ne pokriva nalepnicu | `400` | `INSUFFICIENT_BALANCE` | Dopunite stanje prema detaljima `insufficient_balance` (`shortfall`, `add_funds_url`) i ponovo pozovite korak 8 |
| Nalepnica je kupljena, datoteka prevoznika još nije spremna | `400` pri prvoj kupovini, kasnije `200` | `shipment_label_not_ready` | Čekajte dok je `labelStatus` `pending`; ponovo pozovite korak 8 kada je `failed` |
| ID porudžbine ili broj ne pripada pozivaocu | `401` | — (`Not Auth`) | Proverite ID i `type`; koristite nalog koji je kreirao porudžbinu |
| Prevoznik je odbio otkazivanje ili je porudžbina već otkazana | `400` | — | Tretirajte nalepnicu kao poslatu (ili već otkazanu); ne pokušavajte ponovo |
| Broj za praćenje je nepoznat ili otkazan | `404` | — | Prestanite da prikazujete vremensku liniju za taj broj |
| `endofday` sa pošiljkama koje još nisu poslate | `400` | — | Pokrenite `submitShippingInformation` za te porudžbine, zatim pozovite ponovo |

## Lista provera

Koristite odredište koje kontrolišete i metodu koja može da se otkaže:

- [ ] Lista metoda nije prazna; sačuvali ste jedan `id`.
- [ ] Tarifa vraća cenu za tu metodu i odredište.
- [ ] Submit vraća `id` porudžbine i `rates[].rate_id`; isti `Idempotency-Key` ne kreira drugu porudžbinu.
- [ ] `getShippingDetail` sa izabranim `rate_id` vraća `mainTrackingNumber`; drugi poziv ne naplaćuje ponovo.
- [ ] PDF nalepnice se otvara i pokazuje broj za praćenje prevoznika.
- [ ] Javno praćenje pronalazi pošiljku po tom broju.
- [ ] Stiže `tracking.event` (i `order.created`, kada je uključen); v2 potpis se uspešno proverava.
- [ ] Prevoznik prihvata prekograničnu test pošiljku sa `items`.
- [ ] Otkazivanje uspeva, **ili** ste potvrdili da se ova metoda ne može otkazati posle rezervacije.
- [ ] Ako metoda zahteva zatvaranje dana, test pokretanje se završava bez greške.
