Partner API
HTTP /api/v1 — menu, konfiguracja sklepu, checkout, sloty, rezerwacje, zapytania, catering, wycena ETA i zamówienia. Jeden opis API, zgodny z OpenAPI 3.1.
Partner API v1 to interfejs HTTP dostępny pod https://delieta.pl/api/v1. Klucz API, który Delieta wystawiła jednej firmie, wskazuje tę firmę: w żadnym żądaniu nie ma pola firmy. To jedyny opis dla ludzi; opis dla maszyn to GET /api/v1/openapi (OpenAPI 3.1, bez klucza).
Opublikowane operacje: GET /menu, GET /shop-config, POST /shop-heartbeat, GET /checkout/slots, POST /checkout, POST /checkout/quote, GET /checkout/result, GET /events, POST /quotes, POST /orders, GET /orders/{id}, GET /orders/{id}/status, GET /floor-plan, GET /reservations/availability, POST /reservations, POST /inquiries, POST /catering-orders, GET /catering-orders i GET /catalog-images/{id}/{filename}.
flowchart LR
bearer["Klucz API"] --> verify["Klucz jest firma"]
verify --> scope{"scope"}
scope -->|"catalog:read"| menu["GET /menu"]
scope -->|"checkout:create"| checkout["POST /checkout"]
scope -->|"orders:write"| handoff["POST /orders"]
handoff --> same["to samo zamowienie co panel"]Klucz i zakresy
Dwa równorzędne nagłówki. Pierwszy znaleziony wygrywa; klucz w query stringu nie jest nigdy przyjmowany.
Authorization: Bearer dlt_live_…
x-delieta-api-key: dlt_live_…
Kolejność kontroli: klucz → scope → klucz testowy (tylko przy zapisie) → limit. Brak albo zły klucz → 401 unauthorized.
| Scope | Rodzaj klucza | Operacje |
|---|---|---|
catalog:read | publikowalny | GET /api/v1/menu, GET /api/v1/shop-config, GET /api/v1/checkout/slots, GET /api/v1/events, GET /api/v1/orders/{id}/status, GET /api/v1/floor-plan, GET /api/v1/reservations/availability i POST /api/v1/shop-heartbeat. menu:read to alias starszych kluczy. |
checkout:create | publikowalny | POST /api/v1/checkout, POST /api/v1/checkout/quote, GET /api/v1/checkout/result, POST /api/v1/inquiries i POST /api/v1/reservations (rezerwacja web). |
eta:quote | sekretny | POST /api/v1/quotes |
orders:write | sekretny | POST /api/v1/orders i GET /api/v1/orders/{id} |
reservations:write | sekretny | POST /api/v1/reservations z własnego systemu firmy (rezerwacja api) |
catering:write | sekretny | POST /api/v1/catering-orders i GET /api/v1/catering-orders |
Klucz publikowalny to dokładnie catalog:read + checkout:create i nic więcej: ma go sklep, a sklep trzyma go po stronie serwera, nigdy w przeglądarce. Klucz publikowalny nie może dostać orders:write, eta:quote, reservations:write ani catering:write. Pusta lista scope'ów nie otwiera nic — 403 scope_required. Rezerwacja liczy scope według rodzaju klucza: publikowalny potrzebuje checkout:create, sekretny reservations:write.
dlt_test_… czyta prawdziwe produkty firmy i liczy wycenę. Każda próba zapisu kończy się odmową 403 test_key_readonly: POST /api/v1/checkout, /orders, /reservations, /inquiries, /catering-orders i /shop-heartbeat. Nie ma osobnego zbioru testowego.
Limity
Limity liczą się osobno dla każdego klucza, w oknie 60 s. Domyślnie 60 zapisów i 600 odczytów na minutę, liczone osobno, żeby częste odpytywanie nie zużyło limitu zapisów. Zapisy to tworzenie zamówienia, checkout, rezerwacja, zapytanie, zlecenie cateringowe i wycena. Odczyty to reszta. Po przekroczeniu: 429 rate_limited; respektuj Retry-After oraz RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset.
Z klucza publikowalnego korzysta wielu gości, więc jego limit liczy się na odwiedzającego. Przekaż adres gościa w nagłówku x-delieta-visitor; bez niego liczy się Origin. Dodatkowo działa wspólny limit górny równy 20-krotności limitu. Klucz sekretny ignoruje ten nagłówek.
Błędy
Każda odmowa v1 to application/problem+json (RFC 9457). Rozpoznawaj błąd po code, nie po tekście.
{
"type": "https://delieta.pl/problems/unauthorized",
"title": "Unauthorized",
"status": 401,
"code": "unauthorized",
"detail": "Provide a valid API key as `Authorization: Bearer dlt_live_…`."
}
Wyjątek: GET /api/v1/catalog-images/{id}/{filename} przy przekroczeniu limitu odpowiada zwykłym { "error": "rate_limited" } z 429, nie problem+json.
| code | status | kiedy |
|---|---|---|
unauthorized | 401 | brak albo zły klucz |
scope_required | 403 | klucz bez wymaganego scope |
test_key_readonly | 403 | dlt_test_… próbował zapisu |
module_disabled | 403 | moduł firmy jest wyłączony (plan albo przełącznik właściciela): zamówienia przy POST /checkout i GET /checkout/slots, rezerwacje, catering. Status złożonego zamówienia działa dalej |
invalid_request | 400 | body nie jest JSON-em albo pada walidacja; szczegóły w errors[] (np. unknown_field przy POST /orders) |
idempotency_key_required | 400 | zapis bez Idempotency-Key (8–255 znaków) |
line_price_missing / line_name_missing / total_out_of_range | 400 | linii zamówienia albo sumy nie da się wycenić |
payload_too_large | 413 | body zapytania albo zlecenia cateringowego ponad 64 KiB |
not_found | 404 | nie ma takiego zamówienia dla tego klucza (obce id odpowiada identycznie) |
tenant_not_found | 404 | firma klucza już nie istnieje |
idempotency_key_conflict | 409 | ten sam klucz idempotencji, inne body |
conflict | 409 | np. zapytanie jest już powiązane z innym zleceniem |
venue_not_configured | 409 | wycena, a firma nie ma współrzędnych lokalu |
out_of_range | 409 | wycena albo checkout z dostawą: adres poza zasięgiem dostawy firmy (errors[] na polu address). Przy rezerwacji ten sam code to 422 (data poza zakresem) |
delivery_unconfigured | 409 | checkout z dostawą: lokal ma progi kilometrowe, ale nie ma adresu albo ustawionej dostawy. Odbiór osobisty działa |
total_changed | 409 | checkout: podane expectedTotalGrosze nie zgadza się z kwotą liczoną teraz. Serwer nie użył go do wyceny |
product_outside_hours | 409 | checkout: pozycja jest poza godzinami dostępności w wybranym terminie; linie w errors[] na items.N.itemId |
shop_closed | 409 | sklep jest teraz zamknięty i nie przyjmie „jak najszybciej” |
shop_unconfigured | 409 | brak zapisanego originu sklepu na adresy powrotu płatności |
connect_not_ready | 409 | konto do płatności online nie może jeszcze pobierać płatności |
sample_mode | 409 | płatność online w sklepie na danych przykładowych, gdy płatności są na żywo |
slot_unavailable | 409 | requestedFor nie jest żadnym z terminów z GET /checkout/slots; przy rezerwacji: termin zajęty |
table_taken | 409 | rezerwacja: stolik zajęty (także gdy ktoś inny zarezerwował go chwilę wcześniej) |
fulfilment_unavailable | 409 | sklep nie oferuje takiego odbioru (dostawa albo odbiór osobisty) |
payment_unavailable | 409 | sklep nie przyjmuje takiej płatności (online albo gotówka) |
product_outside_hours | 409 | godziny dostępności pozycji nie obejmują czasu realizacji; każda taka linia jest w errors[] (items.N.itemId) |
out_of_range | 409 | adres dostawy jest dalej niż zasięg lokalu (pole address) |
delivery_unconfigured | 409 | sklep liczy dostawę od odległości, a lokal nie ma współrzędnych |
total_changed | 409 | suma zmieniła się od wyceny (expectedTotalGrosze); nic nie zostało pobrane |
duplicate_external_ref | 409 | to externalRef już utworzyło zamówienie tej firmy |
unprocessable | 422 | nieznana albo niedostępna pozycja (errors[].code: product_unknown, product_unavailable); nieznany format cateringowy |
below_minimum | 422 | koszyk poniżej minimalnej wartości zamówienia sklepu (same dania, bez opakowań i dostawy; pole items) |
product_menu_only | 422 | checkout: pozycja jest „Tylko w karcie” i nie można jej zamówić online; linie w errors[] na items.N.itemId. Gdy zachodzą oba przypadki, wygrywa ten |
address_not_found | 422 | checkout z dostawą: adresu nie dało się ustalić (pole address) |
headcount_out_of_range | 422 | zapytanie cateringowe: liczba osób poza zakresem formatu (catering.headcount) |
venue_not_offered | 422 | zapytanie cateringowe: format nie obsługuje wybranego miejsca (catering.venue) |
unknown_add_on | 422 | zapytanie cateringowe: dodatek nie należy do wybranego formatu (catering.addOns.N.key) |
unknown_event, event_not_bookable, event_past, event_phone_only | 422 | zapytanie o wydarzenie: brak takiego wydarzenia, nie przyjmuje zgłoszeń, już się odbyło albo zapisy tylko telefonicznie (event.eventId) |
unknown_occurrence, event_in_past | 422 | zapytanie o wydarzenie: termin spoza listy albo z przeszłości (event.occurrenceId, event.preferredDate) |
participants_out_of_range | 422 | zapytanie o wydarzenie: liczba uczestników poza zakresem wydarzenia (event.participants) |
option_invalid | 422 | nieznana opcja, brak wymaganej grupy albo za dużo wyborów w grupie |
validation_failed / party_too_large | 422 | rezerwacja: zła treść, grupa większa niż mieści sala |
rate_limited | 429 | przekroczony limit na minutę; zob. limity |
internal | 500 | błąd po naszej stronie; ponów |
create_failed | 500 | POST /orders: zamówienia nie udało się zapisać; ponów tym samym Idempotency-Key |
idempotency_unavailable | 503 | magazyn idempotencji nie działa; ponów tym samym kluczem |
delivery_quote_unavailable | 503 | checkout albo wycena z dostawą: nie udało się teraz policzyć odległości; ponów albo wybierz odbiór osobisty |
module_unavailable | 503 | nie udało się odczytać modułów firmy (moduł nie jest wyłączony); ponów za chwilę (Retry-After) |
Pieniądze
Wyłącznie całkowite grosze. 3000 to 30,00 zł. Żadnych floatów, żadnego pola waluty na kwotach. currency w odpowiedziach jest stałą "PLN".
GET /api/v1/menu
Produkty firmy, do której należy klucz. Scope catalog:read. Pozycje niedostępne są pominięte — w DTO nie ma flagi available do obsługi po stronie klienta. Pozycje „Tylko w karcie” (orderable: false) są w menuOnlyItems, nie w items; godziny dostępności są w availability. W odpowiedzi nie ma flagi „dostępne teraz”: sklep liczy ją sam z availability. Struktura odpowiedzi się nie zmienia, nowe pola mogą tylko dochodzić.
GET /api/v1/menu
Authorization: Bearer dlt_live_…
{
"currency": "PLN",
"generatedAt": "2026-08-26T12:00:00.000Z",
"locale": "pl",
"categories": [
{ "id": "c1", "slug": "pizza", "name": "Pizza", "sort": 0, "orderable": true, "availability": null }
],
"items": [
{
"id": "i1",
"slug": "margherita",
"categoryId": "c1",
"name": "Margherita",
"description": null,
"basePriceGrosze": 3200,
"allergens": [],
"modifierGroups": [],
"imageUrl": null,
"featured": false,
"sort": 0,
"orderable": true,
"availability": null
}
],
"menuOnlyCategories": [],
"menuOnlyItems": []
}
Pusta tablica kategorii i pozycji jest poprawną odpowiedzią, nie błędem. Cache-Control: no-store. Odpowiedź zawiera nagłówek x-delieta-cache-tag; sklep, który ma własny cache, używa go jako klucza.
Opcjonalne ?locale= (pl, en, de, uk) zwraca nazwy i opisy w tym języku, pole po polu, a gdy brakuje tłumaczenia, po polsku. Dla języka, którego firma nie włączyła (albo nieznanego), dostajesz treść po polsku; locale w odpowiedzi mówi, jaki język faktycznie zwrócono. To samo ?locale= czytają GET /api/v1/shop-config (nazwy formatów cateringowych; zwraca też włączone locales, polski pierwszy) i GET /api/v1/floor-plan (nazwy stref).
imageUrl to bezwzględny adres zdjęcia dania (wersja 800 px) albo null, gdy danie nie ma zdjęcia. featured: true oznacza, że właściciel oznaczył danie jako „Polecane”.
orderable: false to „tylko w karcie”: pokaż pozycję z ceną, bez przycisku „Do koszyka”. Takie pozycje nie są w items, tylko w menuOnlyItems, a kategoria, w której każda pozycja jest tylko w karcie, jest w menuOnlyCategories, nie w categories — klient, który nie zna tych kluczy, po prostu ich nie pokaże i nigdy nie zaproponuje czegoś, czego nie da się zamówić. @delieta/shop-runtime od 1.8.0 scala je z powrotem (withMenuOnly). Wartość na pozycji uwzględnia już kategorię (kategoria tylko w karcie = każda jej pozycja). availability to godziny dostępności: { "windows": [{ "days": [1,2,3,4], "from": "12:00", "to": "16:30" }], "fromDate": null, "toDate": null } — do 7 okien, dni od 0 = poniedziałek, obie granice włącznie, czas Europe/Warsaw. Pozycja ma własną regułę, kategoria swoją; obowiązują obie naraz. Pozycja po sezonie (toDate minęło) w ogóle nie trafia do menu. Menu jest cache'owane, więc nie mówi, czy pozycja jest dostępna teraz — policz to z reguły (robi to @delieta/shop-runtime od 1.8.0: itemSaleState, liveMenu). Checkout sprawdza regułę dla czasu realizacji: requestedFor albo teraz + czas przygotowania przy „jak najszybciej”.
GET /api/v1/catalog-images//
Zdjęcie dania albo formatu cateringowego. Bez klucza: adres z pola imageUrl otwiera przeglądarka klienta sklepu, więc nie wolno wymagać sekretu. Trzeba znać id i dokładną nazwę pliku — para, która nie pasuje, to 404. ?size=card zwraca wersję o szerokości 800 px (zdjęcie węższe zwracane jest bez zmian), bez parametru dostajesz oryginał.
GET /api/v1/catalog-images/{id}/{filename}?size=card
Odpowiedź to 302 do magazynu plików albo bajty obrazu (image/jpeg, image/png, image/webp lub image/avif). Adres zdjęcia nigdy nie zmienia zawartości — podmienione zdjęcie dostaje nowy adres — więc odpowiedź niesie Cache-Control: public, max-age=31536000, immutable. Limit: 600 żądań na minutę z jednego IP (rate_limited).
GET /api/v1/shop-config
Marka i zasady sklepu firmy, do której należy klucz, w takiej postaci, w jakiej pokazuje je jej sklep. Scope catalog:read — ten sam co menu, więc klucz publikowalny już go ma. Własne ustawienia marki sklepu mają pierwszeństwo, pole po polu; gdy ich brak, obowiązują nazwa, logo i kolor firmy. Odpowiedź zawiera x-delieta-cache-tag.
GET /api/v1/shop-config
Authorization: Bearer dlt_live_…
{
"name": "Bella Napoli",
"tagline": "Pizza z pieca opalanego drewnem",
"brandColor": "#e2574c",
"logo": "https://delieta.pl/api/v1/catalog-images/{id}/{filename}",
"banner": "https://delieta.pl/api/v1/catalog-images/{id}/{filename}",
"icon": "https://delieta.pl/api/v1/catalog-images/{id}/{filename}",
"currency": "PLN",
"locale": "pl",
"locales": ["pl", "en"],
"layout": "grid",
"links": {
"phone": "+48000000000",
"email": "kontakt@bella.example",
"instagram": "https://instagram.com/bella.example",
"facebook": "https://facebook.com/bella.example",
"address": "Kwiatowa 5, Bydgoszcz",
"mapUrl": "https://www.google.com/maps/dir/?api=1&destination=Kwiatowa%205%2C%20Bydgoszcz"
},
"modules": ["ordering", "reservations", "catering"],
"ordering": {
"fulfilmentModes": ["delivery", "pickup"],
"paymentModes": ["online", "cash"],
"onlineMethods": ["card", "blik"],
"deliveryFeeGrosze": 500,
"minimumOrderGrosze": 4000,
"prepMinutes": 30
},
"hours": {
"days": [
{ "weekday": 0, "closed": false, "open": "10:00", "close": "22:00" },
{ "weekday": 6, "closed": true, "open": "10:00", "close": "22:00" }
]
},
"closures": [
{ "from": "2026-12-24", "to": "2026-12-26", "note": "Święta" }
],
"cateringFormats": [
{
"id": "f1",
"name": "Lunch firmowy",
"description": "Pizza i napoje dla zespołu.",
"fromPriceGrosze": 3500,
"minHeadcount": 10,
"imageUrl": null,
"items": [
{ "productId": "i1", "name": "Margherita", "quantity": 1, "perPerson": true, "note": null }
]
}
],
"generatedAt": "2026-08-26T12:00:00.000Z"
}
| Pole | Znaczenie |
|---|---|
banner | opcjonalny baner 3:1 firmy, albo null |
icon | kwadratowa ikona firmy (min. 512×512, PNG) na favicon i ikonę aplikacji, albo null: sklep zostaje wtedy przy własnej. W runtime 1.9: shopIcons(config) |
locales | języki włączone przez firmę, polski pierwszy |
layout | grid albo list |
links | tylko to, co lokal wypełnił; mapUrl powstaje z adresu (trasa w Google Maps), gdy lokal nie podał własnego |
modules | moduły sklepu, które firma ma włączone: ordering, reservations, catering, events. Tego, czego tu nie ma, sklep nie pokazuje |
ordering | jak sklep przyjmuje zamówienia: fulfilmentModes, paymentModes (online tylko gdy sklep może pobrać kartę; lista może być pusta), onlineMethods (card, blik, p24: metody aktywne na koncie Stripe; pusta bez online; podawaj tylko te. Może być pusta, choć online jest w ofercie (Delieta nie zapisała jeszcze, które metody są aktywne): pokaż ogólne «Płatność online» i nie nazywaj żadnej metody), deliveryFeeGrosze, minimumOrderGrosze, prepMinutes. null, gdy moduł zamówień jest wyłączony: nie pokazuj wtedy formularza kasy |
hours | tygodniowe godziny otwarcia albo null, gdy lokal ich nie ustawił — wtedy nie zakładaj żadnych godzin |
closures | nadchodzące dni zamknięte (from, to, note albo null), daty w Europe/Warsaw. Brak pola znaczy: brak dni zamkniętych |
cateringFormats | formaty cateringowe z listą produktów (items) i zdjęciem (imageUrl); tylko gdy moduł catering jest włączony. Są tu tylko formaty z venue delivery albo both i priceBasis from — te, które sklep na pakiecie 1.7 pokaże poprawnie |
cateringPackages | wszystkie aktywne formaty z pełnymi polami pakietu; tylko gdy moduł katering jest włączony. Pola: slug, venue (onsite, delivery, both), priceBasis (from, per_person, from_per_person), minHeadcount/maxHeadcount, sections (linie z produktem albo z własną nazwą), included („W cenie”), terms („Warto wiedzieć”) i addOns (key, name, amount, unit: szt, kg, g, l, ml albo porcja, priceGrosze, product?). Pakiet sklepu 1.8 czyta to pole zamiast cateringFormats |
W hours.days jest dokładnie siedem wpisów, po jednym na dzień (weekday 0 = poniedziałek … 6 = niedziela); powyżej skrócono do dwóch. open i close to HH:MM w czasie lokalu i mają znaczenie tylko dla closed: false. Moduły, ordering i links są odczytywane przy każdym żądaniu. Firma, do której należał klucz, już nie istnieje → 404 tenant_not_found.
POST /api/v1/shop-heartbeat
Wdrożony sklep zgłasza, z jakim runtime go zbudowano. Raz przy każdym buildzie, nigdy przy każdym żądaniu. Scope catalog:read, bez własnego zakresu. Zapis dotyczy tylko kolumn floty w wierszu sklepu tej firmy. Zawsze 204, także gdy firma nie ma wiersza sklepu albo body jest nieczytelne. Klucz dlt_test_… dostaje odmowę 403 test_key_readonly. Robi to za Ciebie delieta-shop-assert; nie musisz wywoływać tego sam.
POST /api/v1/shop-heartbeat
Authorization: Bearer dlt_live_…
Content-Type: application/json
{ "runtime": "1.6.0", "contract": "v3", "commit": null, "modified": false }
POST /api/v1/checkout
Koszyk sklepu. Scope checkout:create (klucz publikowalny). Wymaga modułu zamówień. Idempotency-Key jest obowiązkowy (8–255 znaków). Klient wysyła co, nigdy ile: { items: [{ itemId, quantity, options? }] }, gdzie quantity to 1–99, najwyżej 50 linii, a options to [{ group, option }] (klucze grupy i opcji z menu). Ceny, dopłaty opcji i suma liczy Delieta z produktów firmy, do której należy klucz, w całkowitych groszach PLN. Pole unitPrice / total / companyId w body jest ignorowane. Adresy po udanej i anulowanej płatności pochodzą z konfiguracji sklepu tej firmy, nigdy z żądania.
Sklep, który sam zbiera dane gościa, dokłada: fulfilment (delivery albo pickup, domyślnie delivery), requestedFor (termin z GET /api/v1/checkout/slots jako data ISO z przesunięciem, albo null = jak najszybciej), customer (name, phone, email?), address (street, number, flat?, postalCode w formacie 00-000, city, note?), invoice (nip z sumą kontrolną, companyName?, address?), note (do 500 znaków), locale i payment (blik, card, p24 albo cash). Zasady: odbiór osobisty, gotówka i adres wymagają customer; dostawa z customer wymaga address; zamówienie na odbiór odrzuca adres. Opcjonalne expectedTotalGrosze to kwota, którą klient widział na ekranie. Serwer nigdy nie wycenia z niej, tylko porównuje ją z kwotą policzoną teraz: gdy się różnią, odpowiada 409 total_changed. Samo items to dawny format: płatność online, dostawa, a Stripe sam pyta o telefon i adres.
POST /api/v1/checkout
Authorization: Bearer dlt_live_…
Content-Type: application/json
Idempotency-Key: chk-2026-08-26-42
{ "items": [{ "itemId": "i1", "quantity": 2 }] }
Odpowiedź 201 (checkout)
{
"url": "https://checkout.stripe.com/c/pay/cs_test_…",
"totalGrosze": 6400,
"currency": "PLN",
"pricingSource": "catalog"
}
Płatność online odpowiada url: przekieruj gościa na stronę płatności. Zamówienie powstaje, gdy zapłaci; wynik czytasz w GET /api/v1/checkout/result. Płatność gotówką (payment: "cash") odpowiada od razu złożonym zamówieniem, jeszcze nieopłaconym, które czeka na przyjęcie przez kuchnię:
{
"orderShortId": "a1b2c3d4",
"trackingUrl": "https://delieta.pl/o/a1b2c3d4",
"totalGrosze": 6400,
"currency": "PLN",
"pricingSource": "catalog"
}
Przekaż gościowi trackingUrl, a status odpytuj przez GET /api/v1/orders/{id}/status. Gotówka nie potrzebuje konta do płatności online, ale wymaga customer. Ten sam Idempotency-Key zwraca to samo zamówienie; ponowione złożone zamówienie gotówkowe odpowiada tym zamówieniem, zanim zostanie sprawdzony jakikolwiek warunek.
Odmowy: moduł zamówień wyłączony → 403 module_disabled. Nieznana albo niedostępna pozycja → 422 unprocessable (errors[].code: product_unknown, product_unavailable). Nieznana opcja, brak wymaganej grupy albo za dużo wyborów → 422 option_invalid. Koszyk poniżej minimum sklepu → 422 below_minimum. Pozycja „Tylko w karcie” → 422 product_menu_only. Pozycja poza godzinami dostępności → 409 product_outside_hours. Dostawa pod adres poza zasięgiem → 409 out_of_range. Adres, którego nie znaleziono → 422 address_not_found. Chwilowy brak wyceny dostawy → 503 delivery_quote_unavailable. Dostawa nieustawiona w lokalu → 409 delivery_unconfigured. Zmieniona kwota → 409 total_changed. Zamknięte godziny i brak „jak najszybciej” → 409 shop_closed. Termin spoza listy → 409 slot_unavailable. Odbiór, którego sklep nie oferuje → 409 fulfilment_unavailable. Płatność, której sklep nie przyjmuje → 409 payment_unavailable. Płatność online w sklepie na danych przykładowych przy płatnościach na żywo → 409 sample_mode (podgląd płaci tylko w trybie testowym). Brak originu sklepu → 409 shop_unconfigured. Konto do płatności online nieprzygotowane → 409 connect_not_ready (nie ma fałszywej płatności).
Pozycje „Tylko w karcie” i poza godzinami wymieniają każdą linię w errors[] (items.N.itemId). Opcjonalne expectedTotalGrosze (suma z wyceny) tylko porównujemy: inna suma → 409 total_changed, nic nie zostaje pobrane.
Opakowania i dostawę dolicza serwer, tak samo jak pokazała je POST /api/v1/checkout/quote: opakowanie za sztukę dania (produkt, inaczej kategoria, inaczej sklep), opłata za zamówienie (np. torba) i dostawa (stała, darmowa od progu albo według progów odległości). W Stripe to osobne pozycje: Opakowanie, nazwa opłaty za zamówienie i Dostawa.
GET /api/v1/checkout/slots
Kiedy sklep przyjmie zamówienie: „jak najszybciej” oraz terminy, które gość może wybrać. Scope catalog:read. Parametr fulfilment to delivery (domyślnie) albo pickup. Terminy liczy się z godzin otwarcia, dni zamkniętych, czasu przygotowania, kroku i horyzontu sklepu w czasie Europe/Warsaw i zwraca jako chwile ISO. Checkout sprawdza te same terminy chwilę później, więc czas spoza listy kończy się 409 slot_unavailable. Nigdy z pamięci podręcznej. Bez ustawionych godzin sklep przyjmuje „jak najszybciej”, ale nie ma siatki terminów.
GET /api/v1/checkout/slots?fulfilment=pickup
Authorization: Bearer dlt_live_…
{
"asap": { "available": true, "readyInMinutes": 30 },
"slots": [
{ "startsAt": "2026-08-26T08:30:00.000Z" },
{ "startsAt": "2026-08-26T08:45:00.000Z" }
]
}
Zły fulfilment → 400 invalid_request. Sklep, który nie oferuje tego odbioru → 409 fulfilment_unavailable. Moduł zamówień wyłączony → 403 module_disabled.
GET /api/v1/checkout/result
Zweryfikowany wynik jednej płatności online, dla strony potwierdzenia sklepu. Scope checkout:create. Podaj session_id z adresu powrotu (cs_test_… albo cs_live_…; płatność dokłada go do adresu sukcesu). Odpowiedź pochodzi z zamówienia i płatności w Delieta, nie z przeglądarki: paid, processing (pieniądze są w drodze albo zamówienie się jeszcze zapisuje; zapytaj za kilka sekund), unpaid (gość wyszedł albo sesja wygasła) lub unknown (to nie jest checkout tego sklepu). statementDescriptor to tekst, który klient zobaczy na wyciągu; jest null, gdy to nie płatność kartą. method to blik, card, p24, cash, other albo null. Brak albo zły session_id → 400 invalid_request. Nigdy z pamięci podręcznej.
GET /api/v1/checkout/result?session_id=cs_live_…
Authorization: Bearer dlt_live_…
{
"status": "paid",
"orderShortId": "K7M2QX",
"trackingUrl": "https://bella.delieta.pl/o/K7M2QX",
"totalGrosze": 5400,
"merchantName": "Bella Pizza",
"statementDescriptor": "BELLA* 3F9A12CD",
"method": "blik",
"livemode": true
}
POST /api/v1/checkout/quote
Wycena koszyka bez zakładania zamówienia: dania, opakowania, dostawa i suma. Scope checkout:create (klucz publikowalny). Body: { items, fulfilment?, address? }, jak w POST /checkout, bez danych gościa i płatności; pola z kwotami są ignorowane. Liczy to ten sam kalkulator, którego używa checkout, więc sklep nie liczy opłat sam, tylko pokazuje to, co zwróci wycena. Limit: 60 wycen na minutę na odwiedzającego (rate_limited), osobno od odczytów menu: każdy nowy adres to płatne geokodowanie.
Odpowiedź 200:
{
"quote": {
"currency": "PLN",
"itemsGrosze": 4500,
"packagingGrosze": 200,
"packaging": [{ "kind": "item", "label": "Opakowanie", "quantity": 2, "unitGrosze": 100, "totalGrosze": 200 }],
"delivery": { "status": "priced", "feeGrosze": 500, "free": false, "freeFromGrosze": 9000, "missingForFreeGrosze": 4500, "distanceMeters": 3200 },
"totalGrosze": 5200,
"minimum": { "minimumOrderGrosze": 3000, "missingGrosze": 0 }
}
}
delivery.status to none (odbiór osobisty), needs_address (progi kilometrowe, a adresu jeszcze nie ma) albo priced. Przy priced dochodzi feeGrosze i, gdy cena zależy od odległości, distanceMeters. Pola fromGrosze i toGrosze opisują zakres opłaty przy cenie zależnej od odległości. Pole packaging[].kind to item (opłata za sztukę) albo order (opłata za zamówienie). freeFromGrosze i missingForFreeGrosze służą do podpowiedzi „do darmowej dostawy brakuje…”. totalGrosze jest null, dopóki potrzebny jest adres.
Poniżej minimum wycena nie jest błędem: brak widać w minimum.missingGrosze. Błąd pojawi się dopiero w POST /checkout jako below_minimum. Odległość to linia prosta od lokalu, zaokrąglona w górę do 100 m; darmowa dostawa i minimum liczą się od wartości dań, bez opakowań i dostawy (dokładnie próg = darmowo). Odmowy wyceny: out_of_range, address_not_found, delivery_quote_unavailable (odbiór dalej się wycenia), delivery_unconfigured, fulfilment_unavailable. Wycena nie sprawdza „Tylko w karcie” ani godzin dostępności (product_menu_only, product_outside_hours): to robi checkout, w czasie realizacji. Kody są w tabeli błędów. Zasady cennika: Sklep.
POST /api/v1/quotes
Szacunek dojazdu zanim istnieje zamówienie. Scope eta:quote. Start to lokal firmy; podajesz tylko cel. Wynik jest zawsze zakresem (minMinutes / maxMinutes). Pole guaranteed jest stałą false — to nie jest SLA. Po expiresAt zapytaj ponownie.
{ "destination": { "lat": 53.123456, "lng": 18.012345 } }
{
"quote": {
"currency": "PLN",
"minMinutes": 35,
"maxMinutes": 50,
"guaranteed": false,
"breakdown": {
"prepMinutes": 15,
"loadMinutes": 4,
"travelMinutes": 12,
"padMinutes": 8
},
"distanceMeters": 4200,
"computedAt": "2026-08-26T12:00:00.000Z",
"expiresAt": "2026-08-26T12:05:00.000Z"
}
}
Pokaż oba krańce przedziału. Jedna liczba w UI partnera wprowadza w błąd. Brak współrzędnych lokalu → 409 venue_not_configured. Cel poza promieniem dostaw firmy → 409 out_of_range. Złe body → 400 invalid_request. Firma, do której należał klucz, nie istnieje → 404 tenant_not_found.
POST /api/v1/orders
Tworzy zamówienie firmy, do której należy klucz. Scope orders:write. Serwer sumuje linie i odrzuca podaną przez Ciebie sumę. Ceny jednostkowe pochodzą od wywołującego: pricingSource to "caller". To zamówienie z kanału partnera, nie koszyk sklepu; koszyk z cenami z katalogu to POST /api/v1/checkout.
Idempotency-Key
Nagłówek obowiązkowy, 8–255 znaków po obcięciu spacji. Jeden klucz na logiczną próbę. Ponów ten sam klucz i to samo body po błędzie sieci, 500 create_failed albo 503 idempotency_unavailable. Udana odpowiedź 201 jest zapamiętywana na 24 godziny: powtórzenie zwraca tę samą odpowiedź i Idempotent-Replayed: true. Ten sam klucz, inne body → 409 idempotency_key_conflict. Gdy magazyn idempotencji jest niedostępny, dostajesz 503, nigdy duplikat.
Body
Nie ma pola firmy. Typowe wywołanie z cenami od wywołującego:
{
"customerName": "Jan Kowalski",
"customerPhone": "+48123456789",
"destinationAddress": "ul. Przykładowa 1, 85-000 Bydgoszcz",
"destinationPoint": { "lat": 53.123456, "lng": 18.012345 },
"items": [
{ "name": "Margherita 32cm", "qty": 1, "unitPrice": 3200 },
{ "name": "Cola 0,5l", "qty": 2, "unitPrice": 800 }
],
"paymentMethod": "cash",
"channel": "web",
"externalRef": "your-storefront-order-42"
}
| Pole | Wymagane | Uwagi |
|---|---|---|
customerName | tak | 1–200 znaków |
customerPhone | tak | 1–40 znaków |
destinationAddress | tak | 1–500 znaków |
destinationPoint | nie | { lat, lng } WGS84. Nie destination ani destinationCoordinates — to 400 invalid_request z errors[].code unknown_field, bo zamówienie bez współrzędnych nie dostanie ETA |
items | tak | 1–50 linii; qty 1–99 |
items[].name / items[].unitPrice | tak | grosze; serwer przelicza totalGrosze |
items[].menuItemId | nie | id produktu; options jest ignorowane przy wycenie |
details | nie | do 4000 znaków |
customerNote | nie | do 2000 znaków |
paymentMethod | nie | cash | card | online (domyślnie cash) |
channel | nie | phone | web | pos | integration (domyślnie web) |
scheduledFor | nie | termin jako data ISO |
sourceVariant | nie | do 64 znaków |
externalRef | nie | do 64 znaków; dedup po Twojej stronie, kolizja → duplicate_external_ref |
Zamówienie z kanału web (domyślny) to zamówienie gościa, więc obowiązują go reguły sprzedaży z menu: linia z menuItemId produktu „tylko w karcie” → 422 product_menu_only, linia poza godzinami dostępności (liczonymi dla scheduledFor albo teraz + czas przygotowania firmy) → 409 product_outside_hours; każda taka linia jest w errors[] jako items.N.menuItemId. Kanały pos, phone i integration to sprzedaż, którą lokal zrobił sam: reguły ich nie dotyczą.
POST /api/v1/orders
Authorization: Bearer dlt_live_…
Content-Type: application/json
Idempotency-Key: ord-2026-08-26-storefront-42
Odpowiedź 201 (zamówienie)
{
"order": {
"id": "1",
"shortId": "a1b2c3d4",
"status": "accepted",
"createdAt": "2026-08-26T12:00:00.000Z",
"tenantSlug": "bella-pizza",
"externalRef": "your-storefront-order-42",
"currency": "PLN",
"totalGrosze": 4800,
"lines": [
{ "name": "Margherita 32cm", "qty": 1, "unitPrice": 3200 },
{ "name": "Cola 0,5l", "qty": 2, "unitPrice": 800 }
],
"pricingSource": "caller",
"trackingUrl": "https://delieta.pl/o/a1b2c3d4"
}
}
status bywa accepted albo pending_acceptance, zależnie od trybu przyjęcia firmy. trackingUrl przekazujesz klientowi. To jedyny publiczny adres śledzenia. Błędy zapisu: 404 tenant_not_found, 409 duplicate_external_ref, 500 create_failed.
GET /api/v1/orders/
Odczyt jednego zamówienia firmy, do której należy klucz. Scope jest ten sam: orders:write — zestaw zakresów jest zamknięty i już wydany na działających kluczach; nowy orders:read złamałby istniejące integracje. Odpytywanie liczy się do limitu odczytów, nie tworzenia.
id to order.id z odpowiedzi 201. Obce albo nieistniejące id → 404 not_found, więc odpowiedź nie zdradza, czy takie zamówienie istnieje. Odpowiedź zawiera mniej pól niż odpowiedź przy tworzeniu. Partner dostaje przebieg zamówienia, a nie kolumny wewnętrzne:
{
"order": {
"id": "1",
"shortId": "a1b2c3d4",
"status": "pending_acceptance",
"externalRef": "your-storefront-order-42",
"createdAt": "2026-08-26T12:00:00.000Z",
"promisedAt": null,
"acceptedAt": null,
"rejectedAt": null,
"rejectionReason": null,
"acceptanceDeadlineAt": 1756200000000
}
}
acceptedAt, rejectedAt i acceptanceDeadlineAt są unix ms albo null. Statusy, które zobaczysz, to kolejne statusy zamówienia: pending_acceptance, accepted, preparing, ready, on_the_way, delivered i rejected. Dostawa pomija ready, odbiór osobisty pomija on_the_way.
GET /api/v1/orders//status
Gdzie jest zamówienie, dla strony śledzenia gościa. Scope catalog:read, więc klucz publikowalny może to odpytywać ze sklepu. {id} to krótki kod zamówienia (orderShortId z odpowiedzi checkoutu, wielkość liter nie ma znaczenia), nie order.id. Odpowiedź nie zawiera żadnych danych klienta: tylko status, sposób odbioru i czas. Nieistniejący, źle zapisany albo cudzy kod odpowiada tak samo: 404 not_found.
GET /api/v1/orders/a1b2c3d4/status
Authorization: Bearer dlt_live_…
{
"status": "preparing",
"fulfilment": "pickup",
"etaMinutes": 12,
"promisedAt": "2026-08-26T08:45:00.000Z"
}
status to jeden z: pending_acceptance, accepted, preparing, ready (odbiór: „Gotowe do odbioru”), on_the_way, delivered, rejected. etaMinutes to pełne minuty do promisedAt, nigdy ujemne, i pojawia się tylko, gdy kuchnia ma zamówienie (accepted, preparing, on_the_way). promisedAt to czas obiecany przez obsługę, a bez niego czas, o który poprosił gość. readyAt pojawia się, gdy zamówienie na odbiór ma status ready.
Rezerwacje stolików
Wymagają włączonego modułu rezerwacji (plan albo przyznanie, oraz przełącznik właściciela). Wyłączony moduł: 403 module_disabled.
GET /api/v1/floor-plan
GET /api/v1/floor-plan
Strefy i stoliki firmy z geometrią (grid to układ { width, height } bez jednostek, sklep skaluje go do rysunku). Zakres catalog:read. ?locale= tłumaczy nazwy stref. Odpowiedź zawiera x-delieta-cache-tag.
GET /api/v1/reservations/availability
GET /api/v1/reservations/availability?date=2030-06-14&partySize=2
Wolne godziny startu i, dla każdej, wolne stoliki mieszczące grupę: date, partySize, durationMinutes i slots[] z startsAt, time i tableIds. time to czas lokalny Europe/Warsaw, startsAt to chwila do zarezerwowania. Gdy nie ma terminów, pole closedReason mówi dlaczego: closed_day, closure albo hours_not_configured. Zły format date albo partySize poza 1–100: 422 validation_failed. Zakres catalog:read. Nigdy z pamięci podręcznej.
POST /api/v1/reservations
POST /api/v1/reservations
Authorization: Bearer dlt_live_…
Idempotency-Key: 4b0b3f0e-6d21-4f0a-9c1c-0d6f0e0a7a11
Content-Type: application/json
{ "startsAt": "2030-06-14T16:00:00.000Z", "partySize": 2, "tableId": "…",
"guest": { "name": "Ala Kowalska", "phone": "600100200", "email": "ala@example.pl" },
"occasion": "birthday", "note": "przy oknie" }
Pola: startsAt (data ISO z przesunięciem), partySize (1–100), tableId?, guest (name do 80 znaków, phone, email?), occasion? (none, birthday, anniversary, date, business), note? (do 500 znaków), locale? (pl, en, de, uk — w tym języku idzie potwierdzenie do gościa). Body ponad 8 KiB → 422 validation_failed.
Dwa rodzaje klucza. Klucz publikowalny z checkout:create robi rezerwację web: start musi być oferowanym terminem, status to requested albo confirmed przy automatycznym potwierdzaniu. Klucz sekretny z reservations:write robi rezerwację api z własnego systemu: o dowolnej godzinie, stolik ma być po prostu wolny, status zawsze confirmed. Idempotency-Key (8–100 znaków) jest opcjonalny, ale respektowany: ten sam klucz zwraca pierwszą rezerwację. Pole website to pułapka na boty, zostaw puste.
201 zwraca id, reference, status, startsAt, durationMinutes, tableIds. status to requested, confirmed, seated, completed, cancelled albo no_show. Odmowy to problem+json ze stabilnym code:
| Status | code |
|---|---|
409 | slot_unavailable, table_taken |
422 | validation_failed, party_too_large, out_of_range |
403 | module_disabled, scope_required |
GET /api/v1/events
Wydarzenia firmy: warsztaty, degustacje, wieczory. Scope catalog:read. Wymaga modułu Wydarzenia: wyłączony → 403 module_disabled, nieodczytalny → 503 module_unavailable. Opcjonalne ?locale=.
Odpowiedź 200: { "events": [...], "locale": "pl", "generatedAt": "..." }. Lista zawiera tylko wydarzenia opublikowane, które mają nadchodzący termin, w kolejności sort, a potem najbliższego terminu. Każde wydarzenie:
| Pole | Znaczenie |
|---|---|
id, slug, name, description, audience, scheduleNote, imageUrl | opis wydarzenia; description to zwykły tekst, linie - to punkty |
priceMode, pricePerPersonGrosze | per_person, free albo on_request; cena tylko przy per_person |
participants | { min, max } na jedno zgłoszenie |
scheduleMode, occurrences | dated albo on_request („termin do uzgodnienia”, pusta lista occurrences); terminy { id, date, time, endTime, note } |
booking, phone | inquiry albo phone; telefon wydarzenia albo firmy. Przy phone pokaż numer: API odrzuca zapisy (event_phone_only) |
Odpowiedź zawiera nagłówek x-delieta-cache-tag (znacznik tenant:{id}:events, odświeżany co 5 minut), a filtr czasu działa po cache, więc minione terminy znikają bez czekania.
POST /api/v1/inquiries
Zapytanie gościa: kontakt, catering albo wydarzenie. Scope checkout:create (klucz publikowalny), więc wywołuj z serwera sklepu, nie z przeglądarki. Limit jest per odwiedzający (przekaż jego adres w x-delieta-visitor). Pole website to pułapka na boty: zostaw puste. Wymagany moduł zależy od rodzaju: catering i contact wymagają modułu catering, event modułu Wydarzenia. Gdy wymagany moduł jest wyłączony, odpowiedź to 403 module_disabled. Body ponad 64 KiB → 413 payload_too_large. Nieznane pole → 400 invalid_request.
Pola: kind (contact, catering albo event), name, phone i/lub email (jedno z nich jest obowiązkowe, przy cateringu phone), message (wymagana przy contact), topic? (reservation, catering, order, other), locale? oraz, dla catering, obiekt catering: formatId?, headcount, eventDate (RRRR-MM-DD), eventTime? (GG:MM), deliveryAddress?, dietary? (vegetarian, gluten_free, lactose_free), dietaryNote?, company?, nip?, venue? (onsite albo delivery) i addOns? ([{ key, quantity }], do 30 pozycji, quantity 1–99, każdy key raz). Body nigdy nie zawiera ceny ani nazwy dodatku: te pochodzą z formatu. Dodatki wolno wysłać tylko z formatId.
Dla event pola to obiekt event: eventId, occurrenceId? (termin z GET /events), preferredDate? i preferredTime? (dla wydarzenia „Do uzgodnienia”; preferredDate jest wtedy wymagane), participants i organization?. phone jest wymagany, message opcjonalna. Body jest ścisłe: nieznane pole to 400.
POST /api/v1/inquiries
Authorization: Bearer dlt_live_…
Content-Type: application/json
{
"kind": "catering",
"name": "Anna Nowak",
"phone": "+48600100200",
"catering": { "formatId": "f1", "headcount": 40, "eventDate": "2026-12-12", "company": "Acme", "nip": "5260250995" }
}
Sukces: 201 { "inquiry": { "reference": "AB12CD", "status": "new" } }. Zły NIP (suma kontrolna), brak telefonu lub nieznane pole → 400 invalid_request z listą errors. Nieznany format → 422 unprocessable. Zapytanie z obsługą na miejscu na dzień, w którym lokal ma dzień zamknięty → 422 venue_closed („W tym dniu lokal jest zamknięty.”).
Pakiety okolicznościowe. catering.venue (onsite albo delivery) mówi, gdzie podać; format onsite lub delivery ustala to sam, format both bierze wybór gościa. Bez catering.venue format z dowozem (delivery albo both) zapisuje się jako dowóz, a format tylko onsite dostaje 422 venue_not_offered (nigdy nie przepisujemy go po cichu). catering.addOns to płatne dodatki wybranego formatu: [{ "key": "nuggetsy", "quantity": 2 }], ilość 1–99, każdy klucz raz, tylko z formatId. Body nigdy nie zawiera ceny ani nazwy: obie bierzemy z zapisanego formatu. Odmowy pakietu to 422 unprocessable z kodem w errors[].code:
headcount_out_of_range— liczba osób poza zakresem pakietu („Ten pakiet jest dla co najmniej 15 osób.”, „… dla maksymalnie 50 osób.”). Minimum formatu wygrywa nawet poniżej minimum sklepu; maksimum sklepu i twarde 1–500 zawsze obowiązują. Zwykły format z dowozem (delivery,from, bezmaxHeadcount) odpowiada jak przed pakietami:400invalid_requestnacatering.headcount(„Podaj liczbę osób od 5 do 500.”).venue_not_offered—catering.venue, którego format nie obsługuje, albo brakcatering.venueprzy formacie tylkoonsite.unknown_add_on—catering.addOns.N.keyspoza formatu.venue_closed— przyjęcie w lokalu w dniu zamknięcia („W tym dniu lokal jest zamknięty.”).event_in_past— termin już minął.
Zapisy na wydarzenia (kind: "event")
Zapis na wydarzenie lokalu (moduł Wydarzenia) to też zapytanie: kind: "event" z blokiem event. Telefon jest wymagany. Wydarzenie z terminami bierze occurrenceId (jeden z terminów z GET /api/v1/events); wydarzenie „termin do uzgodnienia” bierze preferredDate (i opcjonalnie preferredTime). Gdy firma ma wyłączony moduł Wydarzenia, odpowiedź to 403 module_disabled.
{
"kind": "event",
"name": "Anna Nowak",
"phone": "+48600100200",
"event": { "eventId": "e1", "occurrenceId": "o1", "participants": 4, "organization": "Klub książki" }
}
Odmowy to 422 unprocessable z kodem w errors[].code i polskim opisem przy polu: unknown_event, event_not_bookable, event_past, event_phone_only, unknown_occurrence, event_in_past, preferred_date_required, participants_out_of_range.
POST /api/v1/catering-orders
Firma zapisuje zlecenie cateringowe ze swojego systemu. Scope catering:write (tylko klucz sekretny). Delieta zapisuje sprzedaż: w body jest uzgodniona kwota w groszach (agreedAmountGrosze); płatność jest opcjonalna (paid: true oznacza opłacone). Pozycje (lines) wskazują produkty firmy przez menuItemId; cudzy albo nieznany id → 422 unprocessable. Zlecenie ma source: api. Opcjonalny Idempotency-Key: ponowienie zwraca pierwsze zlecenie (200, Idempotent-Replayed: true). Moduł wyłączony → 403 module_disabled. Zajęte powiązanie z zapytaniem → 409 conflict. Body ponad 64 KiB → 413 payload_too_large.
Pola: customer (name, phone?, email?, company?, nip?), eventDate (RRRR-MM-DD), eventTime?, headcount?, deliveryAddress?, formatId?, lines[] (menuItemId, quantity, unitPriceGrosze?), agreedAmountGrosze, note?, status? (planned, confirmed, delivered, cancelled), paid?, inquiryId?.
{
"customer": { "name": "Acme sp. z o.o.", "nip": "5260250995" },
"eventDate": "2026-12-12",
"headcount": 40,
"lines": [{ "menuItemId": "i1", "quantity": 40 }],
"agreedAmountGrosze": 240000
}
Sukces: 201 { "order": { "id", "reference", "status": "planned", "source": "api", "paymentStatus": "unpaid", … } }. paymentStatus to unpaid albo paid.
GET /api/v1/catering-orders
Formaty cateringowe firmy (z listą produktów) są w odpowiedzi konfiguracji sklepu, tylko gdy moduł jest włączony. Pole cateringPackages zawiera wszystkie aktywne formaty, z pakietami w lokalu i ceną za osobę; runtime 1.8 czyta je pierwsze. Pole cateringFormats (runtime 1.7) zawiera tylko formaty z dowozem (venue delivery albo both) z ceną from, czyli te, które starszy sklep sprzeda poprawnie. Każdy format ma imageUrl (adres zdjęcia albo null). Od runtime 1.8.0 format ma też slug (adres w sklepie), venue, priceBasis (from „od 45 zł”, per_person „150 zł/os.”, from_per_person „od 150 zł/os.”), a pakiet dodatkowo maxHeadcount, sections, included („W cenie”), terms („Warto wiedzieć”) i addOns (pomijane, gdy puste). Danie wpisane tekstem ma productId: "".
Formaty cateringowe firmy są w odpowiedzi konfiguracji sklepu, tylko gdy moduł jest włączony: cateringFormats (formaty z dostawą i ceną „od”) oraz cateringPackages (wszystkie formaty z polami pakietu). Każdy format ma imageUrl (adres zdjęcia albo null).
OpenAPI
GET /api/v1/openapi
Publiczny dokument OpenAPI 3.1, bez klucza. Generuj z niego klienta, gdy nie chcesz ręcznego curl. Ta strona jest utrzymywana w zgodzie z nim — gdy się różnią, rację ma OpenAPI i test w repozytorium.
Nie ma operacji webhook od Delieta o statusie zamówienia. Status odczytujesz tutaj, przez GET. Jedyną komunikację w drugą stronę (od Delieta do sklepu) opisuje Webhooki.

