机器翻译。波兰语为规范文档。

Partner API

HTTP /api/v1 — 菜单、店铺配置、结账、时段、预订、咨询、餐饮、ETA 报价与订单。一份 API 说明,与 OpenAPI 3.1 一致。

Partner API v1 是位于 https://delieta.pl/api/v1 的 HTTP 接口。Delieta 签发给某家公司的 API 密钥就代表这家公司:任何请求都没有公司字段。这是唯一面向人的说明;面向机器的描述是 GET /api/v1/openapi(OpenAPI 3.1,无需密钥)。

已发布的操作:GET /menu、GET /shop-config、POST /shop-heartbeat、GET /checkout/slots、POST /checkout、POST /checkout/quote、GET /checkout/result、GET /events、POST /quotes、POST /orders、GET /orders/{id}、GET /orders/{id}/status、GET /floor-plan、GET /reservations/availability、POST /reservations、POST /inquiries、POST /catering-orders、GET /catering-orders 和 GET /catalog-images/{id}/{filename}。

flowchart LR
  bearer["Klucz API"] --> 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"]

密钥与权限范围

两种等价的请求头,先找到的生效;查询字符串里的密钥一律不接受。

Authorization: Bearer dlt_live_…
x-delieta-api-key: dlt_live_…

检查顺序:密钥 → 权限范围 → 测试密钥(仅写操作)→ 限流。缺少或错误的密钥返回 401 unauthorized。

Scope密钥类型操作
catalog:read可发布GET /api/v1/menu、GET /api/v1/shop-config、GET /api/v1/checkout/slots、GET /api/v1/events、GET /api/v1/orders/{id}/status、GET /api/v1/floor-plan、GET /api/v1/reservations/availability 和 POST /api/v1/shop-heartbeat。menu:read 是旧密钥的别名。
checkout:create可发布POST /api/v1/checkout、POST /api/v1/checkout/quote、GET /api/v1/checkout/result、POST /api/v1/inquiries 和 POST /api/v1/reservations(web 预订)。
eta:quote保密POST /api/v1/quotes
orders:write保密POST /api/v1/orders 和 GET /api/v1/orders/{id}
reservations:write保密从公司自己的系统调用 POST /api/v1/reservations(api 预订)
catering:write保密POST /api/v1/catering-orders 和 GET /api/v1/catering-orders

可发布密钥恰好是 catalog:read + checkout:create,仅此而已:店铺持有它,并且只保存在服务端,绝不放进浏览器。可发布密钥不能拥有 orders:write、eta:quote、reservations:write 或 catering:write。空的权限列表什么都打不开 — 403 scope_required。预订按密钥类型计权限:可发布密钥需要 checkout:create,保密密钥需要 reservations:write。

dlt_test_… 读取公司真实的产品并计算报价。所有写操作都会被 403 test_key_readonly 拒绝:POST /api/v1/checkout、/orders、/reservations、/inquiries、/catering-orders 和 /shop-heartbeat。没有单独的测试数据集。

限流

限流按密钥计算,窗口 60 秒。默认每分钟 60 次写 和 600 次读,分开计算,频繁查询不会耗光写入限额。写操作包括创建订单、结账、预订、咨询、餐饮订单和报价,其余为读。超限返回 429 rate_limited;请遵守 Retry-After 以及 RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset。

可发布密钥被许多访客使用,所以它的限流按访客计算。请在 x-delieta-visitor 请求头中传入访客地址;没有该头则按 Origin 计。另有共同上限,为限额的 20 倍。保密密钥忽略此请求头。

错误

v1 的每一次拒绝都是 application/problem+json(RFC 9457)。请根据 code 判断,不要根据文字。

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

例外:GET /api/v1/catalog-images/{id}/{filename} 超限时返回 429 和简单的 { "error": "rate_limited" },不是 problem+json。

