Настройка, включение и проверка подписи входящих webhook-запросов.
1) Как получить настройки
Продавцы → Название продавца → Настройки
- Webhook URL — адрес вашего обработчика.
- Token используется как
webhook_secret— секретный ключ подписи HMAC.
Token — секрет. Не публикуйте его и не храните открытым текстом. Используйте его как ключ HMAC.
2) Как включить отправку webhook
Статусы и путь → Статус заказа → [статус] → Webhook вкл. / выкл.
Webhook отправляется при переходе заказа в статус с включённым Webhook = ON.
3) Формат запроса
- Метод:
POST - Content-Type:
application/json - Тело: JSON-объект заказа
- X-Signature: подпись запроса (HMAC)
Подпись рассчитывается по исходному телу запроса (raw JSON). Для проверки используйте исходное тело, например php://input, без повторного json_encode().
4) Подпись запроса (HMAC SHA-256)
4.1 Формирование подписи на платформе
Подписывается JSON-строка, отправленная в теле webhook.
$payload = json_encode($data, JSON_UNESCAPED_UNICODE);
$sign = hash_hmac('sha256', $payload, $merchant['token']);
Подпись передаётся в заголовке:
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Signature: ' . $sign
],
4.2 Проверка подписи
Проверка подписи webhook позволяет убедиться, что:
- запрос действительно отправлен нашей платформой
- тело запроса не изменилось при передаче
- данные не подделаны третьими лицами
Ниже приведён общий алгоритм, не зависящий от языка программирования.
Общий принцип
Используется алгоритм HMAC-SHA256.
Подпись формируется следующим образом:
signature = HMAC_SHA256(raw_request_body, webhook_secret)
Где:
raw_request_body— точное исходное тело HTTP-запроса без преобразованийwebhook_secret— Token из раздела «Продавцы → Настройки»
Полученная подпись отправляется в заголовке X-Signature.
Пример PHP
<?php
// Token from Merchant → Settings (used as webhook_secret)
$secret = 'WEBHOOK_TOKEN_FROM_MERCHANT_SETTINGS';
// raw body as received (important!)
$rawBody = file_get_contents('php://input');
// signature from X-Signature header
$received = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
// calculate signature
$calculated = hash_hmac('sha256', $rawBody, $secret);
// compare in constant time
if (!hash_equals($calculated, $received)) {
http_response_code(401);
echo 'Invalid signature';
exit;
}
http_response_code(200);
echo 'OK';
Проверяйте подпись по исходному телу. Повторное кодирование JSON через json_encode() может изменить порядок ключей и экранирование, и подпись не совпадёт.
Пример Python
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "WEBHOOK_TOKEN_FROM_MERCHANT_SETTINGS"
@app.route("/webhook", methods=["POST"])
def webhook():
# Raw body exactly as received
raw_body = request.get_data() # bytes
# Signature from header
received_signature = request.headers.get("X-Signature", "")
# Calculate HMAC-SHA256
calculated_signature = hmac.new(
WEBHOOK_SECRET.encode("utf-8"),
raw_body,
hashlib.sha256
).hexdigest()
# Constant-time comparison
if not hmac.compare_digest(calculated_signature, received_signature):
abort(401, "Invalid signature")
return "OK", 200
if __name__ == "__main__":
app.run(port=5000)
Используйте исходное тело request.get_data(). Не сериализуйте JSON повторно перед расчётом подписи: хеш может не совпасть.
5) Поля тела запроса
| Поле | Тип | Описание |
|---|---|---|
| order_id | number | Идентификатор заказа |
| user | object | Данные пользователя |
| user.user_id | string | Идентификатор пользователя |
| user.name | string | Имя пользователя |
| user.phone | string | Телефон |
| user.email | string|null | Электронная почта |
| type | number | Тип заказа (внутренний) |
| company | number | Идентификатор компании |
| merchant | number | Идентификатор продавца |
| destination_address | object | Адрес в многоязычном формате (EN/IL/ES/RU) |
| destination_coordinates | object | Координаты доставки |
| destination_coordinates.lat | string | Широта |
| destination_coordinates.lon | string | Долгота |
| destination_info | string | Дополнительные сведения (строка) |
| destination_info_json | array | Дополнительные сведения (структурированные) |
| comment | string | Комментарий клиента |
| info | string | Дополнительная информация (служебное поле) |
| cost | number | Базовая стоимость товаров |
| discount | number | Скидка |
| bonus | number | Бонусы |
| promocode | string | Промокод |
| promocode_amount | number | Скидка по промокоду |
| tips | number | Чаевые (сумма) |
| tips_percent | number | Чаевые (процент) |
| fee | number | Сервисный сбор |
| vat | number | НДС |
| delivery | number | Стоимость доставки |
| payment_commission | number | Комиссия платёжного провайдера |
| total | number | Итого к оплате |
| paid | bool | Признак оплаты заказа |
| order_status | object | Текущий статус заказа |
| order_status.status | string | Идентификатор статуса |
| order_status.value | object | Название статуса (многоязычное) |
| time_create | string | Дата создания (YYYY-MM-DD HH:MM:SS) |
| last_time | string | Время последнего обновления |
| time_finish | string|null | Дата завершения |
| time_delivery | string|null | Дата доставки |
| courier | null|object | Данные курьера (если назначен) |
| cart | array | Позиции заказа |
| cart[].id | number | Идентификатор товара |
| cart[].qty | number | Количество |
| cart[].price | number | Цена за единицу |
| cart[].discount | number | Скидка на позицию |
| cart[].amount | number | Сумма позиции |
| cart[].total | number | Итог позиции |
| cart[].name | object | Название (многоязычное) |
| cart[].description | object | Описание (многоязычное) |
| cart[].addons | bool|array | Дополнения (если используются) |
| cart[].balance | number | Остаток (служебное поле) |
| currency | string | Символ валюты |
| is_pickup_point | bool | Признак пункта выдачи |
| external_id | string|null | Внешний идентификатор |
Некоторые поля являются служебными и могут быть null в зависимости от настроек продавца и сценария заказа.
6) Пример тела запроса
Пример JSON, отправляемого в теле webhook:
{
"order_id": 2088,
"user": {
"user_id": "107",
"name": "Test",
"phone": "972000000000",
"email": null
},
"type": 1,
"company": 28,
"merchant": 44,
"destination_address": {
"EN": "HaShita, Ofir, Eilat, Beersheba Subdistrict, South District, 8804625, Israel",
"IL": "HaShita, Ofir, Eilat, Beersheba Subdistrict, South District, 8804625, Israel",
"ES": "HaShita, Ofir, Eilat, Beersheba Subdistrict, South District, 8804625, Israel",
"RU": "HaShita, Ofir, Eilat, Beersheba Subdistrict, South District, 8804625, Israel"
},
"destination_coordinates": {
"lat": "29.556174355301",
"lon": "34.949371218681"
},
"destination_info": "",
"destination_info_json": [],
"comment": "",
"info": "",
"cost": 58,
"discount": 0,
"bonus": 0,
"promocode_amount": 0,
"tips": 3,
"tips_percent": 5,
"fee": 0,
"vat": 0,
"delivery": 0.47,
"payment_commission": 0,
"total": 61.47,
"promocode": "",
"paid": false,
"order_status": {
"status": "4",
"value": {
"EN": "Ready",
"IL": " מוּכָן",
"RU": "Ready"
}
},
"time_create": "2026-02-26 14:25:32",
"last_time": "2026-02-26 17:28:01",
"time_finish": "2026-02-26 15:56:32",
"time_delivery": null,
"courier": null,
"cart": [
{
"addons": false,
"price": 38,
"discount": 0,
"amount": 38,
"total": 38,
"id": 399,
"qty": 1,
"name": {
"EN": "Toast Lebowski",
"IL": "טוסט לבובסקי",
"ES": "Tostada Lebowski",
"RU": "Toast Lebowski"
},
"description": {
"EN": "Tomato sauce, pesto, Gouda, purple onion, tomato and Kalamata olives\nServed with a green salad seasoned with vinaigrette dressing",
"IL": "רוטב עגבניות, פסטו, גבינת גאודה, בצל סגול, עגבנייה וזיתים קלמטה \nמוגש עם סלט ירוק מתובל ברוטב ויניגרט",
"ES": "Salsa de tomate, pesto, Gouda, cebolla morada, tomate y aceitunas Kalamata \nServido con una ensalada verde aderezada con vinagreta",
"RU": "Tomato sauce, pesto, Gouda, purple onion, tomato and Kalamata olives\nServed with a green salad seasoned with vinaigrette dressing"
},
"balance": 1
},
{
"addons": false,
"price": 20,
"discount": 0,
"amount": 20,
"total": 20,
"id": 396,
"qty": 1,
"name": {
"EN": "Summer muesli",
"ES": "Summer muesli",
"RU": "Summer muesli"
},
"description": {
"EN": "Real Greek yogurt with green smith apple, pear, banana, dates, halva and granola with touches of silan and raw tahini",
"ES": "Real Greek yogurt with green smith apple, pear, banana, dates, halva and granola with touches of silan and raw tahini",
"RU": "Real Greek yogurt with green smith apple, pear, banana, dates, halva and granola with touches of silan and raw tahini"
},
"balance": 1
}
],
"max_coock_time": 0,
"delivery_time": 0,
"pay_type": {
"pay_type_id": "2",
"name": {
"EN": "Cash",
"RU": "Cash",
"IL": "כסף מזומן"
}
},
"cutlery": false,
"asap": true,
"pay_link": null,
"pay_date": null,
"pay_info": null,
"platform": null,
"ver": null,
"promocode_type": 1,
"promocode_discount": "0",
"odd": 100,
"print": "0",
"last_order_status": 4,
"currency": "₪",
"is_pickup_point": false,
"external_id": null
}
После успешной обработки рекомендуется отвечать 200 OK (или другим 2xx). При неверной подписи — 401, при временной ошибке интегратора — 5xx.