# API doplňku Sestav si vlastní balíček: dokumentace pro vývojáře

> Dvě HTTP volání, žádné API klíče a UI kompletně ve vaší režii. Přehled rozhraní doplňku pro balíčky na míru včetně minimální implementace.

- **Zdroj:** https://www.webotvurci.cz/navody/sestav-si-vlastni-balicek/api-doplnku-sestav-si-vlastni-balicek-pro-vyvojare/
- **Doplněk / kategorie:** Sestav si vlastní balíček (https://www.webotvurci.cz/navody/sestav-si-vlastni-balicek/)
- **Datum:** 2026-08-12
- **Autor:** Webotvůrci (https://www.webotvurci.cz/)

---

Doplněk **Sestav si vlastní balíček** je headless. Naše strana drží konfigurace balíčků, synchronizuje produktová data ze Shoptetu, počítá závaznou cenu a zakládá sady. Jak bude konfigurátor vypadat a chovat se, je čistě na vás.

Není to ideologie, ale praktické rozhodnutí. Agentura, která spravuje klientův e-shop, tak nemusí do jeho šablony pouštět cizí kód a widget si vyladí přesně podle designu. E-shop s vlastním vývojářem si zase může postavit UI, které sedne jeho sortimentu — něco jiného potřebuje prodejce kávy a něco jiného dárkové koše. My dodáme data a závaznou cenu, vy zbytek.

Tenhle text je průvodce rozhraním, ne jeho úplná specifikace. Kompletní referenci pole po poli najdete [na GitHubu](https://github.com/Webotvurci-s-r-o/shoptet-premium-multipack/blob/main/docs/03-api-reference.md).

## Dvě volání, žádné klíče

Celé frontendové API tvoří dva endpointy:

| Metoda | Účel |
|---|---|
| `GET …/multipack/configuration-products/{shortcode}[/{priceListId}]` | načtení konfigurace balíčku a nabídky produktů |
| `POST …/multipack/product-set` | odeslání výběru zákazníka a založení sady |

Základ adresy je `https://shoptet.webotvurci.cz/eshop-endpoints/{projectId}/multipack/`, kde `projectId` je ID e-shopu ze Shoptet dataLayeru. E-shop se identifikuje výhradně tímhle číslem — **žádný API klíč, token ani podpis se neposílá** a CORS je otevřený, takže voláte přímo z prohlížeče včetně vývojového `localhost`. Proxy přes vlastní backend stavět nemusíte.

Konfiguraci identifikuje **shortcode** ve tvaru `w-multipack-ABC12` (prefix plus pět znaků, celkem 17). V administraci se zobrazuje v hranatých závorkách, protože v té podobě ho obchodník vkládá do stránky — **do API se posílá bez nich**.

Doporučujeme volat s `credentials: 'omit'`. Session doplněk nepotřebuje a ušetříte si starosti s cookies třetích stran a `SameSite`.

## Načtení konfigurace

První volání proběhne jednou při inicializaci widgetu a vrátí **všechno naráz**: parametry balíčku i kompletní nabídku produktů. Skládání balíčku pak běží čistě v prohlížeči, bez dalších požadavků.

```bash
curl -H 'Accept: application/json' \
  'https://shoptet.webotvurci.cz/eshop-endpoints/123456/multipack/configuration-products/w-multipack-ABC12'
```

Poslední segment s `priceListId` připojte **jen tehdy**, když má přihlášený zákazník přiřazenou cenovou hladinu (`shoptet.customer.priceListId` v dataLayeru je číslo). Ceny v odpovědi pak odpovídají jeho ceníku. Když hladinu nemá, segment vynechte — nikdy do adresy nedávejte literál `null`.

Na kořenové úrovni odpovědi (zabalené v `data`) najdete čtyři hodnoty:

- **`product_count`** (2–5) — kolik produktů musí zákazník vybrat. Je to **zdroj pravdy pro počet slotů**, neodvozujte ho odjinud.
- **`discount`** (1–100) — sleva na celý balíček v procentech.
- **`list_mode`** — `same_lists`, nebo `separate_lists`.
- **`categories`** — pole kategorií s produkty.

Režim seznamů určuje render. U `same_lists` vykreslíte `product_count` slotů a do každého dáte všechny kategorie i produkty. U `separate_lists` seskupíte kategorie podle jejich `column_number` (1 až `product_count`) a slot *N* naplníte jen kategoriemi s odpovídajícím číslem.

Každý produkt v kategorii nese to, co konfigurátor potřebuje k vykreslení:

```json
{
  "guid": "a1b2c3d4-0001-4000-9000-000000000001",
  "name": "Zrnková káva Brazílie 250 g",
  "url": "https://www.example.cz/zrnkova-kava-brazilie-250-g/",
  "image": "1021-1_zrnkova-kava-brazilie-250-g.jpg?ff=1&x=1024&y=768&q=85&ts=6b0c1d2e&sg=1a2b3c4d",
  "stock": "42.000",
  "rating": 4.6,
  "ratingCount": 18,
  "price_CZK": 289,
  "price_EUR": 11.56
}
```

Čtyři detaily, na kterých se dá snadno naběhnout:

- **Cena má dynamický klíč.** Pro každou měnu e-shopu vzniká jeden — `price_CZK`, `price_EUR` a tak dál. Nehardcodujte `price_CZK`, sahejte na `product['price_' + ctx.currency]`.
- **`stock` je řetězec** s desetinnou částí (`"42.000"`), ne číslo. Před porovnáním použijte `parseFloat`.
- **`image` není URL**, ale jen název souboru včetně podepsaného query stringu. Ten se nesmí odstraňovat ani znovu enkódovat. Výslednou adresu složíte jako `https://cdn.myshoptet.com/usr/{hostname}/user/shop/orig/{image}`, kde hostname berete z pole `url` produktu (`new URL(product.url).hostname`). Počítejte s tím, že `image` i `url` mohou být `null` — mějte připravený placeholder.
- **Pořadí kategorií i produktů si seřaďte sami.** Pokud na něm v UI záleží (a zpravidla ano), setřiďte je podle `column_number` a názvu.

Hodnota `rating` je průměr 0–5, kde nula znamená „zatím nehodnoceno" (spolu s `ratingCount: 0`) — nezobrazujte ji jako nejhorší možné hodnocení.

## Založení sady

Druhé volání odešlete až v okamžiku, kdy zákazník výběr skutečně potvrdí. Tělo je minimalistické:

```json
{
  "shortcode": "w-multipack-ABC12",
  "guids": [
    "a1b2c3d4-0001-4000-9000-000000000001",
    "a1b2c3d4-0002-4000-9000-000000000002",
    "a1b2c3d4-0003-4000-9000-000000000003"
  ]
}
```

Pole `guids` musí obsahovat **přesně `product_count` položek**. A ještě jedno pravidlo, na které se nezapomíná dvakrát: **GUIDy před odesláním deterministicky seřaďte**. Backend rozpoznává už existující sady podle kombinace GUIDů v pořadí, v jakém dorazily, takže táž trojice v jiném pořadí vyrobí druhou sadu se stejným obsahem. Jeden `.sort()` navíc a máte klid.

Odpověď je plochá, bez obálky:

```json
{
  "code": "WT-SET-00042",
  "price": 779.45,
  "products": [
    { "code": "KAVA-BR-250", "name": "Zrnková káva Brazílie 250 g", "image": "1021-1_zrnkova-kava-brazilie-250-g.jpg?ff=1&x=1024&y=768&q=85&ts=6b0c1d2e&sg=1a2b3c4d" },
    { "code": "KAVA-ET-250", "name": "Zrnková káva Etiopie 250 g", "image": "1022-1_zrnkova-kava-etiopie-250-g.jpg?ff=1&x=1024&y=768&q=85&ts=6b0c1d2f&sg=1a2b3c4d" },
    { "code": "KAVA-KO-250", "name": "Zrnková káva Kolumbie 250 g", "image": "1023-1_zrnkova-kava-kolumbie-250-g.jpg?ff=1&x=1024&y=768&q=85&ts=6b0c1d30&sg=1a2b3c4d" }
  ]
}
```

- **`code`** je kód sady — přesně tohle předáte do košíku Shoptetu.
- **`price`** je závazná cena po slevě (917 Kč mínus 15 % = 779,45 Kč).
- **`products`** je rozpis položek v pořadí odeslaných GUIDů. Hodí se pro potvrzovací obrazovku nebo pro hezčí rozpad balíčku v košíku, kde je jinak jedna položka se spojeným názvem.

Stavový kód **201** znamená, že sada právě vznikla, **200** že se stejná už dřív sestavila a vrací se uložená. Tělo je v obou případech identické, takže vám ve frontendu stačí testovat `response.ok`.

Zakládání sady probíhá synchronně a zahrnuje několik volání Shoptet API, takže počítejte s odezvou ve stovkách milisekund až jednotkách sekund. Tlačítko po kliknutí zablokujte, ukažte stav načítání a ošetřete dvojklik. Endpoint nevolejte spekulativně při každé změně výběru — jen při skutečném potvrzení.

Když se sadu založit nepodaří, nechte výběr zákazníka zachovaný a nabídněte opakování akce s obecnou hláškou („Balíček se teď nepodařilo připravit, zkuste to prosím znovu."). Automatické opakování ve smyčce nedělejte. Chyby si logujte i s `projectId` a shortcodem, ať je co dohledat. Validační odpovědi (stav 422) mají v těle klíč pole, kterého se týkají — používejte ho k diagnostice, ne jako text pro zákazníka, protože hlášky jsou anglicky a jejich znění není součástí kontraktu.

## Pravidla, která je potřeba dodržet

Kontrakt je volný, ale čtyři body v něm jsou závazné:

1. **Závazná cena je ta z odpovědi.** Průběžný přepočet ve widgetu je náhled pro zákazníka. Do souhrnu, do měření i do rozpisu v košíku patří hodnota `price` z odpovědi na založení sady. Obě čísla vznikají z trochu jiných vstupů — ceny v nabídce reflektují měnu a případný ceník zákazníka, cena sady se skládá ze základních cen v základní měně e-shopu. U e-shopu s jednou měnou a bez ceníků sedí, jinde se lišit mohou. Cenu proto nikdy neposílejte v požadavku a neslibujte nad orientačním číslem.
2. **Posílejte GUIDy z konfigurace.** Do sady patří jen produkty, které přišly v odpovědi na první volání — a v režimu `separate_lists` jen ty ze správného slotu. Ohlídat to je součást kontraktu frontendu.
3. **Dostupnost hlídejte v UI.** Pole `stock` máte k dispozici u každého produktu, takže vyprodané položky buď nenabízejte, nebo je viditelně označte a zablokujte jejich výběr. Finální kontrola pak proběhne standardními pravidly v košíku Shoptetu.
4. **Kód sady neukládejte.** Je platný pro aktuální nákup, ne napořád — neobjednané sady se pravidelně uklízejí, aby se v katalogu nehromadily. Nepatří tedy do `localStorage` jako uložený balíček, do sdílených odkazů ani do zákaznického účtu. Ukládejte si **GUIDy** vybraných produktů a při návratu zákazníka sadu jednoduše založte znovu.

A jedna věc navíc, která s API nesouvisí, ale s bezpečností ano: **data z API escapujte** dřív, než je vložíte do DOM. Názvy produktů a kategorií jsou uživatelský obsah z administrace e-shopu, takže je skládejte přes `textContent`, ne konkatenací do `innerHTML`.

## Minimální implementace

Celý flow se vejde do dvaceti pěti řádků. Chybí v nich jen UI a ošetření chyb, které si stejně napíšete po svém:

```js
const BASE = 'https://shoptet.webotvurci.cz';
const SHORTCODE = 'w-multipack-ABC12';

const dl = dataLayer.find((d) => d?.shoptet?.projectId !== undefined).shoptet;
const projectId = dl.projectId;
const priceListId = dl.customer?.priceListId ?? null;

// 1) Konfigurace — segment s ceníkem přidáme jen když ceník existuje
const cfgBase = `${BASE}/eshop-endpoints/${projectId}/multipack/configuration-products/${SHORTCODE}`;
const cfgRes = await fetch(priceListId ? `${cfgBase}/${priceListId}` : cfgBase, {
  credentials: 'omit',
  headers: { Accept: 'application/json' },
});
if (!cfgRes.ok) throw new Error('Konfiguraci balíčku se nepodařilo načíst.');
const { data } = await cfgRes.json();

// 2) Výběr zákazníka — přesně data.product_count GUIDů z data.categories
const selectedGuids = getSelectedGuidsFromUi();

// 3) Založení sady — guids vždy seřazené
const setRes = await fetch(`${BASE}/eshop-endpoints/${projectId}/multipack/product-set`, {
  method: 'POST',
  credentials: 'omit',
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
  body: JSON.stringify({ shortcode: SHORTCODE, guids: [...selectedGuids].sort() }),
});
if (!setRes.ok) throw new Error('Sadu se nepodařilo vytvořit.');
const set = await setRes.json(); // { code, price, products[] }

// 4) Do košíku nativní funkcí Shoptetu; závazná cena je set.price
shoptet.cartShared.addToCart({ productCode: set.code, amount: 1 });
```

Poslední řádek stojí za zdůraznění: do košíku se vkládá **nativní funkcí Shoptetu**, ne vlastním požadavkem. Ta dodá CSRF token, přepočítá košík včetně dopravy zdarma a dárků a zobrazí standardní potvrzení šablony. Podrobněji to rozebírá [návod o nasazení](https://www.webotvurci.cz/navody/sestav-si-vlastni-balicek/nasazeni-balicku-na-shoptet-krok-za-krokem/).

## Kam dál

- [Kompletní API reference na GitHubu](https://github.com/Webotvurci-s-r-o/shoptet-premium-multipack/blob/main/docs/03-api-reference.md) — všechna pole, chybové stavy, plné příklady odpovědí pro oba režimy seznamů.
- [Starter widget](https://github.com/Webotvurci-s-r-o/shoptet-premium-multipack/tree/main/examples/starter) — funkční vanilla JS implementace bez závislostí, kterou si můžete vzít jako základ. Celý repozitář je na [github.com/Webotvurci-s-r-o/shoptet-premium-multipack](https://github.com/Webotvurci-s-r-o/shoptet-premium-multipack) pod licencí MIT.
- [Sestav si vlastní balíček](https://www.webotvurci.cz/sluzby/shoptet/shoptet-premium-e-shop-bez-limitu/dynamicke-sady-balicky-na-miru/) — produktová stránka doplňku.

Chcete testovací konfiguraci nebo si nejste jistí návrhem integrace? Ozvěte se na **jsme@webotvurci.cz**, rádi to s vámi projdeme.

---

*Návod pochází z webu Webotvůrci — Zlatý a Premium partner Shoptetu.*
*Všechny návody: https://www.webotvurci.cz/navody/ · Kontakt: jsme@webotvurci.cz*