codestatus何时出现
unauthorized401缺少或错误的密钥
scope_required403密钥缺少所需的权限范围
test_key_readonly403dlt_test_… 尝试写入
module_disabled403公司的模块已关闭(套餐或店主开关):POST /checkout 与 GET /checkout/slots 的下单、预订、餐饮。已下订单的状态仍可查询
invalid_request400请求体不是 JSON 或校验失败;详情在 errors[](例如 POST /orders 的 unknown_field)
idempotency_key_required400写操作缺少 Idempotency-Key(8–255 个字符)
line_price_missing / line_name_missing / total_out_of_range400订单行或合计无法定价
payload_too_large413咨询或餐饮订单的请求体超过 64 KiB
not_found404此密钥下没有这笔订单(他人的 id 同样回答)
tenant_not_found404密钥对应的公司已不存在
idempotency_key_conflict409相同的幂等键,不同的请求体
conflict409例如该咨询已关联到另一笔订单
venue_not_configured409报价,但公司没有门店坐标
out_of_range409报价或带配送的结账:地址超出公司的配送范围(errors[] 指向 address 字段)。预订中同一个 code 是 422(日期超出范围)
delivery_unconfigured409带配送的结账:门店有公里档位,但没有地址或没有设置配送。自取可用
total_changed409结账:提供的 expectedTotalGrosze 与现在计算的金额不一致。服务器没有用它来定价
product_outside_hours409结账:某商品在所选时间不在供应时间内;errors[] 中的行指向 items.N.itemId
shop_closed409店铺此刻已打烊,不接受“尽快”的订单
shop_unconfigured409没有为支付返回地址保存店铺来源
connect_not_ready409在线支付账户还不能收款
sample_mode409示例数据店铺发起在线支付,而支付处于正式模式
slot_unavailable409requestedFor 不在 GET /checkout/slots 给出的时间之内;预订时:该时间已被占用
table_taken409预订:桌位已被占用(包括刚被别人抢先订走)
fulfilment_unavailable409店铺不提供该履约方式(配送或自取)
payment_unavailable409店铺不接受该支付方式(在线或现金)
duplicate_external_ref409该 externalRef 已为此公司创建过订单
unprocessable422商品未知或不可用(errors[].code:product_unknown、product_unavailable);餐饮套餐未知
below_minimum422购物车低于店铺的最低订单金额(只计菜品,不含包装和配送;字段 items)
product_menu_only422结账:某商品是“Tylko w karcie”(仅在菜单上),不能在线下单;errors[] 中的行指向 items.N.itemId。两种情况同时出现时,以此为准
address_not_found422带配送的结账:无法确定地址(字段 address)
headcount_out_of_range422餐饮询价:人数超出该套餐形式的范围(catering.headcount)
venue_not_offered422餐饮询价:该套餐形式不提供所选地点(catering.venue)
unknown_add_on422餐饮询价:加购项不属于所选套餐形式(catering.addOns.N.key)
unknown_event, event_not_bookable, event_past, event_phone_only422活动询价:没有这场活动、不接受报名、已经举办,或只能电话报名(event.eventId)
unknown_occurrence, event_in_past422活动询价:日期不在列表内或已过去(event.occurrenceId、event.preferredDate)
participants_out_of_range422活动询价:参与人数超出该活动的范围(event.participants)
option_invalid422选项未知、缺少必选组或组内选择过多
validation_failed / party_too_large422预订:内容不合法,人数超过场地容量
rate_limited429超过每分钟限额;见限流
internal500我们这边的故障;请重试
create_failed500POST /orders:订单未能保存;用相同的 Idempotency-Key 重试
idempotency_unavailable503幂等存储不可用;用相同的键重试
delivery_quote_unavailable503带配送的结账或报价:暂时无法计算距离;请重试或选择自取
module_unavailable503无法读取公司的模块(模块并未关闭);稍后重试(Retry-After)

金额

只用整数格罗什(grosze)。3000 即 30.00 PLN。不用浮点数,金额上没有币种字段。响应里的 currency 恒为 "PLN"。

GET /api/v1/menu

密钥所属公司的产品。权限 catalog:read。不可用的商品会被省略 — DTO 里没有需要客户端处理的 available 标志。“Tylko w karcie”(仅在菜单上,orderable: false)的商品不在 items 中而在 menuOnlyItems 中,供应时间放在 availability 里。响应里没有“现在可供应”标志:店铺自己根据 availability 计算。结构不会改变,只会新增字段。

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

类别和商品为空数组是有效响应,不是错误。Cache-Control: no-store。响应带有 x-delieta-cache-tag 响应头;自带缓存的店铺可以用它作键。

可选的 ?locale=(pl、en、de、uk)按字段返回该语言的名称和描述,没有翻译时返回波兰语。公司未启用的语言(或未知语言)按波兰语返回;响应里的 locale 说明实际返回的语言。GET /api/v1/shop-config(餐饮套餐名称;同时返回已启用的 locales,波兰语排第一)和 GET /api/v1/floor-plan(区域名称)读取同一个 ?locale=。

imageUrl 是菜品照片的绝对地址(800 px 版本),没有照片时为 null。featured: true 表示店主把该菜品标记为“推荐”。

