Вебхуки

Магазин сам отправляет POST на ваш адрес, когда что-то меняется: заказ, оплата, товар, остаток.

Подписка

Панель магазина: «Настройки» → «API и вебхуки» → «Вебхуки». Укажите адрес https://…, отметьте события и нажмите «Добавить». Секрет whsec_… показывает кнопка «Секрет», «Проверить» отправляет событие ping — результат на вкладке «Журнал».

Что приходит

POST с JSON: номер события, тип, время, магазин и объект в том же виде, что в API. Здесь заказ сокращён.

Тело запроса
{
  "id": "evt_1042",
  "type": "order.paid",
  "created_at": "2026-10-30T09:31:07+00:00",
  "shop": {"site": "myshop", "host": "myshop.okeysait.ru"},
  "data": {"id": 518, "number": "518", "paid": true, "total": "2580.00"}
}
ЗаголовокЗначение
X-Okeano-EventТип события, например order.paid
X-Okeano-DeliveryНомер доставки
X-Okeano-SignatureПодпись t=<unix>,v1=<hex> — как проверить
User-AgentOkeano-Webhooks/1.0

Проверка подписи

Подпись v1 — HMAC-SHA256 от строки <t>.<тело> с секретом whsec_…, в hex. Считайте её по сырому телу, а не по разобранному JSON, и отклоняйте запросы, у которых t старше 5 минут.

# Проверка подписи в оболочке (bash): сырое тело запроса — в файле body.json,
# заголовок X-Okeano-Signature — в $SIG, секрет whsec_… — в $WHSEC
# Секрет виден в списке процессов — запускайте на своём сервере.
# Сравнение не за постоянное время: в языке нет такой функции.
[[ $SIG =~ ^t=([0-9]+),v1=([0-9a-f]{64})$ ]] || { echo invalid; exit; }
t=${BASH_REMATCH[1]}; v1=${BASH_REMATCH[2]}
want=$( { printf '%s.' "$t"; cat body.json; } | openssl dgst -sha256 -hmac "$WHSEC" | sed 's/^.*= //')
age=$(( $(date +%s) - t )); age=${age#-}
if [ "$want" = "$v1" ] && [ "$age" -le 300 ]; then echo valid; else echo invalid; fi
<?php
// PHP: проверка подписи вебхука Океано
function okeano_webhook_valid($body, $header, $secret, $tolerance = 300) {
    if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $header, $m)) return false;
    if (abs(time() - (int)$m[1]) > $tolerance) return false;
    return hash_equals(hash_hmac('sha256', $m[1] . '.' . $body, $secret), $m[2]);
}
$ok = okeano_webhook_valid(
    file_get_contents('php://input'),
    $_SERVER['HTTP_X_OKEANO_SIGNATURE'] ?? '',
    'whsec_…'
);
# Python: проверка подписи вебхука Океано; body — сырые байты тела запроса
import hashlib
import hmac
import re
import time


