# Přepravní služby

API přepravních služeb umožňuje zákaznickému účtu rezervovat přepravní služby, které jeho logistický poskytovatel nastavil a přiřadil mu. Vlastní systém zákazníka načte služby, které může používat, načte pravidla jedné služby, ocení zásilku, vytvoří přepravní objednávku, zaplatí ji ze zůstatku účtu a sleduje zásilku až do doručení. Tato příručka je určena vývojářům, kteří připojují systém dovozce, obchodníka nebo velkoobchodu k logistickému poskytovateli, který ho obsluhuje.

## 1. Co můžete vytvořit

Všechny příklady v této příručce používají jeden scénář. **Harbourline Imports Inc.**, dovozce čaje v Torontu, má zákaznický účet u svého logistického poskytovatele. Poskytovatel nabízí službu `intl_express` (International Express) ze svého skladu Toronto Hub (id skladu `7`). Harbourline předá v Toronto Hub dva kartony vzorků čaje pro distributora v Seattlu pod svou nákupní objednávkou `HLI-PO-1058`.

- **Rezervace ze systému nákupních objednávek.** Když je nákupní objednávka uvolněna, systém Harbourline ocení zásilku ve službě `intl_express`, vytvoří přepravní objednávku s číslem nákupní objednávky jako referencí a zaplatí ji z předplaceného zůstatku účtu, aniž by kdokoli otevíral portál poskytovatele.
- **Kontrola ceny před závazkem.** Nákupčí v Harbourline vidí přepravné, příplatky, daň a celkovou částku za dva kartony ještě před rezervací zásilky a zásilka, kterou služba nedokáže ocenit, se zastaví dříve, než objednávka vznikne.
- **Stav zásilky v ERP.** Sledovací číslo každého kartonu se uloží k nákupní objednávce; webhooky přenášejí stav objednávky a časovou osu sledování do ERP a noční úloha provádí odsouhlasení se seznamem objednávek.
- **Kontrolované změny.** Nezaplacená rezervace se opraví přímo a rezervace, která již není potřeba, se zruší a zaplacená částka se vrátí do kreditu účtu.

## 2. Co tato příručka pokrývá

Tuto skupinu použijte, když je volající **zákazníkem** logistické firmy a rezervuje jednu z vlastních přepravních služeb firmy: firma nastavuje cenový plán, sklady, příplatky a balení a služby zákazníkovi přiřazuje. Zákazník vidí a rezervuje pouze služby, které jsou mu přiřazeny.

V těchto případech použijte jinou skupinu:

- Volající je samotná logistická firma (firemní/klientský účet) a rezervuje vyzvednutí a doručení v tentýž den nebo místní vyzvednutí a doručení vlastní flotilou: viz příručka **Vyzvednutí a doručení (vlastní flotila)**.
- Volající kupuje štítky dopravců (například UPS nebo FedEx) za sjednané sazby účtu: viz příručka **Štítky dopravce**.
- Zákazník skladuje zboží ve skladu poskytovatele a odesílá ho ze zásob: viz příručka **Skladování a výdej**.

**Uniorder: jedno API pro každou zásilku** (`/api/v1/uniorder/...`) je doporučený jediný vstupní bod pro nové integrace místního doručení a štítků dopravce. Uniorder nepokrývá přepravní služby: objednávky přepravních služeb se vytvářejí a spravují pouze přes endpointy `/api/v1/customer/shipping-orders/...` popsané zde.

## 3. Než začnete

- **Typ účtu.** **Zákaznický** účet logistické firmy s **oprávněním k API**, které zapnula firma. Firemní/klientský ani zaměstnanecký účet se nemůže přihlásit přes zákaznické přihlášení uvedené níže.
- **Přiřazení služeb.** Firma musí zákazníkovi přiřadit alespoň jednu aktivní přepravní službu. Zákazník bez přiřazené služby dostane prázdný seznam služeb.
- **Testovací data.** Dohodněte se s firmou na testovacím kódu služby, testovacím skladu a malém předplaceném zůstatku na testovacím účtu. Použijte referenci, například `HLI-PO-1058` nebo `DEV-SHIP-001`, aby se testovací objednávky daly snadno najít a zrušit.
- **Zacházení s tokenem.** API volejte pouze ze svého serveru. Heslo ani přístupový token nevkládejte do prohlížečů ani mobilních klientů. Token vyprší jeden týden po přihlášení (`expires_at`); přihlaste se znovu před jeho vypršením.
- **Zástupné hodnoty.** Nahraďte `YOUR_HOST` názvem hostitele logistické firmy a `ACCESS_TOKEN` tokenem vráceným v kroku přihlášení.
- **Chyby v JSON.** U každého požadavku posílejte `Accept: application/json`, aby chyby validace vracely JSON místo přesměrování.

