Partner API
HTTP /api/v1 — menu, checkout, ETA quote, create and read an order. Contract from OpenAPI 3.1.
Partner API v1 is the HTTP surface under https://delieta.pl/api/v1. The API key Delieta issued to one company decides the tenant. Published operations are GET /menu, POST /checkout, POST /quotes, POST /orders and GET /orders/{id}. Machine description: GET /api/v1/openapi (OpenAPI 3.1, no key).
flowchart LR
bearer["Authorization Bearer"] --> verify["The key is the company"]
verify --> scope{"scope"}
scope -->|"catalog:read"| menu["GET /menu"]
scope -->|"checkout:create"| checkout["POST /checkout"]
scope -->|"orders:write"| handoff["POST /orders"]
handoff --> same["the same order as the panel"]Key and scopes
Header:
Authorization: Bearer dlt_live_…
Missing or bad key → 401 unauthorized.
| Scope | Operation |
|---|---|
catalog:read | GET /api/v1/menu (publishable). menu:read is an alias for older keys. |
checkout:create | POST /api/v1/checkout (publishable). Nothing beyond catalog:read + checkout:create. |
eta:quote | POST /api/v1/quotes (secret) |
orders:write | POST /api/v1/orders and GET /api/v1/orders/{id} (secret) |
An empty scope list opens nothing — 403 scope_required. A publishable key cannot get orders:write. A secret key does not call checkout from a static shop.
dlt_test_… authentically reads and quotes. POST /api/v1/orders refuses 403 test_key_readonly. There is no separate test dataset: test reads the company’s production catalog.
Errors
Every v1 refusal is application/problem+json. Branch on 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 OpenAPI documents and the routes use:
| code | status | when |
|---|---|---|
unauthorized | 401 | missing or bad key |
scope_required | 403 | key without the required scope |
test_key_readonly | 403 | dlt_test_… tried a write |
invalid_request | 400 | body is not JSON or validation fails; details in errors[] |
idempotency_key_required | 400 | creating an order without Idempotency-Key (8–255 chars) |
idempotency_key_conflict | 409 | same idempotency key, different body |
rate_limited | 429 | per-minute budget; honour Retry-After and RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset |
not_found | 404 | GET /orders/{id} — no such order for this key (a foreign id answers identically) |
tenant_not_found | 404 | the key’s company no longer exists |
venue_not_configured | 409 | a quote, and the company has no venue coordinates |
shop_closed | 409 | opening hours are set and the shop is closed now |
shop_unconfigured | 409 | no saved shop origin for Stripe return URLs |
connect_not_ready | 409 | the Connect account cannot take payments yet |
duplicate_external_ref | 409 | this externalRef already created an order for this company |
line_price_missing / line_name_missing / total_out_of_range | 400 | a line or the total cannot be priced |
idempotency_unavailable | 503 | the idempotency store is down; retry with the same key |
Limits are per key, 60 s window. Writes (create order, quote) and reads (menu, GET order) have separate budgets so polling does not eat the evening.
Money
Whole grosz only. 3000 is 30.00 PLN. No floats, no currency field on amounts. currency in responses is the constant "PLN".
GET /api/v1/menu
Catalog of the key’s company. Scope catalog:read. Unavailable items are omitted — the DTO has no available flag for the client to honour. The shape is frozen.
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
}
]
}
Empty category and item arrays are a valid response, not an error. Cache-Control: no-store.
POST /api/v1/checkout
The shop cart. Scope checkout:create (publishable key). The customer sends what, never how much: { items: [{ itemId, quantity }] }. Prices, surcharges and the total are computed by Delieta from the key’s company catalog, in whole grosz PLN. A unitPrice / total / companyId field in the body is ignored. Success and cancel URLs come from the shop config on the tenant, never from the 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"
}
Redirect the guest to url. Unknown or unavailable item → 422 unprocessable. Closed hours → 409 shop_closed. Missing shop origin → 409 shop_unconfigured. Connect account not ready → 409 connect_not_ready (no fake payment).
POST /api/v1/quotes
A travel estimate before an order exists. Scope eta:quote. Start is the company venue; you only send the destination. The result is always a range (minMinutes / maxMinutes). The guaranteed field is the constant false — this is not an SLA. After expiresAt ask again.
{ "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"
}
}
Show both ends. One number in the partner UI lies about the contract. Missing venue coordinates → 409 venue_not_configured. Bad body → 400 invalid_request.
POST /api/v1/orders
Creates an order for the key’s company. Scope orders:write. The server sums the lines and rejects a total you sent. Today unit prices may come from the caller (pricingSource: "caller"); when pricing comes from the catalog, the response says "catalog".
Idempotency-Key
Required header, 8–255 characters after trim. One key per logical attempt. Retry the same key and the same body after a network error or 503 idempotency_unavailable. A successful 201 is frozen: a repeat returns the same envelope and Idempotent-Replayed: true. Same key, different body → 409 idempotency_key_conflict.
Body
There is no tenant field. A typical call with caller pricing:
{
"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"
}
| Field | Required | Notes |
|---|---|---|
customerName | yes | 1–200 characters |
customerPhone | yes | 1–40 characters |
destinationAddress | yes | 1–500 characters |
destinationPoint | no | { lat, lng } WGS84 |
items | yes | 1–50 lines; qty 1–99 |
items[].name / items[].unitPrice | with caller pricing | grosz; the server recomputes totalGrosze |
items[].menuItemId | no | catalog id when you price from the menu |
paymentMethod | no | cash | card | online (default cash) |
channel | no | phone | web | pos | integration (default web) |
externalRef | no | your-side dedup; collision → 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 is accepted or pending_acceptance, depending on the company’s acceptance mode. trackingUrl is what you give the customer. That is the only public tracking URL.
GET /api/v1/orders/
Read one order the key’s company owns. The scope is the same: orders:write — the scope set is closed and already issued on live keys; a new orders:read would break existing integrations. Polling spends the read budget, not create.
id is order.id from the 201 response. A foreign or missing id → 404 not_found, with no existence oracle.
The response is narrower than the create envelope. The partner gets the life cycle, not internal columns:
{
"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 are unix ms or null. Statuses you will see: pending_acceptance, accepted, preparing, on_the_way, delivered, rejected.
OpenAPI
GET /api/v1/openapi
Public OpenAPI 3.1 document. Generate a client from it when you do not want hand-written curl. This page is kept in agreement with it — if they drift, OpenAPI and the repo test win.
There is no webhook operation. You read status here, with GET. See Webhooks.

