DelietaDokumentation

Maschinell übersetzt. Polnisch ist die kanonische Fassung.

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.

ScopeOperation
catalog:readGET /api/v1/menu (veröffentlichbar). menu:read ist ein Alias älterer Schlüssel.
checkout:createPOST /api/v1/checkout (veröffentlichbar). Nothing beyond catalog:read + checkout:create.
eta:quotePOST /api/v1/quotes (geheim)
orders:writePOST /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:

codestatuswann
unauthorized401fehlender oder schlechter Schlüssel
scope_required403Schlüssel ohne nötigen Scope
test_key_readonly403dlt_test_… hat einen Schreibversuch gemacht
invalid_request400Body ist kein JSON oder Validierung fällt; Details in errors[]
idempotency_key_required400Bestellung ohne Idempotency-Key anlegen (8–255 Zeichen)
idempotency_key_conflict409gleicher Idempotenzschlüssel, anderer Body
rate_limited429Budget pro Minute; ehre Retry-After and RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset
not_found404GET /orders/{id} — keine solche Bestellung für diesen Schlüssel (fremde id antwortet identisch)
tenant_not_found404die Firma des Schlüssels existiert nicht mehr
venue_not_configured409ein Zitat, und die Firma hat keine Standortkoordinaten
shop_closed409Öffnungszeiten sind gesetzt und der Shop ist jetzt zu
shop_unconfigured409kein gespeicherter Shop-Origin für Stripe-Rückkehr-URLs
connect_not_ready409das Connect-Konto kann noch keine Zahlungen annehmen
duplicate_external_ref409dieses externalRef hat schon eine Bestellung dieser Firma angelegt
line_price_missing / line_name_missing / total_out_of_range400eine Zeile oder die Summe lässt sich nicht preisen
idempotency_unavailable503der 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"
}
FeldPflichtHinweise
customerNameja1–200 Zeichen
customerPhoneja1–40 Zeichen
destinationAddressja1–500 Zeichen
destinationPointnein{ lat, lng } WGS84
itemsja1–50 Zeilen; qty 1–99
items[].name / items[].unitPricewith caller pricingGroszy; der Server rechnet totalGrosze neu
items[].menuItemIdneinKatalog-id, wenn du aus dem Menü preist
paymentMethodneincash | card | online (Standard cash)
channelneinphone | web | pos | integration (Standard web)
externalRefneinDedup 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.