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.
| Scope | Operacja |
|---|---|
catalog:read | GET /api/v1/menu (publikowalny). menu:read to alias starszych kluczy. |
checkout:create | POST /api/v1/checkout (publikowalny). Nic poza catalog:read + checkout:create. |
eta:quote | POST /api/v1/quotes (sekretny) |
orders:write | POST /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:
| 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 |
invalid_request | 400 | body nie jest JSON-em albo pada walidacja; szczegóły w errors[] |
idempotency_key_required | 400 | tworzenie zamówienia bez Idempotency-Key (8–255 znaków) |
idempotency_key_conflict | 409 | ten sam klucz idempotencji, inne body |
rate_limited | 429 | budżet na minutę; honoruj Retry-After oraz RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset |
not_found | 404 | GET /orders/{id} — nie ma takiego zamówienia dla tego klucza (obce id odpowiada identycznie) |
tenant_not_found | 404 | firma klucza już nie istnieje |
venue_not_configured | 409 | wycena, a firma nie ma współrzędnych lokalu |
shop_closed | 409 | godziny otwarcia są ustawione i sklep jest teraz zamknięty |
shop_unconfigured | 409 | brak zapisanego originu sklepu na URL-e powrotu Stripe |
connect_not_ready | 409 | konto Connect nie może jeszcze przyjmować płatności |
duplicate_external_ref | 409 | to externalRef już utworzyło zamówienie tej firmy |
line_price_missing / line_name_missing / total_out_of_range | 400 | linia albo suma nie da się wycenić |
idempotency_unavailable | 503 | magazyn 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"
}
| 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 |
items | tak | 1–50 linii; qty 1–99 |
items[].name / items[].unitPrice | przy wycenie wołającego | grosze; serwer przelicza totalGrosze |
items[].menuItemId | nie | id z katalogu, gdy wyceniasz z menu |
paymentMethod | nie | cash | card | online (domyślnie cash) |
channel | nie | phone | web | pos | integration (domyślnie web) |
externalRef | nie | dedup 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.

