# Ponuda i porudžbina u jednom toku

Ovaj vodič prolazi kroz Uniorder API (`/api/v1/uniorder/...`) zahtev po zahtev, redosledom kojim se integracija gradi: prijava, ponuda, kreiranje po izabranom `rate_id`, štampa nalepnice, čitanje, praćenje i otkazivanje porudžbine, kao i paketna obrada pošiljki. Jedna ponuda navodi sve načine na koje nalog može da pošalje paket: dostavu koju obavlja sama firma i, na zahtev, svaku uslugu prevoznika za nalepnice. Poručivanje sa `rate_id` kreira porudžbinu za tu uslugu: porudžbinu dostave ili porudžbinu nalepnice sa nalepnicom kupljenom kod ponuđene usluge prevoznika. Namenjen je programerima onlajn prodavnica, sistema za upravljanje porudžbinama i ERP sistema koji šalju preko poslovnog naloga.

## 1. Šta možete da izgradite

Primeri u nastavku prate jednu firmu: **Fleurs du Plateau**, cvećaru na adresi 4500 Rue Saint-Denis, Montreal (H2J 2L3), koja prodaje bukete onlajn. Tipičan paket je jedna kutija od 1,2 kg, dimenzija 40 × 25 × 25 cm, za primaoca Jane Recipient na adresi 6841 Rue Saint-Denis, Montreal (H2S 2S3), po veb porudžbini `WEB-10045`.

- **Checkout koji nudi sve opcije slanja.** Prodavnica jednom traži ponudu za paket i prikazuje lokalnu dostavu istog dana pored svake usluge nalepnica prevoznika na nalogu, svaku sa njenom cenom, a zatim kreira porudžbinu po opciji koju je kupac izabrao.
- **Automatska štampa nalepnica.** Kada je porudžbina kreirana, prodavnica preuzima PDF nalepnice i šalje ga štampaču na stanici za pakovanje, bez obzira na to da li paket dostavlja firma ili prevoznik.
- **Stranica porudžbine sa praćenjem u realnom vremenu.** Stranica porudžbine kupca prikazuje status i vremensku liniju događaja pošiljke, sa dokazom o dostavi kada je buket dostavljen.
- **Noćni paket iz ERP-a.** Za veleprodajne porudžbine dana dobija se ponuda i one se kreiraju u jednom poslu u redu čekanja sa do 500 redova, a svaki rezultat se povezuje sa svojom stavkom porudžbine preko `reference`.

## 2. Šta ovaj vodič obuhvata

Ovo je vodič korak po korak za Uniorder API. Pregled onoga što Uniorder nudi, i zašto, nalazi se u **Uniorder: jedan API za svaku pošiljku**; ovaj vodič daje zahteve, odgovore i provere za svaki poziv.

Uniorder je preporučena jedinstvena ulazna tačka za nove integracije koje šalju pakete lokalnom dostavom ili nalepnicom prevoznika: zamenjuje zasebne pozive API-ju lokalne dostave i API-ju nalepnica prevoznika jednim oblikom zahteva. Raniji endpoint-i opisani u **Preuzimanje i dostava (sopstvena flota)** i **Nalepnice prevoznika** ostaju dostupni i nepromenjeni. Uniorder se ne primenjuje na usluge slanja koje rezerviše nalog kupca niti na porudžbine skladištenja i izlaza; za njih koristite **Usluge slanja** i **Skladištenje i izlaz**.

## 3. Pre nego što počnete

- **Nalog.** Koristite poslovni (klijentski) nalog ili nalog zaposlenog u firmi, sa API dozvolom. Nalog kupca firme takođe može da poziva Uniorder i uvek dobija ponudu i naplatu u svoje ime. Kreiranje porudžbine dostave zahteva dozvolu za postavljanje porudžbina.
- **Kupci.** Klijentski nalog ili nalog zaposlenog može da traži ponudu i da poručuje za jednog od svojih kupaca pomoću `customer_id` ili `customer_code` u ponudi; `rate_id` tada nosi tog kupca, a cena prati plan kupca.
- **Usluge nalepnica.** Da bi dobio tarife `label_service`, nalog (ili navedeni kupac) mora imati podešen najmanje jedan nalog prevoznika za nalepnice.
- **Test podaci.** Za tarife `self_delivery` koristite adresu unutar područja dostave firme, kao i test reference kao što je `WEB-10045` koje se posle mogu otkazati.
- **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.