orderable: false 表示“Tylko w karcie”(仅展示):显示商品和价格,不显示“Do koszyka”按钮。这类商品不在 items 中,而在 menuOnlyItems 中;所有商品都仅展示的分类在 menuOnlyCategories 中,不在 categories 中——不认识这些键的客户端不会显示它们,也永远不会提供无法下单的商品。@delieta/shop-runtime 从 1.8.0 起会把它们合并回来(withMenuOnly)。商品上的标志已考虑其分类(仅展示的分类 = 其中每个商品)。availability 是供应时间:{ "windows": [{ "days": [1,2,3,4], "from": "12:00", "to": "16:30" }], "fromDate": null, "toDate": null }——最多 7 个时段,天从 0 = 星期一算起,两端都包含,时区 Europe/Warsaw。商品有自己的规则,分类也有;两者同时生效。季节已结束(toDate 已过)的商品根本不会进入菜单。菜单有缓存,所以它不会告诉你商品现在是否可售——请根据规则计算(@delieta/shop-runtime 从 1.8.0 起提供:itemSaleState、liveMenu)。结账按履约时间检查规则:requestedFor,或选择“jak najszybciej”时为现在 + 准备时间。

GET /api/v1/catalog-images//

菜品或餐饮套餐的照片。无需密钥:店铺顾客的浏览器会打开 imageUrl 里的地址,所以不能要求密钥。必须知道 id 和准确的文件名 — 不匹配的组合返回 404。?size=card 返回 800 px 宽的版本(更窄的照片原样返回),不带参数则得到原图。

GET /api/v1/catalog-images/{id}/{filename}?size=card

响应是指向文件存储的 302,或图片字节(image/jpeg、image/png、image/webp 或 image/avif)。照片地址的内容永不改变 — 换了照片就有新地址 — 所以响应带 Cache-Control: public, max-age=31536000, immutable。限制:同一 IP 每分钟 600 次请求(rate_limited)。

GET /api/v1/shop-config

密钥所属公司的店铺品牌形象与规则,与其店铺显示的一致。权限 catalog:read — 与菜单相同,可发布密钥已经具备。店铺自己的品牌设置按字段优先;没有设置时,使用公司的名称、标志和颜色。响应带有 x-delieta-cache-tag。

GET /api/v1/shop-config
Authorization: Bearer dlt_live_…
{
  "name": "Bella Napoli",
  "tagline": "Pizza z pieca opalanego drewnem",
  "brandColor": "#e2574c",
  "logo": "https://delieta.pl/api/v1/catalog-images/{id}/{filename}",
  "banner": "https://delieta.pl/api/v1/catalog-images/{id}/{filename}",
  "icon": "https://delieta.pl/api/v1/catalog-images/{id}/{filename}",
  "currency": "PLN",
  "locale": "pl",
  "locales": ["pl", "en"],
  "layout": "grid",
  "links": {
    "phone": "+48000000000",
    "email": "kontakt@bella.example",
    "instagram": "https://instagram.com/bella.example",
    "facebook": "https://facebook.com/bella.example",
    "address": "Kwiatowa 5, Bydgoszcz",
    "mapUrl": "https://www.google.com/maps/dir/?api=1&destination=Kwiatowa%205%2C%20Bydgoszcz"
  },
  "modules": ["ordering", "reservations", "catering"],
  "ordering": {
    "fulfilmentModes": ["delivery", "pickup"],
    "paymentModes": ["online", "cash"],
    "onlineMethods": ["card", "blik"],
    "deliveryFeeGrosze": 500,
    "minimumOrderGrosze": 4000,
    "prepMinutes": 30
  },
  "hours": {
    "days": [
      { "weekday": 0, "closed": false, "open": "10:00", "close": "22:00" },
      { "weekday": 6, "closed": true, "open": "10:00", "close": "22:00" }
    ]
  },
  "closures": [
    { "from": "2026-12-24", "to": "2026-12-26", "note": "Święta" }
  ],
  "cateringFormats": [
    {
      "id": "f1",
      "name": "Lunch firmowy",
      "description": "Pizza i napoje dla zespołu.",
      "fromPriceGrosze": 3500,
      "minHeadcount": 10,
      "imageUrl": null,
      "items": [
        { "productId": "i1", "name": "Margherita", "quantity": 1, "perPerson": true, "note": null }
      ]
    }
  ],
  "generatedAt": "2026-08-26T12:00:00.000Z"
}
字段含义
banner公司可选的 3:1 横幅,或 null
icon公司的方形图标(至少 512×512,PNG),用作 favicon 和应用图标,或 null:此时店铺沿用自己的图标。Runtime 1.9:shopIcons(config)
locales公司启用的语言,波兰语排第一
layoutgrid 或 list
links只有门店填写过的内容;门店没有自己的地图链接时,mapUrl 根据地址生成(Google Maps 路线)
modules公司已开启的店铺模块:ordering、reservations、catering、events。这里没有的,店铺就不显示
ordering店铺如何接单:fulfilmentModes、paymentModes(只有店铺能刷卡时才有 online;列表可以为空)、onlineMethods(card、blik、p24:Stripe 账户上已启用的支付方式;没有 online 时为空;只展示这些。即使提供 online,也可能为空(Delieta 尚未记录哪些方式已启用):此时显示通用的 «Płatność online»,不要列出任何方式)、deliveryFeeGrosze、minimumOrderGrosze、prepMinutes。订单模块关闭时为 null:此时不要显示结账表单
hours每周营业时间,门店未设置时为 null — 此时不要自己假设营业时间
closures即将到来的休息日(from、to、note 或 null),日期按 Europe/Warsaw。字段缺失表示没有休息日
cateringFormats带商品清单(items)和照片(imageUrl)的餐饮套餐;仅在餐饮模块开启时出现。这里只有 venue 为 delivery 或 both 且 priceBasis 为 from 的套餐 — 即 1.7 版店铺包能正确显示的套餐
cateringPackages所有启用的套餐及其全部套餐字段;仅在餐饮模块开启时出现。字段:slug、venue(onsite、delivery、both)、priceBasis(from、per_person、from_per_person)、minHeadcount/maxHeadcount、sections(带商品或自定义名称的行)、included(„W cenie”)、terms(„Warto wiedzieć”)和 addOns(key、name、amount、unit:szt、kg、g、l、ml 或 porcja、priceGrosze、product?)。1.8 版店铺包优先读取此字段,而不是 cateringFormats

