DelietaDokumentacja

Partner API

HTTP /api/v1 — menu, checkout, wycena ETA, tworzenie i odczyt zamówienia. Kontrakt z OpenAPI 3.1.

Partner API v1 to powierzchnia HTTP pod https://delieta.pl/api/v1. Klucz API, który Delieta wystawiła jednej firmie, rozstrzyga najemcę. Opublikowane operacje to GET /menu, POST /checkout, POST /quotes, POST /orders i GET /orders/{id}. Maszynowy opis: GET /api/v1/openapi (OpenAPI 3.1, bez klucza).

flowchart LR
  bearer["Authorization Bearer"] --> 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

Nagłówek:

Authorization: Bearer dlt_live_…

Brak albo zły klucz → 401 unauthorized.

ScopeOperacja
catalog:readGET /api/v1/menu (publikowalny). menu:read to alias starszych kluczy.
checkout:createPOST /api/v1/checkout (publikowalny). Nic poza catalog:read + checkout:create.
eta:quotePOST /api/v1/quotes (sekretny)
orders:writePOST /api/v1/orders i GET /api/v1/orders/{id} (sekretny)

Pusta lista scope'ów nie otwiera nic — 403 scope_required. Klucz publikowalny nie może dostać orders:write. Sekretny klucz nie woła checkoutu ze sklepu statycznego.

dlt_test_… autentycznie czyta i cytuje. POST /api/v1/orders odmawia 403 test_key_readonly. Nie ma osobnego zbioru testowego: test czyta produkcyjny katalog firmy.

Błędy

Każda odmowa v1 to application/problem+json. Branchuj po code.

{
  "type": "https://delieta.pl/problems/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "code": "unauthorized",
  "detail": "Provide a valid API key as `Authorization: Bearer dlt_live_…`."
}

Kody, które dokumentuje OpenAPI i których używają trasy:

codestatuskiedy
unauthorized401brak albo zły klucz
scope_required403klucz bez wymaganego scope
test_key_readonly403dlt_test_… próbował zapisu
invalid_request400body nie jest JSON-em albo pada walidacja; szczegóły w errors[]
idempotency_key_required400tworzenie zamówienia bez Idempotency-Key (8–255 znaków)
idempotency_key_conflict409ten sam klucz idempotencji, inne body
rate_limited429budżet na minutę; honoruj Retry-After oraz RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset
not_found404GET /orders/{id} — nie ma takiego zamówienia dla tego klucza (obce id odpowiada identycznie)
tenant_not_found404firma klucza już nie istnieje
venue_not_configured409wycena, a firma nie ma współrzędnych lokalu
shop_closed409godziny otwarcia są ustawione i sklep jest teraz zamknięty
shop_unconfigured409brak zapisanego originu sklepu na URL-e powrotu Stripe
connect_not_ready409konto Connect nie może jeszcze przyjmować płatności
duplicate_external_ref409to externalRef już utworzyło zamówienie tej firmy
line_price_missing / line_name_missing / total_out_of_range400linia albo suma nie da się wycenić
idempotency_unavailable503magazyn idempotencji nie działa; ponów tym samym kluczem

Limity są per klucz, okno 60 s. Zapisy (tworzenie zamówienia, wycena) i odczyty (menu, GET zamówienia) mają osobne budżety, żeby poll nie zjadł wieczoru.

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

Katalog firmy klucza. Scope catalog:read. Pozycje niedostępne są pominięte — w DTO nie ma flagi available do honorowania po stronie klienta. Kształt jest zamrożony.

GET /api/v1/menu
Authorization: Bearer dlt_live_…
{
  "currency": "PLN",
  "generatedAt": "2026-08-26T12:00:00.000Z",
  "categories": [
    { "id": "c1", "slug": "pizza", "name": "Pizza", "sort": 0 }
  ],
  "items": [
    {
      "id": "i1",
      "slug": "margherita",
      "categoryId": "c1",
      "name": "Margherita",
      "description": null,
      "basePriceGrosze": 3200,
      "allergens": [],
      "modifierGroups": [],
      "sort": 0
    }
  ]
}

