Partner-API
HTTP /api/v1 — Menü, Kasse, ETA-Zitat, Bestellung anlegen und lesen. Vertrag aus OpenAPI 3.1.
Partner-API v1 ist die HTTP-Oberfläche unter https://delieta.pl/api/v1. Der API-Schlüssel, den Delieta einer Firma ausgestellt hat, entscheidet den Mandanten. Veröffentlichte Operationen sind GET /menu, POST /checkout, POST /quotes, POST /orders and GET /orders/{id}. Maschinenbeschreibung: GET /api/v1/openapi (OpenAPI 3.1, no key).
flowchart LR
bearer["Authorization Bearer"] --> verify["Der Schlüssel ist die Firma"]
verify --> scope{"scope"}
scope -->|"catalog:read"| menu["GET /menu"]
scope -->|"checkout:create"| checkout["POST /checkout"]
scope -->|"orders:write"| handoff["POST /orders"]
handoff --> same["dieselbe Bestellung wie das Panel"]Schlüssel und Scopes
Header:
Authorization: Bearer dlt_live_…
Fehlender oder schlechter Schlüssel → 401 unauthorized.
| Scope | Operation |
|---|---|
catalog:read | GET /api/v1/menu (veröffentlichbar). menu:read ist ein Alias älterer Schlüssel. |
checkout:create | POST /api/v1/checkout (veröffentlichbar). Nothing beyond catalog:read + checkout:create. |
eta:quote | POST /api/v1/quotes (geheim) |
orders:write | POST /api/v1/orders and GET /api/v1/orders/{id} (geheim) |
Eine leere Scope-Liste öffnet nichts — 403 scope_required. Ein veröffentlichbarer Schlüssel kann orders:write nicht bekommen. Ein geheimer Schlüssel ruft kein Checkout aus einem statischen Shop.
dlt_test_… liest und zitiert authentisch. POST /api/v1/orders lehnt ab 403 test_key_readonly. Es gibt keinen separaten Testdatensatz: Test liest den Produktionskatalog der Firma.
Fehler
Jede v1-Ablehnung ist application/problem+json. Verzweige nach code.
{
"type": "https://delieta.pl/problems/unauthorized",
"title": "Unauthorized",
"status": 401,
"code": "unauthorized",
"detail": "Provide a valid API key as `Authorization: Bearer dlt_live_…`."
}
Codes, die OpenAPI dokumentiert und die Routen nutzen:
| code | status | wann |
|---|---|---|
unauthorized | 401 | fehlender oder schlechter Schlüssel |
scope_required | 403 | Schlüssel ohne nötigen Scope |
test_key_readonly | 403 | dlt_test_… hat einen Schreibversuch gemacht |
invalid_request | 400 | Body ist kein JSON oder Validierung fällt; Details in errors[] |
idempotency_key_required | 400 | Bestellung ohne Idempotency-Key anlegen (8–255 Zeichen) |
idempotency_key_conflict | 409 | gleicher Idempotenzschlüssel, anderer Body |
rate_limited | 429 | Budget pro Minute; ehre Retry-After and RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset |
not_found | 404 | GET /orders/{id} — keine solche Bestellung für diesen Schlüssel (fremde id antwortet identisch) |
tenant_not_found | 404 | die Firma des Schlüssels existiert nicht mehr |
venue_not_configured | 409 | ein Zitat, und die Firma hat keine Standortkoordinaten |
shop_closed | 409 | Öffnungszeiten sind gesetzt und der Shop ist jetzt zu |
shop_unconfigured | 409 | kein gespeicherter Shop-Origin für Stripe-Rückkehr-URLs |
connect_not_ready | 409 | das Connect-Konto kann noch keine Zahlungen annehmen |
duplicate_external_ref | 409 | dieses externalRef hat schon eine Bestellung dieser Firma angelegt |
line_price_missing / line_name_missing / total_out_of_range | 400 | eine Zeile oder die Summe lässt sich nicht preisen |
idempotency_unavailable | 503 | der Idempotenzspeicher ist unten; wiederhole mit dem selben Schlüssel |
Limits sind pro Schlüssel, 60-s-Fenster. Schreiben (Bestellung anlegen, Zitat) und Lesen (Menü, GET Bestellung) haben getrennte Budgets, damit Polling den Abend nicht frisst.
Geld
Nur ganze Groszy. 3000 is 30.00 PLN. Keine Floats, kein Währungsfeld auf Beträgen. currency in Antworten ist die Konstante "PLN".
GET /api/v1/menu
Katalog der Schlüsselfirma. Scope catalog:read. Nicht verfügbare Positionen sind ausgelassen — das DTO hat keine available-Flagge für den Client. Die Form ist eingefroren.
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
}
]
}
Leere Kategorie- und Positionsarrays sind eine gültige Antwort, kein Fehler. Cache-Control: no-store.
POST /api/v1/checkout
Der Shop-Warenkorb. Scope checkout:create (publishable key). Der Kunde sendet was, nie wie viel: { items: [{ itemId, quantity }] }. Preise, Zuschläge und die Summe berechnet Delieta aus dem Katalog der Schlüsselfirma, in ganzen Groszy PLN. Ein Feld unitPrice / total / companyId im Body wird ignoriert. Erfolgs- und Abbruch-URLs kommen aus der Shop-Konfiguration am Mandanten, nie aus dem Request.
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"
}
Leite den Gast auf url um. Unbekannte oder nicht verfügbare Position → 422 unprocessable. Geschlossene Stunden → 409 shop_closed. Fehlender Shop-Origin → 409 shop_unconfigured. Connect-Konto nicht bereit → 409 connect_not_ready (keine Scheinzahlung).
POST /api/v1/quotes
Eine Fahrt-Schätzung bevor eine Bestellung existiert. Scope eta:quote. Start ist der Firmenstandort; du sendest nur das Ziel. Das Ergebnis ist immer eine Spanne (minMinutes / maxMinutes). Das Feld guaranteed ist die Konstante false — das ist kein SLA. Nach expiresAt erneut fragen.
{ "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"
}
}
Zeige beide Enden. Eine Zahl in der Partner-UI lügt über den Vertrag. Fehlende Standortkoordinaten → 409 venue_not_configured. Schlechter Body → 400 invalid_request.
POST /api/v1/orders
Legt eine Bestellung der Schlüsselfirma an. Scope orders:write. Der Server summiert die Zeilen und lehnt eine von dir gesendete Summe ab. Heute können Stückpreise vom Aufrufer kommen (pricingSource: "caller"); wenn die Preisbildung aus dem Katalog kommt, sagt die Antwort "catalog".
Idempotency-Key
Pflicht-Header, 8–255 Zeichen nach Trim. Ein Schlüssel pro logischem Versuch. Wiederhole denselben Schlüssel und denselben Body nach Netzwerkfehler oder 503 idempotency_unavailable. Ein erfolgreiches 201 ist eingefroren: eine Wiederholung gibt dieselbe Hülle und Idempotent-Replayed: true. Gleicher Schlüssel, anderer Body → 409 idempotency_key_conflict.
Body
Es gibt kein Mandantenfeld. Ein typischer Aufruf mit Aufrufer-Preisbildung:
{
"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"
}
| Feld | Pflicht | Hinweise |
|---|---|---|
customerName | ja | 1–200 Zeichen |
customerPhone | ja | 1–40 Zeichen |
destinationAddress | ja | 1–500 Zeichen |
destinationPoint | nein | { lat, lng } WGS84 |
items | ja | 1–50 Zeilen; qty 1–99 |
items[].name / items[].unitPrice | with caller pricing | Groszy; der Server rechnet totalGrosze neu |
items[].menuItemId | nein | Katalog-id, wenn du aus dem Menü preist |
paymentMethod | nein | cash | card | online (Standard cash) |
channel | nein | phone | web | pos | integration (Standard web) |
externalRef | nein | Dedup auf deiner Seite; Kollision → 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 ist accepted oder pending_acceptance, je nach Annahmemodus der Firma. trackingUrl gibst du dem Kunden. Das ist die einzige öffentliche Tracking-URL.
GET /api/v1/orders/
Liest eine Bestellung, die die Schlüsselfirma besitzt. Der Scope ist derselbe: orders:write — das Scope-Set ist geschlossen und schon auf Live-Schlüsseln ausgegeben; ein neues orders:read würde bestehende Integrationen brechen. Polling verbraucht das Lese-Budget, nicht das Anlege-Budget.
id ist order.id aus der 201-Antwort. Eine fremde oder fehlende id → 404 not_found, ohne Existenzorakel.
Die Antwort ist schmaler als die Anlege-Hülle. Der Partner bekommt den Lebenszyklus, keine internen Spalten:
{
"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 and acceptanceDeadlineAt sind Unix-ms oder null. Status, die du siehst: pending_acceptance, accepted, preparing, on_the_way, delivered, rejected.
OpenAPI
GET /api/v1/openapi
Öffentliches OpenAPI-3.1-Dokument. Erzeuge daraus einen Client, wenn du kein handgeschriebenes curl willst. Diese Seite wird in Übereinstimmung gehalten — wenn sie driften, gewinnen OpenAPI und der Repo-Test.
Es gibt keine Webhook-Operation. Status liest du hier, per GET. Siehe Webhooks.