hours.days 恰好有七项,每天一项(weekday 0 = 周一 … 6 = 周日);上面缩短为两项。open 和 close 是门店当地时间的 HH:MM,仅当 closed: false 时有意义。模块、ordering 和 links 每次请求都重新读取。密钥所属公司已不存在 → 404 tenant_not_found。

POST /api/v1/shop-heartbeat

已部署的店铺报告它是用哪个 runtime 构建的。每次构建一次,绝不按请求发送。权限 catalog:read,没有专属权限。写入只涉及该密钥所属公司店铺记录上的集群字段。始终返回 204,即使公司没有店铺记录或请求体无法读取。dlt_test_… 密钥返回 403 test_key_readonly。delieta-shop-assert 会替你发送,你不必自己调用。

POST /api/v1/shop-heartbeat
Authorization: Bearer dlt_live_…
Content-Type: application/json
{ "runtime": "1.6.0", "contract": "v3", "commit": null, "modified": false }

POST /api/v1/checkout

店铺的购物车。权限 checkout:create(可发布密钥)。需要订单模块。Idempotency-Key 为必填(8–255 个字符)。客户端发送买什么,绝不发送多少钱:{ items: [{ itemId, quantity, options? }] },其中 quantity 为 1–99,最多 50 行,options 为 [{ group, option }](菜单里的组键和选项键)。价格、选项加价和合计由 Delieta 根据密钥所属公司的产品计算,单位为整数 PLN 格罗什。请求体里的 unitPrice / total / companyId 字段会被忽略。支付成功和取消后的地址取自该公司的店铺配置,绝不取自请求。

自己收集顾客信息的店铺可额外提供:fulfilment(delivery 或 pickup,默认 delivery)、requestedFor(来自 GET /api/v1/checkout/slots 的时间,带偏移的 ISO 日期;或 null = 尽快)、customer(name、phone、email?)、address(street、number、flat?、格式为 00-000 的 postalCode、city、note?)、invoice(带校验和的 nip、companyName?、address?)、note(最多 500 个字符)、locale 和 payment(blik、card、p24 或 cash)。规则:自取、现金和地址都需要 customer;带 customer 的配送需要 address;自取订单会丢弃地址。可选的 expectedTotalGrosze 是客户端在屏幕上看到的金额。服务器从不据此定价,只是把它与现在计算的金额比较:不一致时,返回 409 total_changed。只有 items 是旧格式:在线支付、配送,由 Stripe 自己询问电话和地址。

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"
}

在线支付返回 url:把顾客重定向到支付页面。顾客付款后订单才会创建;结果请从 GET /api/v1/checkout/result 读取。现金支付(payment: "cash")会立刻返回已下单但尚未付款的订单,等待厨房接单:

{
  "orderShortId": "a1b2c3d4",
  "trackingUrl": "https://delieta.pl/o/a1b2c3d4",
  "totalGrosze": 6400,
  "currency": "PLN",
  "pricingSource": "catalog"
}