Pusta tablica kategorii i pozycji jest poprawną odpowiedzią, nie błędem. Cache-Control: no-store.

POST /api/v1/checkout

Koszyk sklepu. Scope checkout:create (klucz publikowalny). Klient wysyła co, nigdy ile: { items: [{ itemId, quantity }] }. Ceny, dopłaty i suma liczy Delieta z katalogu firmy klucza, w całkowitych groszach PLN. Pole unitPrice / total / companyId w body jest ignorowane. URL sukcesu i anulowania bierze się z konfiguracji sklepu na najemcy, nigdy z requestu.

POST /api/v1/checkout
Authorization: Bearer dlt_live_…
Content-Type: application/json
Idempotency-Key: chk-2026-08-26-42
{ "items": [{ "itemId": "i1", "quantity": 2 }] }

201

{
  "url": "https://checkout.stripe.com/c/pay/cs_test_…",
  "totalGrosze": 6400,
  "currency": "PLN",
  "pricingSource": "catalog"
}

Przekieruj gościa na url. Nieznana albo niedostępna pozycja → 422 unprocessable. Zamknięte godziny → 409 shop_closed. Brak originu sklepu → 409 shop_unconfigured. Konto Connect nieprzygotowane → 409 connect_not_ready (nie ma fałszywej płatności).

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ż obu krańców. Jedna liczba w UI partnera kłamie o kontrakcie. Brak współrzędnych lokalu → 409 venue_not_configured. Złe body → 400 invalid_request.

POST /api/v1/orders

Tworzy zamówienie firmy klucza. Scope orders:write. Serwer sumuje linie i odrzuca podaną przez Ciebie sumę. Dziś ceny jednostkowe mogą pochodzić od wołającego (pricingSource: "caller"); gdy wycena idzie z katalogu, odpowiedź powie "catalog".

Idempotency-Key

Nagłówek obowiązkowy, 8–255 znaków po trimie. Jeden klucz na logiczną próbę. Ponów ten sam klucz i to samo body po błędzie sieci albo 503 idempotency_unavailable. Udany 201 jest zamrażany: powtórka zwraca tę samą kopertę i Idempotent-Replayed: true. Ten sam klucz, inne body → 409 idempotency_key_conflict.

Body

Nie ma pola najemcy. Typowe wywołanie z ceną od wołają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"
}
PoleWymaganeUwagi
customerNametak1–200 znaków
customerPhonetak1–40 znaków
destinationAddresstak1–500 znaków
destinationPointnie{ lat, lng } WGS84
itemstak1–50 linii; qty 1–99
items[].name / items[].unitPriceprzy wycenie wołającegogrosze; serwer przelicza totalGrosze
items[].menuItemIdnieid z katalogu, gdy wyceniasz z menu
paymentMethodniecash | card | online (domyślnie cash)
channelniephone | web | pos | integration (domyślnie web)
externalRefniededup po Twojej stronie; kolizja → duplicate_external_ref
POST /api/v1/orders
Authorization: Bearer dlt_live_…
Content-Type: application/json
Idempotency-Key: ord-2026-08-26-storefront-42

201

{
  "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 oddajesz klientowi. To jedyny publiczny URL śledzenia.

GET /api/v1/orders/

Odczyt jednego zamówienia, które firma klucza posiada. Scope jest ten sam: orders:write — zestaw zakresów jest zamknięty i już wydany na żywych kluczach; nowy orders:read złamałby istniejące integracje. Poll idzie w budżet odczytów, nie tworzenia.

id to order.id z odpowiedzi 201. Obce albo nieistniejące id → 404 not_found, bez wyroczni istnienia.

Odpowiedź jest węższa niż koperta tworzenia. Partner dostaje cykl życia, 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: pending_acceptance, accepted, preparing, on_the_way, delivered, rejected.

OpenAPI

GET /api/v1/openapi

Publiczny dokument OpenAPI 3.1. Generuj z niego klienta, gdy nie chcesz ręcznego curl. Ta strona jest utrzymywana w zgodzie z nim — jeśli się rozjeżdżają, wygrywa OpenAPI i test w repozytorium.

Nie ma operacji webhook. Status czytasz tutaj, GET-em. Zobacz Webhooki.