AploDelivery Partner API — портал разработчика · v2 (2026-10-16)

Справочник API v2

Работает

Справочник обновлён 15.10.2026 вместе с серверным API. Сертификаты описаны в руководстве; приём включён только на Боброво (точка 3) после проверки реальных заказов. Доступность функций на конкретной точке подтверждается при запуске. Источник истины — код обработчиков сервиса. О расхождениях сообщайте в Поддержку.

Справочник в формате OpenAPI — те же эндпоинты в трёх панелях: навигация по разделам, описание и схемы, примеры запросов и ответов. Спецификацию можно скачать: openapi.yaml.

У каждого раздела стоит метка статуса:

Базовые URL

ОкружениеURLНазначение
Продhttps://api.aplosnab.ruосновной адрес для сайта
Пилотный адресhttps://delivery.beta.aplosoft.ruтот же сервер, те же ключи и данные

Оба адреса обслуживает один и тот же сервер, поэтому заказ, созданный через один адрес, виден через другой. Отдельной песочницы нет: каждый POST /v1/orders создаёт настоящий заказ на кассе ресторана. Тестовые заказы согласуйте с поддержкой заранее.

Общие правила:

Сводка статусов

Метод и путьНазначениеСтатус
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-TimestampUnix-время в секундах; расхождение с сервером не больше 60 секунд
X-Request-IDновый UUID на каждый HTTP-запрос, включая повторы; повторно использованный UUID получит 401
X-Signaturehex 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.

HTTPerrorКогда
400invalid_requestJSON не разобран, есть неизвестное поле (кроме шести служебных — см. выше), не прошла проверка полей (в т.ч. количество позиции равно нулю), id в пути не UUID
401unauthorizedневерная подпись, ключ, время или повтор X-Request-ID
403forbiddenточка не входит в ключ (для GET /v1/orders/{id} — чужой source)
404not_foundобъекта нет или он вне точек вашего ключа: сервер намеренно не различает эти случаи; та же 404 даёт точка без активного маршрута для вашего источника (операции чтения — при создании заказа то же условие это 422 route_mapping_not_found)
409idempotency_conflictтот же externalOrderId пришёл с другим содержимым
409price_changedрасчёт устарел, подделан или изменились цены/стоп-лист; в теле свежий расчёт. Координаты location с расчётом не сверяются
409hold_in_progressкупон или ВкусКоины этого заказа ещё удерживаются; повторите запрос через несколько секунд
409cancel_rejectedкасса уже отказала в отмене этого заказа
422route_mapping_not_foundу точки нет активного маршрута для вашего источника (ответ создания заказа; операции чтения при том же условии возвращают 404 not_found)
422product_mapping_not_foundпозиция без маппинга (заказ без payment)
422product_not_orderableпозицию нельзя заказать напрямую (подарочная или акционная позиция с ценой 0)
422loyalty_rejectedAploCard отказал в удержании купона или ВкусКоинов; заказ не создан
422loyalty_unmappedскидке программы лояльности не сопоставлена скидка iiko; заказ не создан
429rate_limitedбольше 30 запросов лояльности в минуту на один телефон гостя; есть заголовок Retry-After: 60
503authentication_unavailableвременно нельзя проверить X-Request-ID; повторите с тем же X-Request-ID
503loyalty_unavailableAploCard недоступен или не подключён к точке, а в заказе есть купон или ВкусКоины
503checkout_unavailableрасчёт корзины отключён на сервере
500internal_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"]
    }
  ]
}

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
}

Ошибки: 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:

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. Плата за доставку скидками не уменьшается.

Ограничения:

Ошибки: 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:

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

Правила полей:

Ответ 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):

Ограничения:

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. Ответы:

Касса отменяет заказ с причиной «Отказ клиента».

Ограничения:

Вебхуки

Сервер присылает 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:

Сроки появления публикуются в Changelog. Полная спецификация расширения — Спецификация v2 (на английском, для разработчиков).