Настройка, включение и проверка подписи входящих webhook-запросов.

1) Как получить настройки

Путь в интерфейсе

Продавцы → Название продавца → Настройки

Поля
  • Webhook URL — адрес вашего обработчика.
  • Token используется как webhook_secret — секретный ключ подписи HMAC.
Важно

Token — секрет. Не публикуйте его и не храните открытым текстом. Используйте его как ключ HMAC.

2) Как включить отправку webhook

Путь в интерфейсе

Статусы и путь → Статус заказа → [статус] → Webhook вкл. / выкл.

Условие отправки

Webhook отправляется при переходе заказа в статус с включённым Webhook = ON.

3) Формат запроса

HTTP
  • Метод: 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_idnumberИдентификатор заказа
userobjectДанные пользователя
user.user_idstringИдентификатор пользователя
user.namestringИмя пользователя
user.phonestringТелефон
user.emailstring|nullЭлектронная почта
typenumberТип заказа (внутренний)
companynumberИдентификатор компании
merchantnumberИдентификатор продавца
destination_addressobjectАдрес в многоязычном формате (EN/IL/ES/RU)
destination_coordinatesobjectКоординаты доставки
destination_coordinates.latstringШирота
destination_coordinates.lonstringДолгота
destination_infostringДополнительные сведения (строка)
destination_info_jsonarrayДополнительные сведения (структурированные)
commentstringКомментарий клиента
infostringДополнительная информация (служебное поле)
costnumberБазовая стоимость товаров
discountnumberСкидка
bonusnumberБонусы
promocodestringПромокод
promocode_amountnumberСкидка по промокоду
tipsnumberЧаевые (сумма)
tips_percentnumberЧаевые (процент)
feenumberСервисный сбор
vatnumberНДС
deliverynumberСтоимость доставки
payment_commissionnumberКомиссия платёжного провайдера
totalnumberИтого к оплате
paidboolПризнак оплаты заказа
order_statusobjectТекущий статус заказа
order_status.statusstringИдентификатор статуса
order_status.valueobjectНазвание статуса (многоязычное)
time_createstringДата создания (YYYY-MM-DD HH:MM:SS)
last_timestringВремя последнего обновления
time_finishstring|nullДата завершения
time_deliverystring|nullДата доставки
couriernull|objectДанные курьера (если назначен)
cartarrayПозиции заказа
cart[].idnumberИдентификатор товара
cart[].qtynumberКоличество
cart[].pricenumberЦена за единицу
cart[].discountnumberСкидка на позицию
cart[].amountnumberСумма позиции
cart[].totalnumberИтог позиции
cart[].nameobjectНазвание (многоязычное)
cart[].descriptionobjectОписание (многоязычное)
cart[].addonsbool|arrayДополнения (если используются)
cart[].balancenumberОстаток (служебное поле)
currencystringСимвол валюты
is_pickup_pointboolПризнак пункта выдачи
external_idstring|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.