def okeano_webhook_valid(body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
    if not m or abs(time.time() - int(m[1])) > tolerance:
        return False
    want = hmac.new(secret.encode(), m[1].encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, m[2])
// Node.js: проверка подписи вебхука Океано; rawBody — сырые байты тела (Buffer), не разобранный JSON
const crypto = require("crypto");
function okeanoWebhookValid(rawBody, header, secret, tolerance = 300) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || "");
  if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > tolerance) return false;
  const want = crypto.createHmac("sha256", secret).update(m[1] + ".").update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(m[2]));
}
// 1С:Предприятие 8.3.11 и новее, HTTP-сервис: проверка подписи вебхука Океано.
// Тело = Запрос.ПолучитьТелоКакДвоичныеДанные(); Заголовок = Запрос.Заголовки.Получить("X-Okeano-Signature").
// Сравнение не за постоянное время: в языке нет такой функции.
Функция ПодписьОкеаноВерна(Тело, Заголовок, Секрет, Допуск = 300) Экспорт
	Части = СтрРазделить(Заголовок, ",");
	Если Части.Количество() <> 2 Тогда
		Возврат Ложь;
	КонецЕсли;
	Если Не СтрНачинаетсяС(Части[0], "t=") Тогда
		Возврат Ложь;
	КонецЕсли;
	Если Не СтрНачинаетсяС(Части[1], "v1=") Тогда
		Возврат Ложь;
	КонецЕсли;
	Метка = Сред(Части[0], 3);
	Подпись = НРег(Сред(Части[1], 4));
	Если СтрДлина(Метка) > 12 Тогда
		Возврат Ложь;
	КонецЕсли;
	Если Не ТолькоСимволы(Метка, "0123456789") Тогда
		Возврат Ложь;
	КонецЕсли;
	Если СтрДлина(Подпись) <> 64 Тогда
		Возврат Ложь;
	КонецЕсли;
	Если Не ТолькоСимволы(Подпись, "0123456789abcdef") Тогда
		Возврат Ложь;
	КонецЕсли;
	Сейчас = ТекущаяУниверсальнаяДата() - Дата(1970, 1, 1);
	Если Макс(Сейчас - Число(Метка), Число(Метка) - Сейчас) > Допуск Тогда
		Возврат Ложь;
	КонецЕсли;
	Начало = ПолучитьБуферДвоичныхДанныхИзСтроки(Метка + ".", КодировкаТекста.UTF8, Ложь);
	Сообщение = Начало.Соединить(ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(Тело));
	Ключ = ПолучитьБуферДвоичныхДанныхИзСтроки(Секрет, КодировкаТекста.UTF8, Ложь);
	// платформа отдаёт hex заглавными буквами и с пробелами между байтами
	Ожидаемая = НРег(СтрЗаменить(ПолучитьHexСтрокуИзБуфераДвоичныхДанных(HMAC_SHA256(Ключ, Сообщение)), " ", ""));
	Возврат Ожидаемая = Подпись;
КонецФункции

// Непустая строка, целиком состоящая из допустимых символов.
Функция ТолькоСимволы(Строка, Допустимые)
	Если СтрДлина(Строка) = 0 Тогда
		Возврат Ложь;
	КонецЕсли;
	Для Позиция = 1 По СтрДлина(Строка) Цикл
		Если СтрНайти(Допустимые, Сред(Строка, Позиция, 1)) = 0 Тогда
			Возврат Ложь;
		КонецЕсли;
	КонецЦикла;
	Возврат Истина;
КонецФункции

// HMAC-SHA256 по RFC 2104: готовой функции HMAC в платформе нет.
Функция HMAC_SHA256(Знач Ключ, Данные)
	Блок = 64;
	Если Ключ.Размер > Блок Тогда
		Ключ = SHA256(Ключ);
	КонецЕсли;
	Внутр = Новый БуферДвоичныхДанных(Блок);
	Внешн = Новый БуферДвоичныхДанных(Блок);
	Для Индекс = 0 По Блок - 1 Цикл
		Байт = ?(Индекс < Ключ.Размер, Ключ[Индекс], 0);
		Внутр[Индекс] = ПобитовоеИсключительноеИли(Байт, 54);
		Внешн[Индекс] = ПобитовоеИсключительноеИли(Байт, 92);
	КонецЦикла;
	Возврат SHA256(Внешн.Соединить(SHA256(Внутр.Соединить(Данные))));
КонецФункции

Функция SHA256(Буфер)
	Хеш = Новый ХешированиеДанных(ХешФункция.SHA256);
	Хеш.Добавить(ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(Буфер));
	Возврат ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(Хеш.ХешСумма);
КонецФункции
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"regexp"
	"strconv"
	"time"
)

var okeanoSig = regexp.MustCompile(`^t=(\d+),v1=([0-9a-f]{64})$`)

