Вебхуки
Вебхук — это POST-запрос от Orda на ваш адрес, когда что-то произошло. Не нужно опрашивать API: статус доставки приходит через секунды.
Подключение
Заголовок раздела «Подключение»curl -s -X POST "$ORDA_API/webhooks" -H "X-Api-Key: $ORDA_KEY" -H "Content-Type: application/json" \ -d '{ "url": "https://erp.example.kz/orda/webhook", "events": ["order.status_changed", "order.delivered", "order.failed", "plan.approved"], "description": "1С: статусы доставки" }'В ответе поле secret (whsec_…) — секрет подписи, показывается один раз. Новый секрет:
POST /webhooks/{id}/rotate-secret (старый перестаёт действовать сразу). Тестовое событие ping:
POST /webhooks/{id}/test. Права ключа: integrations.view/create/edit/delete.
Требования к адресу: https, публичный адрес (не localhost и не внутренняя сеть), без логина и пароля в URL.
События
Заголовок раздела «События»| Событие | Когда |
|---|---|
order.status_changed |
статус заказа изменился (кроме черновых new ↔ planned) |
order.delivered |
заказ доставлен |
order.failed |
заказ не доставлен (reason, если указана) |
route.planned |
построен маршрут в черновике плана (может ещё измениться) |
plan.approved |
план дня утверждён: маршруты окончательные |
route.started |
водитель начал смену в Orda Driver: заказы маршрута «в пути» |
stop.arrived |
водитель нажал «Я на месте» (с расстоянием до точки) |
pod.created |
подтверждение доставки: доставлено (подпись, фото, оплата) или не доставлено (причина) |
ping |
тестовое событие |
Формат запроса
Заголовок раздела «Формат запроса»POST /orda/webhook HTTP/1.1Content-Type: application/jsonUser-Agent: OrdaTMS-Webhooks/1.0Orda-Event: order.deliveredOrda-Event-Id: 7f9c2b1e-…Orda-Delivery-Id: 0c41d7a2-…Orda-Attempt: 1Orda-Signature: t=1763370000,v1=5f2b…c9
{ "id": "7f9c2b1e-…", "type": "order.delivered", "createdAt": "2026-11-20T09:47:12.531Z", "apiVersion": "v1", "data": { "orderId": "4b0c…", "externalId": "РТ-00001", "number": "РТ-00001", "previousStatus": "in_transit", "status": "delivered" }}route.planned: planId, routeId, date, vehicleId, driverId, timezone, leaveAt, returnAt,
distanceKm, stops[] (seq, orderId, externalId, number, arriveAt).
plan.approved: planId, date, orders[] (orderId, externalId, number).
route.started: routeId, planId, date, driverId, vehicleId, startedAt, startMode (depot/home),
orders[] (orderId, externalId, number).
stop.arrived: routeId, stopId, orderId, externalId, number, driverId, arrivedAt, location,
distanceM (от точки, если известно), outsideRadius (дальше радиуса из настроек компании, по умолчанию 300 м).
pod.created: orderId, externalId, number, routeId, stopId, driverId, outcome (delivered/failed),
reason, action (return_to_warehouse/retry_tomorrow), recipientName, hasSignature, photoIds[],
payment (method, amount, currency), location, happenedAt. Фото и подпись — GET /api/v1/orders/{id}/proof
(фото могут догрузиться позже: водитель отправляет их после действия, когда есть сеть).
Проверка подписи
Заголовок раздела «Проверка подписи»Orda-Signature: t=<время Unix>,v1=<подпись>, где подпись — HMAC-SHA256 от строки "<t>.<тело запроса>"
с вашим секретом, в hex. Проверяйте сырое тело запроса (до разбора JSON) и отклоняйте метки старше 5 минут.
// Node.js (Express): app.post('/orda/webhook', express.raw({ type: 'application/json' }), handler)import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyOrdaSignature(rawBody, header, secret, toleranceSec = 300) { const parts = Object.fromEntries(header.split(',').map((p) => p.split('='))); const t = Number(parts.t); if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); const given = Buffer.from(parts.v1 ?? '', 'hex'); return given.length === 32 && timingSafeEqual(given, Buffer.from(expected, 'hex'));}# Python (Flask / FastAPI): используйте request.get_data() / await request.body()import hashlib, hmac, time
def verify_orda_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) t = int(parts.get("t", "0")) if abs(time.time() - t) > tolerance: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", ""))<?php // PHPfunction verify_orda_signature(string $rawBody, string $header, string $secret, int $tolerance = 300): bool { parse_str(str_replace(',', '&', $header), $p); $t = (int)($p['t'] ?? 0); if (abs(time() - $t) > $tolerance) return false; $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret); return hash_equals($expected, $p['v1'] ?? '');}Для 1С — см. 1С (HMAC считается встроенными средствами платформы).
Ответ, повторы, порядок
Заголовок раздела «Ответ, повторы, порядок»- Ответьте
2xxза 10 секунд. Тяжёлую обработку делайте после ответа (в очереди). - Любой другой ответ, таймаут или ошибка соединения → повтор через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, 12 ч, 24 ч
(всего 8 попыток, ~45 часов). Затем доставка помечается
failed; её можно отправить снова:POST /webhooks/deliveries/{id}/retry. - Редиректы (
3xx) не выполняются — укажите окончательный адрес. - События одного адреса отправляются по порядку возникновения, но при повторах порядок может нарушиться —
сверяйте
createdAtи храните последний статус заказа. - Одно и то же событие может прийти дважды (например, вы обработали, но ответ не дошёл). Используйте
idсобытия (Orda-Event-Id) для защиты от повторной обработки.
GET /webhooks/{id}/deliveries?status=failed — последние доставки: событие, попытки, код ответа вашего сервера,
текст ошибки, время следующей попытки и само тело события.