Справочник API v2
Справочник обновлён 15.10.2026 вместе с серверным API. Сертификаты описаны в руководстве; приём включён только на Боброво (точка 3) после проверки реальных заказов. Доступность функций на конкретной точке подтверждается при запуске. Источник истины — код обработчиков сервиса. О расхождениях сообщайте в Поддержку.
Справочник в формате OpenAPI — те же эндпоинты в трёх панелях: навигация по разделам, описание и схемы, примеры запросов и ответов. Спецификацию можно скачать: openapi.yaml.
У каждого раздела стоит метка статуса:
- Работает — эндпоинт задеплоен и проверен на реальных запросах;
- Работает с ограничениями — задеплоен, но есть оговорки, они перечислены прямо в разделе;
- В разработке — описан в спецификации, на сервере его ещё нет: запрос получит
404.
Базовые URL
| Окружение | URL | Назначение |
|---|---|---|
| Прод | https://api.aplosnab.ru | основной адрес для сайта |
| Пилотный адрес | https://delivery.beta.aplosoft.ru | тот же сервер, те же ключи и данные |
Оба адреса обслуживает один и тот же сервер, поэтому заказ, созданный через один адрес, виден через другой. Отдельной песочницы нет: каждый POST /v1/orders создаёт настоящий заказ на кассе ресторана. Тестовые заказы согласуйте с поддержкой заранее.
Общие правила:
- тело запросов и ответов — JSON в UTF-8; неизвестные поля в запросе отклоняются с
400 invalid_request(исключение — шесть служебных полей заказа:payment.code,appliedDiscounts,mappedDiscounts,giftLines,deliveryFeeMinor,deliveryFeeProductId— сервер их принимает, но перезаписывает своими значениями); - деньги — целые копейки:
{"amountMinor": 42000, "currency": "RUB"}, другой валюты нет; - время — RFC 3339 со смещением, например
2026-10-03T12:00:00+03:00; деловое время сервиса —Europe/Moscow; pointIdиexternalPointId— номер точки ВиЛ строкой ("3"— Боброво);- тело запроса — не больше 2 МиБ.
Сводка статусов
| Метод и путь | Назначение | Статус |
|---|---|---|
GET /health/live, GET /health/ready | проверка живости сервиса | Работает |
GET /v1/points | точки, доступные ключу | Работает |
GET /v1/points/{pointId}/status | принимает ли точка заказы прямо сейчас | Работает с ограничениями |
GET /v1/points/{pointId}/menu | меню точки со стоп-листом | Работает |
GET /v1/points/{pointId}/stop-list | только стоп-лист | Работает |
GET /v1/catalog/revision | ревизии меню и стоп-листа всех точек | Работает |
GET /v1/points/{pointId}/products | старые товарные маппинги | Работает |
POST /v1/delivery/resolve | точка, зона, плата и время доставки по координатам | Работает |
GET /v1/points/{pointId}/delivery-zones | полигоны зон точки | Работает |
POST /v1/addresses/check | улица по справочнику кассы | Работает |
POST /v1/checkout/calculate | расчёт корзины, купона и ВкусКоинов | Работает с ограничениями |
POST /v1/orders | создать заказ | Работает с ограничениями |
GET /v1/orders?externalOrderId= | найти заказ по своему номеру | Работает |
GET /v1/orders/{id}?source= | статус заказа по id | Работает |
POST /v1/orders/{id}/cancel | отменить заказ | Работает с ограничениями |
Вебхуки order.* | уведомления о заказе | Работает с ограничениями |
GET /v1/guests/{phone}/..., GET /v1/status, вебхуки menu.updated и подобные | профиль гостя, свежесть синхронизации | В разработке |
Где сайт может принимать заказы сегодня
Сводка выше говорит, что умеет сервер. Дойдёт ли заказ до кухни, зависит ещё и от кассы точки.
| Что | Точка 3 (Боброво) | Остальные точки ключа сайта |
|---|---|---|
Касса забирает заказы сайта (source: website) | да | нет: заказ будет принят (201), но останется в queued |
| Комментарий к заказу на кассе | да | не применяется до обновления плагина кассы |
| Онлайн-оплата, ВкусКоины, промокод, строка платы за доставку на кассе | да | не применяются до обновления плагина кассы |
Ключ сайта открыт на 14 точек: 3, 4, 5, 6, 7, 8, 9, 10, 14, 16, 18, 21, 22, 25. Пока приём включён только на Боброво, отправляйте через AploDelivery заказы только точки 3, остальные — прежним путём. Поле acceptsOrders пока не учитывает, включён ли приём заказов сайта на кассе точки (см. GET /v1/points/{pointId}/status). Точки подключаются по одной, после каждого подключения выходит запись в Changelog.
Аутентификация
Каждый запрос, кроме /health/*, подписывается HMAC-SHA256. Ключ и секрет выдаёт AploDelivery. Секрет хранится только на backend сайта, в браузер и мобильное приложение он не попадает.
| Заголовок | Значение |
|---|---|
X-API-Key | ваш ключ |
X-Timestamp | Unix-время в секундах; расхождение с сервером не больше 60 секунд |
X-Request-ID | новый UUID на каждый HTTP-запрос, включая повторы; повторно использованный UUID получит 401 |
X-Signature | hex HMAC-SHA256 от строки ниже, ключ — ваш секрет |
Строка для подписи — пять строк через \n, без перевода строки в конце:
X-Timestamp
X-Request-ID
МЕТОД (GET или POST)
путь без query-строки, например /v1/orders
hex SHA-256 тела (у GET — SHA-256 пустой строки: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855)
Query-параметры в подпись не входят: для GET /v1/orders?externalOrderId=site-1042 подписывается путь /v1/orders. Тело подписывается ровно теми байтами, которые уходят в запрос. Тестовые векторы и готовые примеры на Python, shell и Go — в разделе Аутентификация и подпись запроса.
Пример на Python (ключ и секрет — заглушки):
import hashlib, hmac, json, time, uuid, urllib.request
BASE = "https://api.aplosnab.ru"
API_KEY = "website-example" # выдаёт AploDelivery
SECRET = b"test-secret-0123456789abcdef0123456789" # храните только на backend
def call(method, path, body=None, query=""):
raw = b"" if body is None else json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode()
ts, rid = str(int(time.time())), str(uuid.uuid4())
canonical = "\n".join([ts, rid, method, path, hashlib.sha256(raw).hexdigest()])
sig = hmac.new(SECRET, canonical.encode(), hashlib.sha256).hexdigest()
req = urllib.request.Request(BASE + path + query, data=raw or None, method=method, headers={
"X-API-Key": API_KEY, "X-Timestamp": ts, "X-Request-ID": rid, "X-Signature": sig,
"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=10) as resp:
return resp.status, json.loads(resp.read() or b"null")
print(call("GET", "/v1/points"))
Скрипт выше при ошибке HTTP бросает исключение HTTPError. Тело ошибки читайте через e.read().
Ошибки
Ошибка приходит в виде {"error": "код"}. Код машиночитаемый и стабильный. У 409 price_changed в теле есть ещё поле calculation. Ответ 413 (тело больше 2 МиБ) — обычный текст, не JSON.
| HTTP | error | Когда |
|---|---|---|
400 | invalid_request | JSON не разобран, есть неизвестное поле (кроме шести служебных — см. выше), не прошла проверка полей (в т.ч. количество позиции равно нулю), id в пути не UUID |
401 | unauthorized | неверная подпись, ключ, время или повтор X-Request-ID |
403 | forbidden | точка не входит в ключ (для GET /v1/orders/{id} — чужой source) |
404 | not_found | объекта нет или он вне точек вашего ключа: сервер намеренно не различает эти случаи; та же 404 даёт точка без активного маршрута для вашего источника (операции чтения — при создании заказа то же условие это 422 route_mapping_not_found) |
409 | idempotency_conflict | тот же externalOrderId пришёл с другим содержимым |
409 | price_changed | расчёт устарел, подделан или изменились цены/стоп-лист; в теле свежий расчёт. Координаты location с расчётом не сверяются |
409 | hold_in_progress | купон или ВкусКоины этого заказа ещё удерживаются; повторите запрос через несколько секунд |
409 | cancel_rejected | касса уже отказала в отмене этого заказа |
422 | route_mapping_not_found | у точки нет активного маршрута для вашего источника (ответ создания заказа; операции чтения при том же условии возвращают 404 not_found) |
422 | product_mapping_not_found | позиция без маппинга (заказ без payment) |
422 | product_not_orderable | позицию нельзя заказать напрямую (подарочная или акционная позиция с ценой 0) |
422 | loyalty_rejected | AploCard отказал в удержании купона или ВкусКоинов; заказ не создан |
422 | loyalty_unmapped | скидке программы лояльности не сопоставлена скидка iiko; заказ не создан |
429 | rate_limited | больше 30 запросов лояльности в минуту на один телефон гостя; есть заголовок Retry-After: 60 |
503 | authentication_unavailable | временно нельзя проверить X-Request-ID; повторите с тем же X-Request-ID |
503 | loyalty_unavailable | AploCard недоступен или не подключён к точке, а в заказе есть купон или ВкусКоины |
503 | checkout_unavailable | расчёт корзины отключён на сервере |
500 | internal_error | ошибка сервера; повторите с новым X-Request-ID, при повторе — в поддержку |
Подробнее о каждой ошибке — в разделе Справочник ошибок.
Служебные
GET /health/live
Без подписи. 200 {"status":"ok"} — процесс жив. На https://api.aplosnab.ru/health отвечает тот же обработчик.
GET /health/ready — 200 {"status":"ready"}, если сервер видит свою базу данных, иначе 503 {"status":"unavailable"}.
Точки
GET /v1/points
Точки, на которые ваш ключ может отправлять заказы, с адресом, расписанием и признаком приёма заказов.
{
"points": [
{
"externalPointId": "3",
"active": true,
"streetsSyncedAt": "2026-10-03T09:50:12Z",
"name": "Боброво",
"address": "…",
"location": {"lat": 55.0, "lon": 37.0},
"phone": "…",
"timezone": "Europe/Moscow",
"schedule": [{"weekday": 1, "open": "10:00", "close": "22:00"}],
"isOpenNow": true,
"acceptsOrders": true,
"deliveryTypes": ["courier", "pickup"]
}
]
}
weekday: 0 — воскресенье, 6 — суббота. Время — московское; еслиcloseменьше или равноopen, окно переходит через полночь.name,address,location,phone,schedule,deliveryTypesравныnull, если у точки не заполнены метаданные.acceptsOrders= маршрут активен, касса на связи с iiko и точка открыта (или расписание не задано).
GET /v1/points/{pointId}/status
{"acceptsOrders": false, "terminalAlive": true, "isOpenNow": false, "reason": "closed"}
reason: terminal_offline, closed или null, когда точка принимает заказы. Ошибки: 403 — точка не в ключе, 404 — у точки нет активного маршрута.
Ограничение: acceptsOrders проверяет маршрут, связь кассы с iiko и расписание. Включён ли на кассе приём заказов вашего источника, флаг не проверяет. Для сайта сейчас true у всех 14 точек ключа, а заказы сайта забирает только касса точки 3 (см. Где сайт может принимать заказы сегодня).
Меню и стоп-лист
GET /v1/points/{pointId}/menu
Меню точки, которое сервер синхронизирует из iiko (цель — не реже раза в 10 минут). Стоп-лист уже учтён в поле available. У 19 точек источник меню — внешнее меню доставки iiko, у точек 3, 7, 10, 16, 18 — номенклатура точки. Формат ответа одинаковый.
{
"pointId": "3",
"revision": "9b1f…c4",
"currency": "RUB",
"categories": [{"id": "0d6c…", "name": "Роллы", "sort": 10, "parentId": null}],
"items": [
{
"id": "3f0c1d2e-0000-4000-8000-000000000001",
"categoryId": "0d6c…",
"name": "Ролл с лососем",
"description": "…",
"price": {"amountMinor": 42000, "currency": "RUB"},
"weightGrams": 230,
"nutrition": {"kcalPer100g": 180, "proteinPer100g": 8, "fatPer100g": 6, "carbsPer100g": 24,
"kcalPortion": 414, "proteinPortion": 18.4, "fatPortion": 13.8, "carbsPortion": 55.2},
"available": true,
"unavailableReason": null
}
],
"syncedAt": "2026-10-03T10:01:46Z",
"stale": false
}
items[].id— productId iiko. Этот id передаётся вPOST /v1/checkout/calculateи вexternalProductIdзаказа.available: falseиunavailableReason: "stop_list"— блюдо в стоп-листе.nutritionотсутствует, если КБЖУ в iiko не заполнены. Картинок в ответе нет: фото сайт хранит у себя по productId.- Заголовок
ETag— ревизия меню. Повторный запрос сIf-None-Matchвернёт304без тела, если меню не изменилось. Исключение: приstale: trueсервер всегда отвечает200. stale: true— меню не синхронизировалось дольше 30 минут. Показывайте меню, но предупредите оператора.
Ошибки: 403 — точка не в ключе, 404 — нет активного маршрута.
GET /v1/points/{pointId}/stop-list
Лёгкий запрос для частого опроса (цель свежести — 60 секунд). ETag и stale работают так же, как у меню.
{"pointId": "3", "revision": "51aa…", "items": [{"id": "3f0c1d2e-0000-4000-8000-000000000001", "balance": 0}],
"syncedAt": "2026-10-03T10:02:10Z", "stale": false}
GET /v1/catalog/revision
Ревизии меню и стоп-листа всех точек ключа одним запросом. Перезагружайте меню точки, только когда её ревизия изменилась.
{"points": {"3": {"menu": "9b1f…c4", "stopList": "51aa…"}, "4": {"menu": "…", "stopList": "…"}}}
GET /v1/points/{pointId}/products
Старый способ: список ваших собственных id товаров, сопоставленных с iiko для точки. Нужен только для заказов без checkout (без поля payment). Новым интеграциям используйте id из меню.
{"externalPointId": "3", "products": [{"externalProductId": "site-321", "kind": "product"}]}
Доставка и адрес
POST /v1/delivery/resolve
Подбирает точку и зону доставки по координатам гостя. Геокодирование — на стороне сайта: сервер адрес в координаты не переводит.
{"deliveryType": "courier", "location": {"lat": 55.75, "lon": 37.61},
"address": {"city": "Москва", "street": "Тверская", "house": "1"}}
Ответ, если доставка возможна:
{"serviceable": true, "pointId": "3", "zoneId": "6a1e…",
"minOrder": {"amountMinor": 100000, "currency": "RUB"},
"deliveryFee": {"amountMinor": 0, "currency": "RUB"},
"freeDeliveryFrom": null,
"etaMinutes": {"min": 45, "max": 60},
"street": {"status": "ok", "name": "Тверская"}}
Ответ, если нет: {"serviceable": false, "reason": "outside_zones"}. Значения reason:
outside_zones— координаты вне зон доставки;no_location— для курьера не переданlocation;geocoding_required— передан только адрес без координат;point_not_accepting— подходящая точка сейчас не принимает заказы.
street.status: ok, ambiguous (улица встречается в справочнике кассы несколько раз), not_found, unknown (справочник улиц точки ещё не загружен). Заказ с ambiguous или not_found касса не создаст, он уйдёт в reconciliation_required. Проверяйте улицу до оформления.
Для самовывоза ("deliveryType": "pickup") координаты не обязательны. Тело с ошибкой — 400, ключ не того типа — 403.
GET /v1/points/{pointId}/delivery-zones
Активные зоны точки — полигоны для карты на сайте.
{"zones": [{"id": "6a1e…", "name": "Зона 1", "polygon": [[55.70, 37.50], [55.71, 37.55], [55.68, 37.56]],
"minOrder": {"amountMinor": 100000, "currency": "RUB"}, "deliveryFee": {"amountMinor": 0, "currency": "RUB"},
"freeDeliveryFrom": null, "etaMinutes": {"min": 45, "max": 60}, "priority": 1, "active": true}]}
Вершины полигона — [широта, долгота]. Если зоны пересекаются, выигрывает меньший priority. Точка вне ключа — 404.
POST /v1/addresses/check
Сверяет улицу со справочником улиц кассы точки.
{"externalPointId": "3", "street": "Тверская", "house": "1", "city": "Москва"}
{"status": "ambiguous", "candidates": [{"name": "Ленина", "city": "…"}, {"name": "Ленина", "city": "…"}], "house": "1"}
status: ok (с полем street), ambiguous (с candidates), not_found, unknown. Дом не проверяется: в справочнике кассы домов нет.
Расчёт корзины
POST /v1/checkout/calculate
Считает корзину по ценам сервера, без побочных эффектов: купон и ВкусКоины не удерживаются. Возвращает calculationId — подписанный расчёт, действительный 15 минут. Цены из запроса не принимаются: цена позиции всегда берётся из меню точки.
{
"pointId": "3",
"deliveryType": "courier",
"location": {"lat": 55.75, "lon": 37.61},
"items": [{"id": "3f0c1d2e-0000-4000-8000-000000000001", "quantity": "2"}],
"guest": {"phone": "+79000000000"},
"coupon": "PROMO500",
"bonusToSpend": {"amountMinor": 10000, "currency": "RUB"}
}
guest, coupon, bonusToSpend, certificate, deliveryType, location, deliverAt — необязательные. quantity — строка, больше нуля, до 3 знаков после точки.
{
"pointId": "3",
"deliveryType": "courier",
"items": [{"id": "3f0c1d2e-0000-4000-8000-000000000001", "name": "Ролл с лососем", "quantity": "2",
"unitPrice": {"amountMinor": 102000, "currency": "RUB"},
"lineTotal": {"amountMinor": 204000, "currency": "RUB"}, "available": true}],
"subtotal": {"amountMinor": 204000, "currency": "RUB"},
"programDiscount": {"amountMinor": 50000, "currency": "RUB"},
"coupon": {"status": "invalid", "discount": {"amountMinor": 0, "currency": "RUB"}},
"bonus": {"requested": true, "maxSpend": {"amountMinor": 10000, "currency": "RUB"},
"spend": {"amountMinor": 10000, "currency": "RUB"}},
"deliveryFee": {"amountMinor": 0, "currency": "RUB"},
"minOrder": {"met": true, "missing": {"amountMinor": 0, "currency": "RUB"}},
"total": {"amountMinor": 144000, "currency": "RUB"},
"warnings": [],
"calculationId": "eyJ…",
"expiresAt": "2026-10-03T12:15:00+03:00"
}
Как считается итог: total = (subtotal − programDiscount − coupon.discount − bonus.spend, но не меньше 0) + deliveryFee. Плата за доставку скидками не уменьшается.
items[].warning:stop_list,unknown_item(позиции нет в меню точки),hidden_item(позицию нельзя заказать напрямую). Заказ с такой позицией не создастся.warnings— текстовые пояснения: позиция в стоп-листе, не набрана минимальная сумма, доставка по адресу недоступна, купон не подходит, ВкусКоины ограничены правилом программы.loyaltyUnavailable: true— AploCard не отвечает или не подключён к точке. Корзина посчитана без купона и ВкусКоинов.bonus.maxSpend— сколько ВкусКоинов можно списать по балансу гостя и правилам программы. По действующему правилу ВиЛ можно оплатить ВкусКоинами весь чек.
Ограничения:
- Плата за доставку и минимальная сумма заказа в расчёте сейчас не применяются:
deliveryFeeвсегда 0,minOrder.metвсегдаtrue. Зоны доставки сейчас работают только вPOST /v1/delivery/resolveиGET /v1/points/{pointId}/delivery-zones; подключение зонового прайсинга к расчёту объявят в Changelog. coupon.statusможет бытьinvalid, хотя скидка купона применилась. Сейчас скидка промокода приходит вprogramDiscount, а не вcoupon.discount. Решение о том, применился ли купон, принимайте поprogramDiscountиtotal. Исправление в работе.- Модификаторы блюд в расчёте не поддерживаются: блюдо с модификаторами через checkout не продать.
- Сервер не проверяет, что телефон принадлежит гостю. Сайт сам подтверждает телефон (SMS, вход в личный кабинет) до запроса с
guest.phone.
Ошибки: 400, 403 (точка не в ключе), 429 rate_limited, 503 checkout_unavailable.
Подарочные сертификаты
API включён и проверен на кассе Боброво (точка 3). В POST /v1/checkout/calculate и POST /v1/orders добавлено поле:
{"certificate":{"serial":"EXAMPLE-ONLY","amountMinor":50000}}
Один сертификат на заказ, сумма — положительные целые копейки RUB. Расчёт проверяет доступность без списания и возвращает certificate: {status, requestedMinor, spendMinor}. После скидок, ВкусКоинов и доставки сертификат уменьшает total — доплату обычным способом. В заказ передаются тот же сертификат и calculationId, а payment.amountMinor равен доплате. Даже при полном покрытии поле payment обязательно; для нулевой доплаты подходит method: "cash", amountMinor: 0, paid: false.
При оформлении сумма резервируется в AploCard. Подтверждённое создание на кассе приводит к списанию, окончательный отказ — к освобождению, подтверждённая отмена после списания — к возврату. Таймаут сам по себе не освобождает резерв. Номер сертификата не возвращается в проекции заказа, вебхуках и открытом содержимом токена расчёта.
Без настройки типа оплаты или доступа к AploCard запрос получает 503 loyalty_unavailable; неприменимый сертификат — 422 loyalty_rejected; изменившийся расчёт — 409 price_changed. Подробнее: алгоритм и условия включения.
Заказы
POST /v1/orders
Создаёт заказ. Идемпотентен по паре «ваш источник + externalOrderId»: повтор того же тела вернёт 200 с тем же заказом, другое тело с тем же externalOrderId — 409 idempotency_conflict.
Есть два вида заказа:
- с checkout — в теле есть
paymentиcalculationId. Сервер пересчитывает корзину, удерживает купон и ВкусКоины в AploCard и только потом создаёт заказ. Это путь для сайта; - без checkout — без
payment. Позиции передаются вашими id изGET /v1/points/{pointId}/products, цена и способ оплаты на кассу не передаются. Это старый путь колл-центра.
Пример заказа с checkout:
{
"schemaVersion": 1,
"source": "website",
"externalOrderId": "site-1042",
"externalPointId": "3",
"createdAt": "2026-10-03T12:00:00+03:00",
"deliverAt": "2026-10-03T13:00:00+03:00",
"guest": {"externalClientId": "client-9001", "name": "Иван", "phone": "+79000000000"},
"address": {"city": "Москва", "street": "Тверская", "house": "1", "flat": "10",
"entrance": "2", "floor": "5", "doorphone": "10", "comment": "Код домофона 10"},
"items": [{"externalProductId": "3f0c1d2e-0000-4000-8000-000000000001", "quantity": "2",
"price": {"amountMinor": 102000, "currency": "RUB"}}],
"total": {"amountMinor": 204000, "currency": "RUB"},
"comment": "Без васаби, позвонить за 10 минут",
"deliveryType": "courier",
"location": {"lat": 55.75, "lon": 37.61},
"persons": 2,
"coupon": "PROMO500",
"bonusToSpend": {"amountMinor": 10000, "currency": "RUB"},
"calculationId": "eyJ…",
"payment": {"method": "online", "paid": true, "externalPaymentId": "pay-7c1e…", "amountMinor": 144000}
}
Правила полей:
externalOrderId— ваш номер заказа: латиница, цифры и._:/-, до 50 символов. Повторная отправка с тем же номером не создаст дубль.total— сумма позиций до скидок:price × quantityпо всем строкам.items[].price— цена из меню.payment.amountMinor— то, что платит гость: ровноtotalиз ответаcalculate.deliveryType,coupon,bonusToSpend,certificateи телефон гостя должны совпадать с запросомcalculate. Иначе сервер вернёт409 price_changed. Координатыlocationс расчётом не сверяются.payment.method:online,cashилиcard_on_delivery. Дляonlineобязательныpaid: trueиexternalPaymentId: онлайн-заказ создаётся после успешной оплаты. Для наличных можно передать сдачу:"change": {"from": 200000}.comment— до 500 символов, касса показывает его в заказе. До кассы доходят первые 250 символов.address.streetиaddress.houseобязательны и для самовывоза. Улица ищется в справочнике кассы точки по точному совпадению названия, без учёта города. Если в справочнике несколько улиц с таким названием, заказ не создастся.guest.phone— формат E.164:+7XXXXXXXXXX.- Служебные поля (
payment.code,appliedDiscounts,mappedDiscounts,giftLines,deliveryFeeMinor,deliveryFeeProductId) перечислены в схемеOrderRequest: сервер принимает их, но перезаписывает своими значениями. Любое другое неизвестное поле —400 invalid_request.
Ответ 201 (создан) или 200 (повтор):
{
"id": "0f8e2c4a-0000-5000-8000-000000000000",
"externalOrderId": "site-1042",
"source": "website",
"state": "queued",
"payloadSha256": "c0ffee…",
"route": {"tenantId": "…", "source": "website", "externalPointId": "3", "organizationId": "…",
"terminalGroupId": "…", "orderTypeId": "…", "operatorUserId": "…", "originName": "…"},
"iikoOrderId": "…",
"iikoDeliveryId": "…",
"deliveryStatus": null,
"deliveryStatusAt": null,
"cancelState": "none",
"cancelReason": null,
"cancelDetail": null,
"createdAt": "2026-10-03T09:00:01Z",
"updatedAt": "2026-10-03T09:00:01Z"
}
409 price_changed содержит свежий расчёт без calculationId: {"error": "price_changed", "calculation": {…}}. Покажите гостю новую сумму и запросите calculate заново.
Как заказ отражается на кассе (проверено на точке 3):
- онлайн-оплата — предоплата «Оплата онлайн» (ECARD), проведённая внешней системой, кассир деньги не собирает;
- ВкусКоины — отдельная вторая строка оплаты «Вкускоин» (VKUS2);
- промокод — скидка на весь заказ, без распределения по позициям;
- плата за доставку — отдельная позиция «Доставка Приложение»;
- заказ приходит в статусе «Не подтверждена», кухня видит его после подтверждения кассиром.
Ограничения:
- Заказы сайта сейчас забирает только касса точки 3. На других точках заказ будет принят, но не дойдёт до кассы.
- Модификаторы через checkout не поддерживаются.
- Если купон или ВкусКоины удержаны, а касса не создала заказ, удержание снимается автоматически примерно через 30 минут. В редких случаях (заказ ушёл в сверку и отмена не исполнилась) удержание снимает поддержка вручную.
GET /v1/orders?externalOrderId=
Ищет заказ по вашему номеру среди заказов вашего источника. Ответ — тот же объект заказа, 404, если заказа нет или он вне точек ключа. Query-параметр в подпись не входит.
GET /v1/orders/{id}?source=
Статус заказа по id из ответа на создание. source — ваш источник (website). Чужой source — 403, заказ вне точек ключа — 404.
Состояния state:
| state | Что значит |
|---|---|
queued | принят сервером, ждёт кассу |
claimed, prepared, create_sending | касса забрала заказ и создаёт его в iiko |
created_verified | заказ создан на кассе и сверен |
reconciliation_required | касса не смогла создать заказ или не уверена в результате (часто — неоднозначная улица); заказ разбирает поддержка |
manual_review | заказ закрыт вручную после разбора |
rejected_final | касса окончательно отказала |
cancelled | отменён до того, как касса его забрала |
deliveryStatus (статус доставки iiko после создания): Unconfirmed («Не подтверждена»), New, Waiting, OnWay, Delivered, Closed, Cancelled. Значения CookingStarted и CookingCompleted описаны в контракте, но кассы ВиЛ их не присылают. Подробнее — в разделе Жизненный цикл заказа.
POST /v1/orders/{id}/cancel
{"reason": "guest_request", "comment": "Гость передумал"}
reason: guest_request, payment_failed, duplicate, other. Ответы:
200— заказ ещё не забран кассой и отменён сразу (state: cancelled), либо он уже в конечном состоянии;202— отмена передана кассе (cancelState: requested); результат придёт вcancelStateи вебхукомorder.cancel_result;409 cancel_rejected— касса уже отказала в отмене (cancelDetail:not_new— заказ ушёл дальше «Новой»,sdk_error,not_found,no_cancel_cause);404— заказа нет или он вне точек ключа.
Касса отменяет заказ с причиной «Отказ клиента».
Ограничения:
- Заказ с онлайн-оплатой или ВкусКоинами касса сейчас через API не отменяет: ответ
cancelState: rejected,cancelDetail: sdk_error. Такой заказ отменяет ресторан на кассе, гостю показывайте «свяжитесь с рестораном». Исправление в работе. - Если заказ завис в
preparedилиreconciliation_required, отмена встанет в очередь, но касса её не исполнит. Такие заказы разбирает поддержка.
Вебхуки
Сервер присылает POST на ваш webhookUrl, когда заказ меняется. URL и отдельный секрет вебхука настраивает AploDelivery по вашему запросу. Сейчас для ключа сайта URL не настроен, вебхуки не отправляются — пришлите URL в поддержку. До этого опрашивайте GET /v1/orders/{id}.
Тело:
{"eventId": "1b4e…", "type": "order.delivery_status_changed", "occurredAt": "2026-10-03T12:05:00+03:00",
"order": {"id": "…", "externalOrderId": "site-1042", "state": "created_verified", "deliveryStatus": "New", "…": "…"}}
order — тот же объект, что в GET /v1/orders/{id}, без имени, телефона и адреса гостя.
Заголовки: X-Aplo-Event-Id (= eventId), X-Aplo-Timestamp (Unix-секунды), X-Aplo-Signature — hex HMAC-SHA256 секретом вебхука от строки:
X-Aplo-Timestamp
X-Aplo-Event-Id
hex SHA-256 тела
Строки соединяются \n. Проверяйте подпись по сырому телу до разбора JSON. Доставка — «хотя бы один раз»: дедуплицируйте по eventId. Повторы при ответе не 2xx: 10 с, 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, таймаут запроса — 5 секунд. Редиректы не выполняются.
| Событие | Когда | Статус |
|---|---|---|
order.state_changed | изменился state | Работает (после настройки URL) |
order.delivery_status_changed | касса сообщила новый статус доставки | Работает (после настройки URL) |
order.cancel_requested | отмена поставлена кассе | Работает (после настройки URL) |
order.cancel_result | касса отменила или отказала | Работает (после настройки URL) |
order.payment_recorded | заказ с checkout создан, оплата записана | Работает (после настройки URL) |
order.loyalty_confirmed | купон и ВкусКоины списаны в AploCard | Работает (после настройки URL) |
order.loyalty_released | удержание купона и ВкусКоинов снято | Работает (после настройки URL) |
order.accrued | гостю начислены ВкусКоины за доставленный заказ | Работает (после настройки URL) |
menu.updated, stoplist.updated, point.status_changed | изменилось меню, стоп-лист, статус точки | В разработке |
Подробнее и пример проверки на Go — в разделе Вебхуки.
В разработке
Этих эндпоинтов на сервере нет, запрос получит 404:
GET /v1/guests/{phone}/loyalty— баланс ВкусКоинов и активные купоны гостя;GET /v1/guests/{phone}/orders— история заказов гостя;GET /v1/guests/{phone}/addresses— последние адреса гостя;GET /v1/status— свежесть синхронизации с iiko;- общие лимиты запросов (600 в минуту на ключ, 100 в минуту на IP) и заголовки
X-RateLimit-*. Сейчас действует только лимит 30 запросов лояльности в минуту на телефон гостя; - вебхуки
menu.updated,stoplist.updated,point.status_changed.
Сроки появления публикуются в Changelog. Полная спецификация расширения — Спецификация v2 (на английском, для разработчиков).