把 trackingUrl 交给顾客,并通过 GET /api/v1/orders/{id}/status 轮询状态。现金不需要在线支付账户,但需要 customer。相同的 Idempotency-Key 返回同一笔订单;重试一笔已下的现金订单时,会在检查任何条件之前直接返回该订单。

拒绝情形:订单模块关闭 → 403 module_disabled。商品未知或不可用 → 422 unprocessable(errors[].code:product_unknown、product_unavailable)。选项未知、缺少必选组或选择过多 → 422 option_invalid。购物车低于店铺最低金额 → 422 below_minimum。商品为“Tylko w karcie” → 422 product_menu_only。商品不在供应时间内 → 409 product_outside_hours。配送到范围之外的地址 → 409 out_of_range。找不到的地址 → 422 address_not_found。暂时无法报出配送费 → 503 delivery_quote_unavailable。门店未设置配送 → 409 delivery_unconfigured。金额变了 → 409 total_changed。已打烊且没有“尽快” → 409 shop_closed。时间不在列表内 → 409 slot_unavailable。店铺不提供的履约方式 → 409 fulfilment_unavailable。店铺不接受的支付方式 → 409 payment_unavailable。示例数据店铺在支付正式模式下发起在线支付 → 409 sample_mode(预览只能用测试模式支付)。没有店铺来源 → 409 shop_unconfigured。在线支付账户未就绪 → 409 connect_not_ready(不会有假支付)。

GET /api/v1/checkout/slots

店铺何时接单:“尽快”以及顾客可选的时间。权限 catalog:read。参数 fulfilment 为 delivery(默认)或 pickup。时间根据店铺的营业时间、休息日、备餐时间、步长和时间范围按 Europe/Warsaw 时区计算,以 ISO 时刻返回。结账稍后会检查同样的时间,所以不在列表里的时间会以 409 slot_unavailable 结束。从不缓存。没有设置营业时间时,店铺接受“尽快”,但没有时间网格。

GET /api/v1/checkout/slots?fulfilment=pickup
Authorization: Bearer dlt_live_…
{
  "asap": { "available": true, "readyInMinutes": 30 },
  "slots": [
    { "startsAt": "2026-08-26T08:30:00.000Z" },
    { "startsAt": "2026-08-26T08:45:00.000Z" }
  ]
}

错误的 fulfilment → 400 invalid_request。店铺不提供该履约方式 → 409 fulfilment_unavailable。订单模块关闭 → 403 module_disabled。

GET /api/v1/checkout/result

一笔在线支付的已核实结果,用于店铺的确认页面。权限 checkout:create。传入返回地址里的 session_id(cs_test_… 或 cs_live_…;支付会把它附加到成功地址上)。答案来自 Delieta 中的订单与支付,而不是浏览器:paid、processing(款项在路上,或订单仍在保存;几秒后再问)、unpaid(顾客离开或会话过期)或 unknown(这不是该店铺的结账)。statementDescriptor 是顾客在对账单上看到的文字;不是刷卡支付时为 null。method 为 blik、card、p24、cash、other 或 null。缺少或错误的 session_id → 400 invalid_request。从不缓存。

GET /api/v1/checkout/result?session_id=cs_live_…
Authorization: Bearer dlt_live_…
{
  "status": "paid",
  "orderShortId": "K7M2QX",
  "trackingUrl": "https://bella.delieta.pl/o/K7M2QX",
  "totalGrosze": 5400,
  "merchantName": "Bella Pizza",
  "statementDescriptor": "BELLA* 3F9A12CD",
  "method": "blik",
  "livemode": true
}

POST /api/v1/checkout/quote

不创建订单的购物车报价:菜品、包装、配送和合计。权限 checkout:create(可发布密钥)。请求体:{ items, fulfilment?, address? },与 POST /checkout 相同。店铺不自己计算费用,只显示报价返回的内容。

响应 200:

{
  "quote": {
    "currency": "PLN",
    "itemsGrosze": 4500,
    "packagingGrosze": 200,
    "packaging": [{ "kind": "item", "label": "Opakowanie", "quantity": 2, "unitGrosze": 100, "totalGrosze": 200 }],
    "delivery": { "status": "priced", "feeGrosze": 500, "free": false, "freeFromGrosze": 9000, "missingForFreeGrosze": 4500, "distanceMeters": 3200 },
    "totalGrosze": 5200,
    "minimum": { "minimumOrderGrosze": 3000, "missingGrosze": 0 }
  }
}