## 4. Přihlášení jako zákazník

Přihlášení vymění e-mail a heslo zákazníka za token typu bearer. Každé další volání v této příručce tento token posílá.

**REST:** `POST /api/v1/user/customer/login` — [Příručka REST](/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`: posílejte ho u každého požadavku jako `Authorization: Bearer ACCESS_TOKEN`. GraphQL používá stejnou hlavičku na `POST /api/graphql`.
- `expires_at` / `expires_timestamp`: nové přihlášení naplánujte před tímto časem.

**Ověření:** odpověď obsahuje `result: true` a `access_token`. Požadavek bez tokenu vrátí `401`; přihlášení účtem, který není zákaznický nebo nemá oprávnění k API, také vrátí `401`.

## 5. Seznam služeb přiřazených zákazníkovi

Seznam služeb informuje integraci, které kódy služeb může rezervovat a zda každá služba přijímá předání ve skladu, vyzvednutí nebo obojí. Uložte `service_code`; používá ho každé další volání služby.

**REST:** `GET /api/v1/customer/shipping-orders/services` — [Příručka REST](/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`: parametr cesty každého dalšího volání služby.
- `offer_pickup` / `allow_warehouse_delivery`: povolené hodnoty `origin_type` (`pickup` / `warehouse`).
- `support_multi_package`: zda jedna objednávka může obsahovat více než jeden řádek balíků.
- Prázdné pole `services` znamená, že tomuto zákazníkovi není přiřazena žádná služba.

**GraphQL:** `customerShippingOrderServices` ([Příručka GraphQL](/api/graphql/documentation#/customer/customerShippingOrderServices))

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

**Ověření:** seznam obsahuje alespoň jednu službu a uložili jste její `service_code` (v této příručce: `intl_express`).

## 6. Načtení konfigurace služby

Konfigurace vrátí vše, co potřebuje formulář objednávky jedné služby: sklady, které přijímají předání, volitelné příplatky, katalog balení a spotřebního materiálu, jednotky a země, ze kterých služba může vyzvedávat a do kterých může doručovat. Před oceněním nebo vytvořením čehokoli podle ní ověřte data objednávky.

**REST:** `GET /api/v1/customer/shipping-orders/services/{serviceCode}/config` — [Příručka REST](/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`, které se posílá, když je `origin_type` `warehouse`. Id, které není v tomto seznamu, se při vytvoření odmítne.
- `service.weight_mode`: která pole balíku cenový plán vyžaduje: `0` skutečná hmotnost (hmotnost), `1` objemová hmotnost (délka, šířka a výška), `2` účtovaná hmotnost (obojí). `null` znamená, že služba se oceňuje ručně. Abyste splnili každý režim, pošlete hmotnost i všechny tři rozměry.
- `delivery_allowed_countries` / `pickup_allowed_countries`: zemi doručení nebo vyzvednutí mimo tyto seznamy odmítněte ještě před voláním odhadu.
- `surcharges[].id`, `packagings[].id`, `products[].id`: id pro volitelné příplatky, balení a nákup spotřebního materiálu.
- `weight_units` / `dimension_units`: jednotky balíků se posílají jako čísla. Posílejte `weight_unit: 2` (kg) a `dimension_unit: 2` (cm), jak to dělají všechny příklady v této příručce; obě hodnoty jsou zároveň výchozí, pokud se pole vynechají.

**GraphQL:** `customerShippingOrderServiceConfig` ([Příručka GraphQL](/api/graphql/documentation#/customer/customerShippingOrderServiceConfig))

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

**Ověření:** `result` je `true` a u předání ve skladu `warehouses` obsahuje sklad, který chcete použít. `403` znamená, že služba není přiřazena tomuto zákazníkovi; `404` znamená, že kód služby neexistuje nebo je neaktivní.

## 7. Odhad ceny

Odhad ocení zásilku podle cenového plánu služby, aniž by cokoli zapsal. Celkovou částku zobrazte nákupčímu a objednávku nevytvářejte, pokud odhad hlásí odmítnutí.

**REST:** `POST /api/v1/customer/shipping-orders/services/{serviceCode}/estimate-price` — [Příručka REST](/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` (zákazník předá zboží ve skladu; pošlete `warehouse_id`) nebo `pickup` (poskytovatel zboží vyzvedne; pošlete `pickup_postcode` a `pickup_country`). Použijte pouze hodnotu, kterou povoluje krok 5.
- `packages`: jeden řádek na skupinu stejných balíků; `quantity` řádek násobí.
- `total` a `currency`: částka k zobrazení. `total` je `null`, dokud není vypočten některý poplatek.
- `needs_manual_quote` / `has_items_needing_quote`: firma oceňuje objednávku ručně; objednávku lze vytvořit a zaplatí se poté, co firma stanoví cenu.
- `refused` / `refusal_message`: služba odmítá zásilky, které nedokáže ocenit. Objednávku nevytvářejte; místo toho zobrazte `refusal_message`.
- Volitelné vstupy: `surcharges`, `products` (mapa id produktu na množství, zohledňuje se pouze tehdy, když je `allow_purchase_supplies` true), `has_special_requirements`, `coupon_code`.

**Ověření:** `result` je `true`, `refused` je `false` a buď `total` má hodnotu, nebo `needs_manual_quote` je `true`.

## 8. Vytvoření přepravní objednávky

Volání vytvoření rezervuje zásilku ve službě. Integrace uloží vrácené `id` ke své nákupní objednávce; toto id používá každé další volání.

**REST:** `POST /api/v1/customer/shipping-orders/services/{serviceCode}/orders` — [Příručka REST](/api/documentation#/paths/v1-customer-shipping-orders-services-serviceCode--orders/post)

Pošlete `Idempotency-Key` odvozený od vašeho vlastního stabilního id (zde od čísla nákupní objednávky). Opakování se stejným klíčem a stejným tělem vrátí první odpověď s `"replayed": true` a hlavičkou `Idempotency-Replayed: true` a nevytvoří druhou objednávku.

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

- Klíč těla pro balíky je při vytvoření `package` (při odhadu je to `packages`). Každý řádek s `quantity` N se stane N balíky a každý balík dostane vlastní sledovací číslo.
- Povinná pole: `delivery_name`, `delivery_telephone`, `delivery_address_1`, `delivery_city`, `delivery_province`, `delivery_country`, `delivery_postcode`, `origin_type`, `package[].weight`; plus `warehouse_id` pro `warehouse` nebo `pickup_name`, `pickup_telephone`, `pickup_address_1`, `pickup_city`, `pickup_province`, `pickup_country`, `pickup_postcode` pro `pickup`.
- Volitelná pole: `reference` (ukládá se jako `reference_number` objednávky), `delivery_email`, `delivery_address_2`, `scheduled_date`, `time_window`, `note`, `special_requirements` (pole textových řádků, zohledňuje se pouze tehdy, když je služba povoluje), `products`, `surcharges`, `coupon_code`.
- `id`: uložte ho. `status` `0` znamená Čekající (čeká na platbu).
- `total_price`: částka, kterou zaúčtuje krok 9. Je `0`, dokud objednávka čeká na ruční cenovou nabídku.
- `tracking_number` na úrovni objednávky je `null`; sledovací čísla jsou u balíků a načítají se v kroku 10.
- Endpoint u nové objednávky odpoví HTTP `201`.

**Ověření:** odpověď obsahuje `result: true` a `id`. Zopakování téhož požadavku se stejným `Idempotency-Key` vrátí stejné `id` s `"replayed": true`.

## 9. Platba objednávky ze zůstatku účtu

Přepravní objednávky se platí v plné výši ze zůstatku zákaznického účtu. Zaplacená objednávka přejde ze stavu Čekající do stavu Potvrzená a poskytovatel ji začne vyřizovat.

Nejprve načtěte částku:

**REST:** `GET /api/v1/customer/shipping-orders/{id}/payment-info` — [Příručka REST](/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 }
    ]
  }
}
```

Poté zaplaťte:

**REST:** `POST /api/v1/customer/shipping-orders/{id}/pay` — [Příručka REST](/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`: pokud zůstatek nepokrývá `remaining_balance`, před platbou dobijte účet.
- `payment_type`: podporuje se pouze `remaining_balance`; vždy se zaúčtuje celá zbývající částka.
- `data.status` `1` znamená Potvrzená.

**Ověření:** volání platby vrátí `result: true` a `status` `1` a druhé volání `payment-info` vrátí `400`, protože objednávka je zcela zaplacena. Volání platby bez dostatečného zůstatku vrátí `422` a nic nezaúčtuje.

## 10. Načtení objednávky a sledování balíků

Volání detailu vrátí aktuální stav a sledovací číslo každého balíku. Sledovací čísla balíků uložte k nákupní objednávce; veřejné sledování přijímá každé z nich.

**REST:** `GET /api/v1/customer/shipping-orders/{id}` — [Příručka REST](/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` Čekající, `1` Potvrzená, `2` Na cestě, `3` Odeslaná, `4` Zrušená, `5` Neúspěšná, `6` Částečně vyzvednutá, `7` Vyzvednutá, `8` Zpracovává se.
- `can_edit` / `can_cancel`: zda je krok 12 aktuálně povolen.
- `packages[].tracking_number`: čísla k uložení a sledování.
- `shipping_code`: kód, který přijímají obrazovky předání ve skladu; vytiskněte ho na průvodní doklady k předání.

**GraphQL:** `customerShippingOrderShow` ([Příručka GraphQL](/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
    }
  }
}
```

Pro odsouhlasení všech objednávek jedné služby, například v noční úloze, je načtěte seznamem s filtrem. Filtr `id` odpovídá id objednávky, sledovacímu číslu nebo referenci.

**REST:** `GET /api/v1/customer/shipping-orders/services/{serviceCode}/orders` — [Příručka REST](/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` ([Příručka GraphQL](/api/graphql/documentation#/customer/customerShippingOrders))

Veřejné sledování nevyžaduje token a vrací časovou osu událostí jednoho balíku:

**REST:** `GET /api/v1/tracking/{trackingNumber}` — [Příručka REST](/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` ([Příručka GraphQL](/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
    }
  }
}
```

**Ověření:** detail vrátí objednávku tohoto zákazníka s jedním sledovacím číslem na balík a veřejné sledování vrátí `result: true` pro sledovací číslo balíku. Id objednávky jiného zákazníka vrátí `404`.

## 11. Příjem webhooků

Webhooky doručují vytvoření objednávky, změny stavu a události sledování na váš server, takže integrace nemusí opakovaně dotazovat. Zákaznický účet si nastavuje vlastní URL webhooků a podpisové tajemství.

Při vytvoření přepravní objednávky poskytovatel vytvoří také propojenou objednávku vyzvednutí pro svůj dispečink. Webhooky se posílají pro tuto propojenou objednávku: její `ref` je `Shipping-Pickup-{shipping order id}` (například `Shipping-Pickup-9001`) a každý její balík nese sledovací číslo přepravního balíku v `external_tracking_number`. Příchozí události párujte podle těchto dvou polí.

| Nastavení | Událost | Co integrace udělá |
|---|---|---|
| `order_create_webhook_url` | `order.created` | Propojí událost s přepravní objednávkou přes `ref` a `packages[].external_tracking_number` |
| `tracking_event_webhook_url` | `tracking.event` | Přidá událost do časové osy balíku |
| `order_status_change_webhook_url` | `order.status_change` | Aktualizuje stav zobrazený ve vašem systému |

**REST:** `PUT /api/v1/webhook-settings` — [Příručka REST](/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"
  }
}
```

- Mění se pouze odeslané klíče; prázdný řetězec URL vymaže. `webhook_sign_secret` musí mít 16 až 255 znaků a dokud je tajemství prázdné, neodešle se žádný webhook.
- `recipient_type` je u zákaznického účtu `customer`.

**GraphQL:** `webhookSettingsUpdate` ([Příručka GraphQL](/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"
  )
}
```

Ověřte podpis **v2** nad nezpracovaným tělem: `HMAC_SHA256(timestamp + "." + raw_body, secret)` proti `X-Webhook-Signature-V2`. Duplicity odstraňujte podle `X-Webhook-Event-Id`. Odpovězte **2xx do 3 sekund** a událost zpracujte až poté.

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

**Ověření:** po volání nastavení jedno testovací vytvoření vyvolá událost `order.created`, jejíž `ref` je `Shipping-Pickup-{id}` pro id nové přepravní objednávky, a kontrola podpisu projde.

## 12. Úprava nebo zrušení objednávky

Objednávku lze opravit, dokud je ve stavu Čekající (před platbou), a zrušit, dokud je ve stavu Čekající nebo Potvrzená. Zrušení zaplacené objednávky vrátí zaplacenou částku do kreditu účtu.

Pro úpravu pošlete znovu celou objednávku se stejnými poli jako v kroku 8. Cena se přepočítá.

**REST:** `PUT /api/v1/customer/shipping-orders/{id}` — [Příručka REST](/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
  }
}
```

Pro zrušení:

**REST:** `POST /api/v1/customer/shipping-orders/{id}/cancel` — [Příručka REST](/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` znamená Zrušená. Propojená objednávka vyzvednutí se odstraní.
- `refund_amount`: částka vrácená do kreditu účtu; `0` u nezaplacené objednávky.
- Před nabídnutím těchto akcí uživatelům načtěte `can_edit` a `can_cancel` z kroku 10.

**Ověření:** zrušení vrátí `status` `4` a detail ukazuje `status_name` `Cancelled`. Druhé zrušení nebo zrušení objednávky ve stavu Na cestě nebo pozdějším vrátí `403` se zprávou `This order can no longer be cancelled.`; úprava zaplacené objednávky vrátí `403`.

## 13. Zpracování chyb

| Situace | Stav HTTP | Kód | Co integrace udělá |
|---|---|---|---|
| Chybějící, prošlý nebo neplatný token; přihlášení nezákaznickým účtem nebo bez oprávnění k API | `401` | — | Přihlaste se znovu; pokud selže samotné přihlášení, požádejte firmu o kontrolu typu účtu a oprávnění k API |
| Služba není přiřazena tomuto zákazníkovi | `403` | — | Znovu načtěte seznam služeb (krok 5) a rezervujte pouze přiřazené služby |
| API se volá z relace aplikace platformy, jejíž aplikace má vypnuté přepravní objednávky | `403` | `APP_CAPABILITY_DISABLED` | Požádejte firmu o zapnutí přepravních objednávek pro aplikaci |
| Neznámý nebo neaktivní kód služby; id objednávky nebylo pro tohoto zákazníka nalezeno | `404` | — | Obnovte seznam služeb; zkontrolujte uložené id objednávky |
| Povinné pole chybí nebo je neplatné | `422` | — | Přečtěte `errors` v těle, opravte pole a odešlete znovu |
| Služba nenabízí daný typ původu nebo sklad není v seznamu služby | `422` | — | Použijte `origin_type` a `warehouse_id` z kroků 5 a 6 |
| Služba nedokáže zásilku ocenit a neoceněné zásilky odmítá | `422` | `unpriced_refused` | Nic nebylo vytvořeno; zobrazte `message` a neopakujte beze změny |
| Objednaný spotřební materiál není skladem | `422` | — | Přečtěte `stock_shortages`, snižte množství a odešlete znovu |
| Stejný `Idempotency-Key` s jiným tělem | `409` | `IDEMPOTENCY_CONFLICT` | Pro novou objednávku použijte nový klíč; klíč nikdy nepoužívejte znovu pro jiný obsah |
| Opakování, zatímco první požadavek s tímto klíčem ještě probíhá | `409` | `IDEMPOTENCY_IN_PROGRESS` | Počkejte počet sekund z `Retry-After` a poté opakujte se stejným klíčem a tělem |
| Platba bez dostatečného zůstatku | `422` | — | Dobijte účet a poté zaplaťte znovu |
| Informace o platbě nebo platba u zcela zaplacené objednávky | `400` | — | Objednávku považujte za zaplacenou; načtěte detail |
| Zrušení poté, co objednávka opustila stav Čekající nebo Potvrzená | `403` | — | Zobrazte, že objednávku již nelze zrušit; kontaktujte firmu |
| Úprava po platbě | `403` | — | Zrušte ji a vytvořte novou objednávku, nebo kontaktujte firmu |
| Chyba serveru během odhadu, vytvoření, platby nebo zrušení | `500` | — | Opakujte jednou; u vytvoření opakujte se stejným `Idempotency-Key` |

## Seznam testů

Použijte testovací referenci, například `DEV-SHIP-001` nebo `HLI-PO-1058`:

- [ ] Přihlášení zákazníka vrátí `access_token`; požadavek bez tokenu vrátí `401`.
- [ ] Seznam služeb není prázdný a uložili jste jeden `service_code`.
- [ ] Konfigurace vrátí sklady, jednotky a povolené země pro tuto službu a váš formulář je používá.
- [ ] Odhad vrátí `total` (nebo `needs_manual_quote: true`) a odmítnutá zásilka se nevytvoří.
- [ ] Vytvoření vrátí `id`; stejný `Idempotency-Key` se stejným tělem vrátí stejné `id` s `"replayed": true`.
- [ ] Platba uspěje a stav se změní na Potvrzená, nebo jste ověřili, že nedostatečný zůstatek vrátí `422` a nic nezaúčtuje.
- [ ] Detail ukazuje objednávku tohoto zákazníka s jedním sledovacím číslem na balík a veřejné sledování najde každý balík.
- [ ] Webhooky jsou nastaveny s podpisovým tajemstvím; testovací vytvoření vyvolá `order.created` s `ref` `Shipping-Pickup-{id}` a kontrola podpisu projde.
- [ ] Zrušení testovací objednávky vrátí `status` `4` a očekávanou `refund_amount`; druhé zrušení vrátí `403`.