## 4. Prijava

Svaki Uniorder poziv izvršava se u ime nekog 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":"orders@fleursduplateau.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 svaku uslugu

Ponuda navodi sve načine na koje paket može da se pošalje, sa cenom i `rate_id` za svaki. Checkout prikazuje tarife kao opcije; ništa se ne kreira niti rezerviše.

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

Pošiljalac i primalac su potpune adrese; opcioni su samo `from_address_2` i `to_address_2`. Svaki paket zahteva `weight`, `length`, `width` i `height`. Postavite `quote_labels` na `true` da biste dodali usluge prevoznika za nalepnice; imena i telefoni obe strane su tada obavezni. Klijentski nalog ili nalog zaposlenog može da traži ponudu za jednog od svojih kupaca pomoću `customer_id` ili `customer_code`. Vremenski prozor dostave (`time_window_start`, `time_window_end`, format `YYYY-MM-DD HH:MM:SS`) uzima se u obzir kada cena zavisi od njega.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "quote_labels": true,
    "packages": [{
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

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

```json
{
  "result": true,
  "rates": [
    {
      "rate_id": "eyJpdiI6Ik1rT2Z...",
      "type": "self_delivery",
      "channel_id": null,
      "shipping_price": "14.60",
      "currency": "CAD",
      "price_details": { "shipping_fee": 12.92, "sub_total": "12.92" },
      "warning": null
    },
    {
      "rate_id": "eyJpdiI6IlpxR0...",
      "type": "label_service",
      "shipping_price": "18.40",
      "currency": "CAD",
      "shipping_method_id": 72,
      "shipping_method_name": "UPS",
      "carrier_name": "ups",
      "service_code": "ups_standard",
      "service_name": "UPS STANDARD",
      "transit_days": 3
    }
  ],
  "errors": []
}
```

- `type` `self_delivery`: dostava koju obavlja firma. Najviše jedna po ponudi.
- `type` `label_service`: jedna po usluzi svakog naloga za nalepnice. Prikažite kupcu `service_name`, `shipping_price` i `transit_days`.
- `errors` navodi šta nije moglo da dobije ponudu, sa njegovim `type`. Adresa van područja dostave je greška tipa `self_delivery` sa kodom `OUT_OF_DELIVERY_AREA`; prikažite samo usluge nalepnica.
- `rate_id` važi 30 minuta i samo za nalog koji je zatražio ponudu. Čuvajte ga uz sesiju checkout-a.
- `result` je `true` kada je pronađena najmanje jedna tarifa.

**GraphQL:** `uniorderRate` ([GraphQL priručnik](/api/graphql/documentation#/orders/uniorderRate)). Odgovor je JSON skalar, pa operacija nema selection set.

```graphql
mutation QuoteBouquet($packages: [Json]!) {
  uniorderRate(
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    quote_labels: true
    packages: $packages
  )
}
```

Promenljive:

```json
{ "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**Verifikacija:** `rates` sadrži tarifu `self_delivery` za adresu u području i, uz `quote_labels`, jednu tarifu `label_service` po usluzi prevoznika. Ništa se ne kreira.

## 6. Kreiranje porudžbine po izabranoj tarifi

Kada kupac plati, prodavnica kreira porudžbinu sa `rate_id` izabrane opcije i istom pošiljkom. `rate_id` određuje uslugu; ništa drugo u zahtevu je ne bira.

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

Uz svako kreiranje pošaljite zaglavlje `Idempotency-Key`, jedinstveno za svaku porudžbinu. Ponovni pokušaj sa istim ključem i istim telom vraća prvi odgovor sa `replayed` `true` i ne kreira drugu porudžbinu.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: checkout-10045" \
  -d '{
    "rate_id": "eyJpdiI6Ik1rT2Z...",
    "ref": "WEB-10045",
    "type": "D",
    "from_name": "Fleurs du Plateau",
    "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis",
    "from_city": "Montreal",
    "from_province": "QC",
    "from_country": "CA",
    "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient",
    "to_telephone": "5145550199",
    "to_email": "jane@example.com",
    "to_address": "6841 Rue Saint-Denis",
    "to_address_2": "Apt 2",
    "to_city": "Montreal",
    "to_province": "QC",
    "to_country": "CA",
    "to_postcode": "H2S2S3",
    "time_window_start": "2026-10-02 13:00:00",
    "time_window_end": "2026-10-02 17:00:00",
    "delivery_instruction": "Ring the bell at the side door.",
    "packages": [{
      "ref": "WEB-10045-1",
      "weight": 1.2,
      "weight_unit": 2,
      "length": 40,
      "width": 25,
      "height": 25,
      "dimension_unit": 2
    }]
  }'
```

Tarifa `self_delivery` kreira porudžbinu dostave. Za `type` `D` primalac je stanica; postavite `need_pick_up` na `1` da bi paket bio preuzet kod pošiljaoca. Za `type` `P` pošiljalac je stanica.

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800001"],
  "shipping_price": "14.60",
  "quoted_price": "14.60",
  "currency": "CAD"
}
```

Tarifa `label_service` kreira porudžbinu nalepnice i kupuje nalepnicu kod ponuđene usluge prevoznika. `type` mora biti `D`, a `from_name`, `from_telephone`, `to_name` i `to_telephone` su obavezni. Da je kupac izabrao UPS STANDARD, odgovor bi bio:

```json
{
  "result": true,
  "type": "label_service",
  "id": 123457,
  "ref": "WEB-10045",
  "tracking_numbers": ["SR26092800002"],
  "shipping_price": "18.40",
  "quoted_price": "18.40",
  "currency": "CAD",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456784",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `id`: sačuvajte ga uz veb porudžbinu; svaki kasniji poziv ga koristi.
- `tracking_numbers`: sopstveni brojevi za praćenje pošiljke, jedan po paketu.
- `shipping_price`: naplaćena cena. Cena porudžbine se određuje pri kreiranju; `quoted_price` je cena iz ponude. Ove dve cene mogu da se razlikuju.
- `label.main_tracking_number` i `label.shipping_label` (samo porudžbina nalepnice): broj za praćenje prevoznika i PDF nalepnice u base64.
- `result` `false` sa kodom `LABEL_PURCHASE_FAILED` (samo porudžbina nalepnice): porudžbina postoji, ali nema nalepnicu. Sačuvajte `id` i nastavite sa korakom 11.

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

```graphql
mutation CreateBouquetOrder($packages: [Json]!) {
  uniorderCreate(
    rate_id: "eyJpdiI6Ik1rT2Z..."
    ref: "WEB-10045"
    type: "D"
    from_name: "Fleurs du Plateau"
    from_telephone: "5145550100"
    from_address: "4500 Rue Saint-Denis"
    from_city: "Montreal"
    from_province: "QC"
    from_country: "CA"
    from_postcode: "H2J2L3"
    to_name: "Jane Recipient"
    to_telephone: "5145550199"
    to_address: "6841 Rue Saint-Denis"
    to_city: "Montreal"
    to_province: "QC"
    to_country: "CA"
    to_postcode: "H2S2S3"
    packages: $packages
  )
}
```

Promenljive:

```json
{ "packages": [{ "ref": "WEB-10045-1", "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }] }
```

**Verifikacija:** `result` je `true` i `id` je postavljen. `rate_id` koji je istekao ili pripada drugom nalogu vraća `400` sa kodom `RATE_ID_INVALID`, i ništa se ne kreira.

## 7. Štampa nalepnice

Stanica za pakovanje štampa nalepnicu čim porudžbina postoji. Isti poziv vraća sopstvenu nalepnicu firme za porudžbinu dostave i kupljenu nalepnicu prevoznika za porudžbinu nalepnice.

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

```bash
curl "https://YOUR_HOST/api/v1/uniorder/123456/label?hide_sender_address=0" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "format": "pdf",
  "tracking_numbers": ["SR26092800001"],
  "pdf_data": "JVBERi0xLjQK..."
}
```

- `pdf_data`: PDF nalepnice u base64. Dekodirajte ga i pošaljite datoteku štampaču.
- `hide_sender_address`, `hide_receiver_address` (`1` za skrivanje): primenjuju se na sopstvenu nalepnicu firme za porudžbinu dostave.
- `label_status` (porudžbina nalepnice): `ready` kada je datoteka vraćena. Kada prevoznik još nije napravio datoteku, odgovor je `200` sa `result` `false` i `label_status` `pending`; zatražite nalepnicu ponovo kasnije.
- Ovaj poziv nikada ne kupuje nalepnicu: nalepnica koja nije kupljena vraća `409` sa kodom `LABEL_PURCHASE_FAILED`. Kupite je u koraku 11.

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

```graphql
query {
  uniorderLabel(order_id: 123456, hide_sender_address: 0)
}
```

**Verifikacija:** `result` je `true`, a dekodirani `pdf_data` se otvara kao PDF koji pokazuje broj za praćenje porudžbine.

## 8. Čitanje porudžbine

Prodavnica čita porudžbinu da bi prikazala njen status, adrese i pakete na stranici porudžbine ili na ekranu korisničke podrške.

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

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "ref": "WEB-10045",
  "order_type": "D",
  "status": "pending",
  "created_at": "2026-10-02 09:14:05",
  "time_window_start": "2026-10-02 13:00:00",
  "time_window_end": "2026-10-02 17:00:00",
  "from": { "name": "Fleurs du Plateau", "address": "4500 Rue Saint-Denis", "city": "Montreal", "postcode": "H2J2L3" },
  "to": { "name": "Jane Recipient", "address": "6841 Rue Saint-Denis", "address_2": "Apt 2", "city": "Montreal", "postcode": "H2S2S3" },
  "packages": [
    { "id": 998877, "ref": "WEB-10045-1", "tracking_number": "SR26092800001", "weight": 1.2 }
  ],
  "shipping_price": "14.60",
  "currency": "CAD"
}
```

- `type`: `self_delivery` ili `label_service`; ostala polja imaju isti oblik za oba.
- `status`: `pending`, `in_transit`, `out_for_pickup`, `out_for_delivery`, `ready_for_self_pickup`, `delivered`, `exception` ili `cancelled` za porudžbinu dostave, i `label_pending`, `label_purchased` ili `cancelled` za porudžbinu nalepnice.
- `label` (samo porudžbina nalepnice): prevoznik, usluga, `carrier_tracking_numbers` i `label_status` (`not_purchased`, `pending`, `ready` ili `failed`).

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

```graphql
query {
  uniorder(order_id: 123456)
}
```

**Verifikacija:** porudžbina vraća svoj `status` i `packages`, a `ref` se poklapa sa veb porudžbinom.

## 9. Praćenje porudžbine

Stranica porudžbine prikazuje vremensku liniju pošiljke. Čitajte je kada kupac otvori stranicu ili je održavajte ažurnom preko webhook-ova.

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

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456/tracking \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "status": "delivered",
  "tracking_numbers": ["SR26092800001"],
  "events": [
    { "code": "delivered", "description": "Delivered", "location": "Montreal", "time": "2026-10-02 15:42:10", "time_zone": "America/Toronto", "source": "shipper" }
  ],
  "proofs": [
    { "type": "photo", "url": "https://YOUR_HOST/storage/pod/123456.jpg", "uploaded_at": "2026-10-02 15:42:08" }
  ]
}
```

- `events`: vremenska linija, od najnovijeg, svaki sa `code`, `description`, `location` i vremenom.
- `proofs`: datoteke dokaza o dostavi. Prikažite ih kada `status` postane `delivered`.
- `carrier` (samo porudžbina nalepnice): naziv prevoznika, broj za praćenje i link za praćenje (`tracking_url`).

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

```graphql
query {
  uniorderTracking(order_id: 123456)
}
```

**Verifikacija:** poziv praćenja vraća `result` `true`, `status` porudžbine i njene `events`.

## 10. Otkazivanje porudžbine

Kada kupac otkaže veb porudžbinu, prodavnica otkazuje pošiljku istim pozivom za porudžbinu dostave i za porudžbinu nalepnice. Nalepnica se prvo poništava kod svog prevoznika.

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

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123456/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cancel-10045" \
  -d '{}'
```

```json
{
  "result": true,
  "type": "self_delivery",
  "id": 123456,
  "already_cancelled": false,
  "message": "The order has been cancelled."
}
```

- `already_cancelled`: `true` kada je porudžbina otkazana pre ovog poziva. Tretirajte to kao uspeh.
- Kada porudžbina nije otkazana, odgovor je `409` i porudžbina je nepromenjena: `ORDER_STATUS_NOT_CANCELLABLE` (prekasno za otkazivanje), `ORDER_CANCEL_REFUSED` (trenutno ne može da se otkaže) ili `LABEL_CANCEL_FAILED` (prevoznik nije poništio nalepnicu). Ostavite veb porudžbinu otvorenom i pošiljku obradite ručno.

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

```graphql
mutation {
  uniorderCancel(order_id: 123456)
}
```

**Verifikacija:** `result` je `true`. Ponovno otkazivanje iste porudžbine vraća `already_cancelled` `true`.

## 11. Kasnija kupovina nalepnice (samo posle LABEL_PURCHASE_FAILED)

Ovaj korak se primenjuje samo na porudžbinu nalepnice čije je kreiranje odgovorilo sa `LABEL_PURCHASE_FAILED`. Odgovor je bio `200` sa `result` `false`, kodom `LABEL_PURCHASE_FAILED` i `id` porudžbine: porudžbina je zadržana bez nalepnice. Ne šaljite porudžbinu ponovo; kupite nalepnicu za tu porudžbinu.

**REST:** `POST /api/v1/uniorder/{orderId}/label` — [REST priručnik](/api/documentation#/paths/v1-uniorder-orderId--label/post)

Kreiranje koje nije uspelo da kupi nalepnicu odgovorilo je:

```json
{
  "result": false,
  "code": "LABEL_PURCHASE_FAILED",
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "tracking_numbers": ["SR26092800003"],
  "quoted_price": "18.40",
  "message": "The quoted service is not offered for this shipment."
}
```

Kupite nalepnicu za porudžbinu `123458`:

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/123458/label \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: label-10046" \
  -d '{}'
```

Nalepnica se kupuje kod usluge izabrane pri kreiranju porudžbine. Da biste kupili kod druge usluge istog naloga, pošaljite u telu novi `label_service` `rate_id` iz koraka 5 (`{"rate_id": "eyJpdiI6IlpxR0..."}`). Nalepnica koja je već kupljena vraća se i ne kupuje se ponovo.

```json
{
  "result": true,
  "type": "label_service",
  "id": 123458,
  "ref": "WEB-10046",
  "shipping_price": "18.40",
  "label": {
    "carrier_name": "ups",
    "service_code": "ups_standard",
    "main_tracking_number": "1Z999AA10123456791",
    "label_status": "ready",
    "shipping_label": "JVBERi0xLjQK..."
  }
}
```

- `label.shipping_label`: PDF nalepnice u base64; odštampajte ga kao u koraku 7.
- `result` `false` ponovo sa `LABEL_PURCHASE_FAILED`: prevoznik je i dalje odbio. Pokušajte ponovo kasnije ili kupite kod druge usluge sa novim `rate_id`.

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

```graphql
mutation {
  uniorderPurchaseLabel(order_id: 123458)
}
```

**Verifikacija:** `result` je `true` i `label.shipping_label` sadrži PDF, ili je `label.label_status` `pending` dok prevoznik pravi datoteku.

## 12. Paketna obrada

Paketna obrada daje ponudu ili kreira mnogo pošiljki u jednom pozivu, na primer veleprodajne porudžbine iz ERP-a. Svaki red prolazi kroz pojedinačni poziv i vraća ono što bi taj poziv vratio; red koji ne uspe ne zaustavlja ostale redove.

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

Do 20 redova po pozivu, sa odgovorom u istom odgovoru: `shipments` za paketnu ponudu, `orders` za paketno kreiranje. Svaki red ima ista polja kao pojedinačni poziv, plus opcioni `reference` koji se vraća uz njegov rezultat.

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/batch \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: erp-2026-10-01" \
  -d '{
    "orders": [
      {
        "reference": "ERP-7781",
        "rate_id": "eyJpdiI6Ik1rT2Z...",
        "ref": "ERP-7781",
        "from_name": "Fleurs du Plateau",
        "from_telephone": "5145550100",
        "from_address": "4500 Rue Saint-Denis",
        "from_city": "Montreal",
        "from_province": "QC",
        "from_country": "CA",
        "from_postcode": "H2J2L3",
        "to_name": "Jane Recipient",
        "to_telephone": "5145550199",
        "to_address": "6841 Rue Saint-Denis",
        "to_city": "Montreal",
        "to_province": "QC",
        "to_country": "CA",
        "to_postcode": "H2S2S3",
        "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }]
      }
    ]
  }'
```

```json
{
  "result": true,
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `results`: jedan po redu, sa `index` reda, njegovim `reference` i `status` i `body` koje bi vratio pojedinačni poziv. Povežite svaki rezultat sa njegovom stavkom porudžbine preko `reference`.

**REST:** `POST /api/v1/uniorder/rate/batch-async` — [REST priručnik](/api/documentation#/paths/v1-uniorder-rate-batch-async/post) · `POST /api/v1/uniorder/batch-async` — [REST priručnik](/api/documentation#/paths/v1-uniorder-batch-async/post) · `GET /api/v1/uniorder/jobs/{jobId}` — [REST priručnik](/api/documentation#/paths/v1-uniorder-jobs-jobId/get)

Do 500 redova, stavljenih u red čekanja kao jedan posao. Poziv vraća `job_id`; čitajte posao dok `status` ne postane `done`, zatim pročitajte `results`. Isti paket poslat ponovo dok je prvi još u redu čekanja vraća prvi posao sa `duplicate` `true`.

```bash
curl https://YOUR_HOST/api/v1/uniorder/jobs/8813 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

```json
{
  "result": true,
  "job_id": 8813,
  "kind": "create",
  "status": "done",
  "count": 1,
  "results": [
    { "index": 0, "reference": "ERP-7781", "status": 200, "body": { "result": true, "type": "self_delivery", "id": 123460 } }
  ]
}
```

- `status`: `queued`, `done` ili `failed` sa `message` kada posao nije mogao da se obradi.
- Posao se izvršava jednom i ne ponavlja se. `rate_id` koji istekne pre nego što se njegov red izvrši vraća `RATE_ID_INVALID` za taj red; pošaljite posao kreiranja ubrzo nakon što se posao ponude završi.

**GraphQL:** `uniorderRateBatch` ([GraphQL priručnik](/api/graphql/documentation#/orders/uniorderRateBatch)) · `uniorderCreateBatch` ([GraphQL priručnik](/api/graphql/documentation#/orders/uniorderCreateBatch)) · `uniorderRateBatchAsync` ([GraphQL priručnik](/api/graphql/documentation#/orders/uniorderRateBatchAsync)) · `uniorderCreateBatchAsync` ([GraphQL priručnik](/api/graphql/documentation#/orders/uniorderCreateBatchAsync)) · `uniorderJob` ([GraphQL priručnik](/api/graphql/documentation#/orders/uniorderJob))

```graphql
query {
  uniorderJob(job_id: 8813)
}
```

**Verifikacija:** paket vraća jedan rezultat po redu; asinhroni posao dostiže `status` `done`.

## 13. Obrada grešaka

| Situacija | HTTP status | Kod | Šta integracija radi |
|---|---|---|---|
| Obavezno polje nedostaje ili je neispravno | 400 | `VALIDATION_FAILED` | Ispravite polje navedeno u `message` i ponovo pošaljite zahtev. |
| Primalac je van područja dostave (ponuda) | 200 | `OUT_OF_DELIVERY_AREA` u `errors` | Ponudite samo tarife `label_service`. |
| `rate_id` je istekao, neispravan je ili pripada drugom nalogu | 400 | `RATE_ID_INVALID` | Zatražite novu ponudu i kreirajte sa njenim `rate_id`. Ništa nije kreirano. |
| Porudžbina nalepnice je kreirana, ali njena nalepnica nije kupljena | 200 (`result` `false`) | `LABEL_PURCHASE_FAILED` | Sačuvajte `id`; kupite nalepnicu pomoću `POST /api/v1/uniorder/{orderId}/label`. Nikada ne kreirajte porudžbinu ponovo. |
| Nalepnica je zatražena pre nego što je kupljena | 409 | `LABEL_PURCHASE_FAILED` | Kupite nalepnicu pomoću `POST /api/v1/uniorder/{orderId}/label`. |
| Porudžbina je previše odmakla da bi se otkazala | 409 | `ORDER_STATUS_NOT_CANCELLABLE` | Ostavite porudžbinu nepromenjenom; povraćaj obradite zasebno. |
| Porudžbina trenutno ne može da se otkaže | 409 | `ORDER_CANCEL_REFUSED` | Ostavite porudžbinu nepromenjenom; pokušajte ponovo kasnije ili se obratite firmi. |
| Prevoznik nije poništio nalepnicu | 409 | `LABEL_CANCEL_FAILED` | Porudžbina je nepromenjena; pokušajte otkazivanje ponovo kasnije. |
| Porudžbina ili posao ne postoji ili pripada drugom nalogu | 404 | `ORDER_NOT_FOUND` | Proverite `id` sačuvan uz veb porudžbinu. |
| `Idempotency-Key` je ponovo upotrebljen sa drugačijim telom | 409 | `IDEMPOTENCY_CONFLICT` | Za drugačiji zahtev koristite novi ključ. |
| Token nedostaje ili je istekao, ili nalog ne sme da postavlja porudžbine | 401 | — | Ponovo se prijavite; proverite dozvole naloga. |

## Lista provera

Koristite test `ref` kao što je `WEB-10045`:

- [ ] Ponuda vraća tarifu `self_delivery` za adresu u području.
- [ ] Uz `quote_labels`, ponuda vraća tarife `label_service`, svaku sa `rate_id`.
- [ ] Poručivanje sa `self_delivery` `rate_id` vraća `id` i `tracking_numbers`.
- [ ] Poručivanje sa `label_service` `rate_id` vraća nalepnicu ponuđene usluge.
- [ ] Isti `Idempotency-Key` ne kreira drugu porudžbinu.
- [ ] `rate_id` stariji od 30 minuta vraća `RATE_ID_INVALID`.
- [ ] Nalepnica svake porudžbine dekodira se u PDF spreman za štampu.
- [ ] Porudžbina, njena nalepnica i njeno praćenje mogu se pročitati sa `id` iz kreiranja.
- [ ] Otkazivanje test porudžbine vraća `result: true`; ponovno otkazivanje vraća `already_cancelled: true`.
- [ ] Posle `LABEL_PURCHASE_FAILED`, `POST /api/v1/uniorder/{orderId}/label` kupuje nalepnicu za istu porudžbinu.
- [ ] Paket od dva reda vraća dva rezultata sa njihovim `reference`.