delivery.status 为 none(自取)、needs_address(有公里档位而还没有地址)或 priced。priced 时会有 feeGrosze,当价格取决于距离时还有 distanceMeters。fromGrosze 和 toGrosze 字段描述按距离定价时费用的范围。packaging[].kind 字段为 item(按件收费)或 order(按订单收费)。freeFromGrosze 和 missingForFreeGrosze 用于“距免费配送还差……”的提示。需要地址时 totalGrosze 为 null。

低于最低金额时,报价不算错误:差额见 minimum.missingGrosze。错误只在 POST /checkout 中以 below_minimum 出现。报价的拒绝情形:product_menu_only、product_outside_hours、out_of_range、address_not_found、delivery_quote_unavailable、delivery_unconfigured。代码见错误表。定价规则:店铺。

POST /api/v1/quotes

在订单存在之前对行程的估算。权限 eta:quote。起点是公司的门店;你只需给出目的地。结果始终是一个范围(minMinutes / maxMinutes)。guaranteed 字段恒为 false — 这不是 SLA。expiresAt 之后请重新询问。

{ "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"
  }
}

请同时显示区间的两端。合作方界面上只显示一个数字会产生误导。没有门店坐标 → 409 venue_not_configured。目的地在公司配送半径之外 → 409 out_of_range。请求体有误 → 400 invalid_request。密钥所属公司不存在 → 404 tenant_not_found。

POST /api/v1/orders

为密钥所属公司创建订单。权限 orders:write。服务器会自己汇总订单行,并丢弃你传的合计。单价由调用方提供:pricingSource 为 "caller"。这是来自合作方渠道的订单,不是店铺购物车;按目录定价的购物车是 POST /api/v1/checkout。

Idempotency-Key

必填请求头,去除空白后 8–255 个字符。每次逻辑尝试一个键。网络错误、500 create_failed 或 503 idempotency_unavailable 之后,用相同的键和相同的请求体重试。成功的 201 响应会保存 24 小时:重复请求返回同样的响应和 Idempotent-Replayed: true。相同的键、不同的请求体 → 409 idempotency_key_conflict。幂等存储不可用时,你得到 503,而不是重复订单。

请求体

没有公司字段。带调用方价格的典型调用:

{
  "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"
}
字段必填说明
customerName是1–200 个字符
customerPhone是1–40 个字符
destinationAddress是1–500 个字符
destinationPoint否{ lat, lng } WGS84。不要用 destination 或 destinationCoordinates — 那会得到 400 invalid_request,errors[].code 为 unknown_field,因为没有坐标的订单拿不到 ETA
items是1–50 行;qty 1–99
items[].name / items[].unitPrice是格罗什;服务器会重新计算 totalGrosze
items[].menuItemId否产品 id;定价时忽略 options
details否最多 4000 个字符
customerNote否最多 2000 个字符
paymentMethod否cash | card | online(默认 cash)
channel否phone | web | pos | integration(默认 web)
scheduledFor否ISO 日期格式的时间
sourceVariant否最多 64 个字符
externalRef否最多 64 个字符;在你这边去重,冲突 → 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 为 accepted 或 pending_acceptance,取决于公司的接单模式。把 trackingUrl 交给客户,它是唯一的公开跟踪地址。写入错误:404 tenant_not_found、409 duplicate_external_ref、500 create_failed。

GET /api/v1/orders/

读取密钥所属公司拥有的一笔订单。权限相同:orders:write — 权限集合是封闭的,并且已发放给在用密钥;新增 orders:read 会破坏现有集成。查询计入读取限额,而不是创建限额。

id 是 201 响应中的 order.id。他人或不存在的 id → 404 not_found,不会泄露是否存在。响应的字段比创建时少。合作方拿到的是订单进度,而不是内部字段:

