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

Вебхуки

Работает

Основной канал уведомлений об изменении заказа. Поллинг (GET /v1/orders, GET /v1/orders/{id}) остаётся резервным — не полагайтесь только на вебхуки, отсутствие одного вебхука не доказывает отсутствие изменения (сеть ненадёжна в обе стороны).

Настройка

Вебхук настраивается на стороне AploDelivery для вашего ingress-credential: webhookUrl (только https://, http://127.0.0.1 разрешён исключительно для тестов) и webhookSecret (≥ 32 символов, обязательно отличается от вашего API HMAC-секрета — сервер откажет в конфигурации, если они совпадают). Если webhookUrl не задан — вебхуки не отправляются, остаётся только поллинг. Один webhookUrl на пару tenantId + source; настройка выпуска/смены webhookUrl в v1 — конфигурационная на стороне AploDelivery, self-service API для этого не задеплоен.

Событийная модель (v1)

СобытиеКогда
order.state_changedлюбое изменение state заказа, включая переход в cancelled
order.delivery_status_changedплагин сообщил новый deliveryStatus (см. Жизненный цикл заказа)
order.cancel_requestedзаказ был в процессе создания, партнёр запросил отмену — она поставлена в очередь плагину
order.cancel_resultплагин сообщил исход отмены — cancelled или rejected

docs/PARTNER_API_V2_SPEC.md §6 добавляет планируемые типы событий: menu.updated, stoplist.updated, point.status_changed, order.payment_recorded, order.loyalty_confirmed, order.loyalty_released, order.accrued. Они пока не отправляются.

Тело запроса

{
  "eventId": "5c9c9e0a-2b7a-4c86-9b8e-...",
  "type": "order.delivery_status_changed",
  "occurredAt": "2026-09-23T10:30:00+03:00",
  "order": { "...": "то же тело, что GET /v1/orders/{id}" }
}

order — полная проекция заказа (OrderView/OrderStatus) без внутренних технических полей; PII (имя/телефон/адрес гостя) в это тело не входит — вебхук несёт только заказные/статусные поля, PII не пересылается webhook-ом.

Заголовки и подпись

ЗаголовокЗначение
X-Aplo-Event-IdUUID события, тот же, что eventId в теле
X-Aplo-TimestampUnix time в секундах, момент отправки
X-Aplo-Signaturehex HMAC-SHA256 от canonical-строки

Canonical-строка вебхука:

timestamp + "\n" + eventId + "\n" + sha256hex(body)

— обратите внимание, это другая (короче) формула, чем у входящих партнёрских запросов (там ещё есть метод и путь, см. Аутентификацию); не переиспользуйте одну функцию для обеих проверок без параметризации.

Проверка на Go (ingressclient.VerifyWebhook)

body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
if err != nil {
	http.Error(w, "request too large", http.StatusRequestEntityTooLarge)
	return
}
event, err := ingressclient.VerifyWebhook(
	[]byte(os.Getenv("APLODELIVERY_WEBHOOK_SECRET")),
	r.Header,
	body,
	time.Now(),
	time.Minute,
)
if err != nil {
	http.Error(w, "invalid webhook", http.StatusUnauthorized)
	return
}
// Дедуплицировать event.EventID ДО бизнес-обработки.

VerifyWebhook (pkg/ingressclient/webhook.go) проверяет по порядку: наличие всех трёх заголовков, что X-Aplo-Event-Id — валидный UUID, что X-Aplo-Timestamp в пределах переданного допуска (в примере — 60 секунд), HMAC-подпись, и что тело — строгий JSON без неизвестных полей и без лишних значений после основного объекта. При любой ошибке возвращается zero-value Event{} — не пытайтесь читать частично распарсенные поля из результата с ошибкой.

Если готового SDK на вашем языке нет — реализуйте формулу проверки вручную по той же схеме: заголовки → парсинг timestamp/UUID → пересчитать HMAC от timestamp + "\n" + eventId + "\n" + sha256hex(body) → сравнить постоянным по времени способом (hmac.Equal в Go; на других языках — соответствующий constant-time compare, не ==/memcmp).

Дедупликация

Доставка — at-least-once. Дедуплицируйте по eventId до применения бизнес-эффекта (например по уникальному индексу на eventId в своей БД или проверкой «уже обработан» перед изменением состояния заказа на своей стороне). Повторная доставка одного и того же eventId с одинаковым телом — нормальная ситуация, не ошибка.

Ретраи

При неуспешной доставке (таймаут, сетевая ошибка, ответ не 2xx) сервис повторяет попытку по расписанию:

10s → 1m → 5m → 30m → 2h → 6h  (≈ 9 часов суммарно)

После исчерпания расписания запись помечается dead и больше не повторяется — на этот случай и нужен резервный поллинг (FindByExternalOrderID/GetOrder). Таймаут одного HTTP-запроса на доставку — 5 секунд. Worker не следует HTTP-редиректам (3xx считается неуспехом наравне с 5xx) — если ваш эндпоинт переехал, обновите webhookUrl, не полагайтесь на редирект. При сбое доставки в лог сервиса пишется структурная запись без тела запроса/ответа (PII) — если нужна диагностика, обращайтесь в поддержку с eventId и примерным временем.

Практические рекомендации

Связанное