// OkeanoWebhookValid проверяет подпись вебхука Океано; body — сырое тело запроса.
func OkeanoWebhookValid(body []byte, header, secret string) bool {
	m := okeanoSig.FindStringSubmatch(header)
	if m == nil {
		return false
	}
	t, err := strconv.ParseInt(m[1], 10, 64)
	if err != nil {
		return false
	}
	age := time.Now().Unix() - t
	if age < 0 {
		age = -age
	}
	if age > 300 {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(m[1] + "."))
	mac.Write(body)
	want := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(want), []byte(m[2]))
}

Не сошлось — ответьте 400, доставка повторится. После проверки отвечайте сразу, а тяжёлую работу делайте в фоне.

Доставка и повторы

  • Ответьте 2xx за 10 секунд — доставка засчитана. Другой ответ или тишина — неудача. Редиректы не выполняются.
  • Повторы: через 1 минуту, 5 минут, 30 минут, 2 часа, 6 часов и 12 часов. После 7 попыток — «не доставлено»; повторить вручную можно в «Журнале».
  • 50 неудач подряд выключают вебхук, владельцу приходит письмо. Включить — кнопкой «Включить» в панели.
  • «Хотя бы один раз»: событие может прийти дважды — отбрасывайте повторы по id. Порядок событий не гарантирован.
  • Адрес: только https, порт 443 или 8443; внутренние адреса запрещены.

События

СобытиеКогда приходит и что в data
order.createdСоздан заказ. Новый заказ с сайта, через embed.js или API. В data — заказ целиком, с позициями.
order.paidЗаказ оплачен. Заказ отмечен оплаченным (оплата на сайте, из панели или через API). При оплате order.paid приходит всегда; вместе с ним могут прийти order.updated и order.status_changed (например, при оплате картой или через Robokassa) — отсекайте повторы по id заказа и updated_at.
order.status_changedСменился статус заказа. Новый статус — в data.status. Вместе с ним приходит и order.updated; при оплате (например, через Robokassa) он может прийти вместе с order.paid.
order.updatedЗаказ изменён. Изменились данные заказа: статус, покупатель, адрес, заметка, состав и другое. Если вы подписаны на order.created, для заказа, созданного в том же запросе, order.updated не приходит. При смене статуса приходят оба события — order.status_changed и order.updated; при оплате к order.paid могут добавиться order.updated и order.status_changed. Отсекайте повторы по id заказа и updated_at.
product.createdСоздан товар. В data — товар целиком, как в GET /products/{id}.
product.updatedИзменён товар. Изменились поля товара, его варианты, разделы или фото. Несколько правок одного товара за один запрос дают одно событие. Если товар к моменту отправки уже удалён, в data будет {"id": 42, "deleted": true}.
product.deletedУдалён товар. В data — номер и артикулы удалённого товара.
stock.changedИзменились остатки или цены. Изменились остаток, цена или старая цена вариантов. Изменения за один запрос собираются в одно событие. В data.items — текущее состояние затронутых вариантов, не больше 200: если изменений за запрос больше 200, вместо stock.changed приходит один catalog.imported. Поле truncated оставлено для совместимости и всегда false.
catalog.importedМассовое изменение каталога. Больше 200 изменений товаров и остатков за одну операцию либо загрузка файлом: вместо события на каждый товар приходит одно catalog.imported со счётчиками. Получив его, перечитайте каталог через GET /products?updated_since=…. При подписке на события товаров и остатков это событие добавляется автоматически.
customer.createdЗарегистрирован покупатель. В data — покупатель, как в GET /customers/{id}. Персональные данные: подписывайтесь, только если они нужны.
feedback.createdНовое сообщение из формы обратной связи. Посетитель написал через форму магазина.
pingПроверка из панели. В data — {"message": "…"}.
Выбор города
Ваш город можно выбрать через корзину или в комментариях к заказу