{
  "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 和 acceptanceDeadlineAt 是 unix 毫秒或 null。你会看到的状态依次是:pending_acceptance、accepted、preparing、ready、on_the_way、delivered 和 rejected。配送跳过 ready,自取跳过 on_the_way。

GET /api/v1/orders//status

订单在哪里,供顾客的跟踪页面使用。权限 catalog:read,因此可发布密钥可以从店铺轮询。{id} 是订单短码(结账响应里的 orderShortId,不区分大小写),不是 order.id。响应不含任何客户数据:只有状态、履约方式和时间。不存在、拼错或别人的短码,回答都一样:404 not_found。

GET /api/v1/orders/a1b2c3d4/status
Authorization: Bearer dlt_live_…
{
  "status": "preparing",
  "fulfilment": "pickup",
  "etaMinutes": 12,
  "promisedAt": "2026-08-26T08:45:00.000Z"
}

status 是以下之一:pending_acceptance、accepted、preparing、ready(自取:“可取餐”)、on_the_way、delivered、rejected。etaMinutes 是距 promisedAt 的整分钟数,不会为负,仅在厨房持有订单时出现(accepted、preparing、on_the_way)。promisedAt 是员工承诺的时间,没有时则为顾客要求的时间。自取订单处于 ready 时会出现 readyAt。

餐桌预订

需要开启预订模块(套餐或授予,加上店主的开关)。模块关闭:403 module_disabled。

GET /api/v1/floor-plan

GET /api/v1/floor-plan

公司的区域和桌位及其几何信息(grid 是无单位的 { width, height } 布局,店铺把它缩放到自己的图上)。权限 catalog:read。?locale= 翻译区域名称。响应带有 x-delieta-cache-tag。

GET /api/v1/reservations/availability

GET /api/v1/reservations/availability?date=2030-06-14&partySize=2

空闲的开始时间,以及每个时间下能容纳该人数的空闲桌位:date、partySize、durationMinutes 以及含 startsAt、time 和 tableIds 的 slots[]。time 是 Europe/Warsaw 的当地时间,startsAt 是可预订的时刻。没有时间时,closedReason 字段说明原因:closed_day、closure 或 hours_not_configured。date 格式错误或 partySize 超出 1–100:422 validation_failed。权限 catalog:read。从不缓存。

POST /api/v1/reservations

POST /api/v1/reservations
Authorization: Bearer dlt_live_…
Idempotency-Key: 4b0b3f0e-6d21-4f0a-9c1c-0d6f0e0a7a11
Content-Type: application/json

{ "startsAt": "2030-06-14T16:00:00.000Z", "partySize": 2, "tableId": "…",
  "guest": { "name": "Ala Kowalska", "phone": "600100200", "email": "ala@example.pl" },
  "occasion": "birthday", "note": "przy oknie" }

字段:startsAt(带偏移的 ISO 日期)、partySize(1–100)、tableId?、guest(name 最多 80 个字符、phone、email?)、occasion?(none、birthday、anniversary、date、business)、note?(最多 500 个字符)、locale?(pl、en、de、uk — 发给顾客的确认使用该语言)。请求体超过 8 KiB → 422 validation_failed。

两种密钥。带 checkout:create 的可发布密钥创建 web 预订:开始时间必须是提供的时间,状态为 requested,自动确认时为 confirmed。带 reservations:write 的保密密钥从你自己的系统创建 api 预订:任意时间,桌位只要空闲即可,状态始终为 confirmed。Idempotency-Key(8–100 个字符)可选但会被遵守:相同的键返回第一次的预订。website 字段是机器人陷阱,请保持为空。

201 返回 id、reference、status、startsAt、durationMinutes、tableIds。status 为 requested、confirmed、seated、completed、cancelled 或 no_show。拒绝是带稳定 code 的 problem+json:

Statuscode
409slot_unavailable, table_taken
422validation_failed, party_too_large, out_of_range
403module_disabled, scope_required

GET /api/v1/events

公司的活动:工作坊、品鉴会、主题夜。权限 catalog:read。需要活动模块:关闭 → 403 module_disabled,无法读取 → 503 module_unavailable。可选 ?locale=。

响应 200:{ "events": [...], "locale": "pl", "generatedAt": "..." }。列表只包含有即将到来日期的已发布活动,按 sort 排序,然后按最近的日期。每场活动:

字段含义
id、slug、name、description、audience、scheduleNote、imageUrl活动描述;description 是纯文本,以 - 开头的行是要点
priceMode、pricePerPersonGroszeper_person、free 或 on_request;仅 per_person 时有价格
participants每次询价的 { min, max }
scheduleMode、occurrencesdated 或 on_request;日期 { id, date, time, endTime, note }
booking、phoneinquiry 或 phone;活动电话或公司电话

响应带有 x-delieta-cache-tag 响应头(标记 tenant:{id}:events,每 5 分钟刷新),时间过滤在缓存之后进行,所以已过去的日期无需等待就会消失。

POST /api/v1/inquiries

顾客咨询:联系、餐饮或活动。权限 checkout:create(可发布密钥),所以请从店铺服务端调用,不要从浏览器调用。限流按访客计(在 x-delieta-visitor 中传入其地址)。website 字段是机器人陷阱:请保持为空。所需模块取决于类型:catering 和 contact 需要餐饮模块,event 需要活动模块。所需模块关闭时,响应为 403 module_disabled。请求体超过 64 KiB → 413 payload_too_large。未知字段 → 400 invalid_request。

字段:kind(contact、catering 或 event)、name、phone 和/或 email(二者必有其一,餐饮需要 phone)、message(contact 必填)、topic?(reservation、catering、order、other)、locale?,以及对于 catering 的 catering 对象:formatId?、headcount、eventDate(YYYY-MM-DD)、eventTime?(HH:MM)、deliveryAddress?、dietary?(vegetarian、gluten_free、lactose_free)、dietaryNote?、company?、nip?、venue?(onsite 或 delivery)和 addOns?([{ key, quantity }],最多 30 项,quantity 为 1–99,每个 key 一次)。请求体从不携带加购项的价格或名称:它们来自套餐形式。加购项只能与 formatId 一起发送。

对于 event,字段是一个 event 对象:eventId、occurrenceId?(来自 GET /events 的日期)、preferredDate? 和 preferredTime?(用于“Do uzgodnienia”的活动;此时 preferredDate 必填)、participants 和 organization?。phone 必填,message 可选。请求体是严格的:未知字段返回 400。

POST /api/v1/inquiries
Authorization: Bearer dlt_live_…
Content-Type: application/json
{
  "kind": "catering",
  "name": "Anna Nowak",
  "phone": "+48600100200",
  "catering": { "formatId": "f1", "headcount": 40, "eventDate": "2026-12-12", "company": "Acme", "nip": "5260250995" }
}

成功:201 { "inquiry": { "reference": "AB12CD", "status": "new" } }。NIP 有误(校验和)、缺少电话或未知字段 → 400 invalid_request,并附 errors 列表。套餐未知 → 422 unprocessable。现场服务的询价若落在场所的休息日 → 422 venue_closed(„W tym dniu lokal jest zamknięty.”)。

活动报名(kind: "event")

报名参加场所的活动(Wydarzenia 模块)也是一条咨询:kind: "event" 加 event 块。电话为必填。有固定日期的活动使用 occurrenceId(来自 GET /api/v1/events 的某个日期);“termin do uzgodnienia”(日期待定)的活动使用 preferredDate(可选 preferredTime)。公司关闭 Wydarzenia 模块时返回 403 module_disabled。拒绝为 422 unprocessable,代码在 errors[].code,字段旁附波兰语说明:unknown_event、event_not_bookable、event_past、event_phone_only、unknown_occurrence、event_in_past、preferred_date_required、participants_out_of_range。

POST /api/v1/catering-orders

公司从自己的系统记录一笔餐饮订单。权限 catering:write(仅保密密钥)。Delieta 记录这笔销售:请求体带有以格罗什计的约定金额(agreedAmountGrosze);付款可选(paid: true 表示已付)。订单行(lines)通过 menuItemId 指向公司的产品;他人或未知的 id → 422 unprocessable。订单的 source 为 api。可选的 Idempotency-Key:重试返回第一笔订单(200、Idempotent-Replayed: true)。模块关闭 → 403 module_disabled。咨询的关联已被占用 → 409 conflict。请求体超过 64 KiB → 413 payload_too_large。

字段:customer(name、phone?、email?、company?、nip?)、eventDate(YYYY-MM-DD)、eventTime?、headcount?、deliveryAddress?、formatId?、lines[](menuItemId、quantity、unitPriceGrosze?)、agreedAmountGrosze、note?、status?(planned、confirmed、delivered、cancelled)、paid?、inquiryId?。

{
  "customer": { "name": "Acme sp. z o.o.", "nip": "5260250995" },
  "eventDate": "2026-12-12",
  "headcount": 40,
  "lines": [{ "menuItemId": "i1", "quantity": 40 }],
  "agreedAmountGrosze": 240000
}

成功:201 { "order": { "id", "reference", "status": "planned", "source": "api", "paymentStatus": "unpaid", … } }。paymentStatus 为 unpaid 或 paid。

GET /api/v1/catering-orders

GET /api/v1/catering-orders?page=1&limit=50 返回列表 { orders, page, limit, total },最新的在前(limit 最大 100)。权限相同,为 catering:write。

公司的餐饮套餐位于店铺配置响应中,仅在模块开启时出现:cateringFormats(可配送、按“起价”计价的套餐)和 cateringPackages(所有套餐及其套餐字段)。每个套餐都有 imageUrl(照片地址或 null)。

OpenAPI

GET /api/v1/openapi

公开的 OpenAPI 3.1 文档,无需密钥。不想手写 curl 时,可以由它生成客户端。本页与它保持一致 — 若出现分歧,以 OpenAPI 和仓库中的测试为准。

Delieta 没有订单状态的 webhook 操作。状态请在此用 GET 读取。唯一反方向的通信(Delieta 到店铺)见 Webhook。