Webhooks
Webhooks let you receive real-time HTTP notifications whenever an event occurs
in your SmartElektroHub account — a stream goes live, a subscriber joins, a record is
published, and more. Your endpoint receives a POST request with a
JSON payload.
Creating a webhook
Webhooks can be created from the Dashboard under Settings → Webhooks, or via the API:
curl -X POST https://api.smart-elektro.ru/v2/webhooks \
-H "Authorization: Bearer eh_sk_••••••••••••••••" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourserver.example/webhooks/sehub",
"events": ["device.online", "device.offline", "report.generated"],
"description": "Production webhook"
}'
Response
{
"id": "wh_01jz4nmrw8bq5vkcpx2t3ea",
"object": "webhook",
"url": "https://yourserver.example/webhooks/sehub",
"events": ["device.online", "device.offline", "report.generated"],
"signingSecret": "whsec_9f2k3hx7p4qm1rlnbva8czdt",
"status": "active",
"createdAt": "2026-03-26T14:32:11Z"
}
signingSecret immediately — it is shown only once and
cannot be retrieved later. Use it to verify incoming
payloads.
Event types
| Event | Trigger |
|---|---|
device.online | MQTT connection received; stream is now live |
device.offline | MQTT connection dropped; transmission finished |
device.error | Ingestion error detected (format mismatch, bitrate spike, etc.) |
subscriber.connected | A subscriber connected to a stream |
subscriber.disconnected | A subscriber disconnected from a stream |
report.generated | A report record was published and available in the feed |
report.deleted | A published record was deleted |
webhook.test | Sent when you click Send test event in the Dashboard |
Payload structure
All events share the same envelope. The data field contains the
affected object in its current state.
POST https://yourserver.example/webhooks/sehub
Content-Type: application/json
SmartElektroHub-Signature: t=1711460331,v1=3c8a2f…
SmartElektroHub-Event: device.online
{
"id": "evt_01kp2xcm9fjr7tlqbu3nz8w",
"object": "event",
"type": "device.online",
"created": 1711460331,
"livemode": true,
"data": {
"object": {
"id": "stm_01hx83nqp5fk2rjavt9c6e",
"title": "Evening Show",
"status": "live",
"subscriberCount": 0,
"startedAt": "2026-03-26T14:38:51Z"
}
}
}
Signature verification
Every webhook request includes a SmartElektroHub-Signature header.
Verify it using HMAC-SHA256 to ensure the request is genuinely from SmartElektroHub.
Verification algorithm
- Split the header on
,to get thet(timestamp) andv1(signature) values. - Construct the signed payload:
{t}.{raw_request_body} - Compute HMAC-SHA256 of the signed payload using the
signingSecretas key. - Compare your result against the
v1value (constant-time comparison). - Reject requests where
tis more than 300 seconds old.
Node.js example
import crypto from 'node:crypto';
const SIGNING_SECRET = process.env.EH_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;
function verifyWebhook(rawBody, signatureHeader) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const timestamp = parseInt(parts.t, 10);
if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) {
throw new Error('Webhook timestamp out of tolerance');
}
const signedPayload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', SIGNING_SECRET)
.update(signedPayload)
.digest('hex');
const received = parts.v1;
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
throw new Error('Webhook signature mismatch');
}
}
// Express usage
app.post('/webhooks/sehub', express.raw({ type: 'application/json' }), (req, res) => {
verifyWebhook(req.body.toString(), req.headers['sehub-signature']);
const event = JSON.parse(req.body);
// handle event…
res.sendStatus(200);
});
Python example
import hashlib
import hmac
import time
SIGNING_SECRET = os.environ["EH_WEBHOOK_SECRET"]
TOLERANCE = 300
def verify_webhook(raw_body: bytes, signature_header: str):
parts = dict(p.split("=", 1) for p in signature_header.split(","))
ts = int(parts["t"])
if abs(time.time() - ts) > TOLERANCE:
raise ValueError("Webhook timestamp out of tolerance")
signed = f"{ts}.{raw_body.decode()}".encode()
expected = hmac.new(SIGNING_SECRET.encode(), signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts["v1"]):
raise ValueError("Webhook signature mismatch")
Вебхуки
Вебхуки позволяют получать HTTP-уведомления в реальном времени при наступлении
событий в вашем аккаунте SmartElektroHub — когда поток выходит в эфир, подписчик
подключается, запись публикуется и так далее. На ваш эндпоинт поступает
POST-запрос с JSON-телом.
Создание вебхука
Вебхуки создаются в разделе Dashboard → Settings → Webhooks или через API:
curl -X POST https://api.smart-elektro.ru/v2/webhooks \
-H "Authorization: Bearer eh_sk_••••••••••••••••" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourserver.example/webhooks/sehub",
"events": ["device.online", "device.offline", "report.generated"],
"description": "Продакшн-вебхук"
}'
Ответ
{
"id": "wh_01jz4nmrw8bq5vkcpx2t3ea",
"object": "webhook",
"url": "https://yourserver.example/webhooks/sehub",
"events": ["device.online", "device.offline", "report.generated"],
"signingSecret": "whsec_9f2k3hx7p4qm1rlnbva8czdt",
"status": "active",
"createdAt": "2026-03-26T14:32:11Z"
}
signingSecret сразу — он показывается только один раз
и не может быть получен позже. Используйте его для
верификации входящих запросов.
Типы событий
| Событие | Когда отправляется |
|---|---|
device.online | Получено MQTT-соединение; поток вышел в эфир |
device.offline | MQTT-соединение разорвано; мониторинг завершено |
device.error | Ошибка ингеста (несоответствие протокола, скачок битрейта и т.п.) |
subscriber.connected | Подписчик подключился к потоку |
subscriber.disconnected | Подписчик отключился от потока |
report.generated | Запись отчёта опубликован и доступен в фиде |
report.deleted | Опубликованный запись удалён |
webhook.test | Тестовое событие из раздела Dashboard |
Структура payload
Все события имеют единый формат. Поле data содержит затронутый
объект в его текущем состоянии.
POST https://yourserver.example/webhooks/sehub
Content-Type: application/json
SmartElektroHub-Signature: t=1711460331,v1=3c8a2f…
SmartElektroHub-Event: device.online
{
"id": "evt_01kp2xcm9fjr7tlqbu3nz8w",
"object": "event",
"type": "device.online",
"created": 1711460331,
"livemode": true,
"data": {
"object": {
"id": "stm_01hx83nqp5fk2rjavt9c6e",
"title": "Вечернее шоу",
"status": "live",
"subscriberCount": 0,
"startedAt": "2026-03-26T14:38:51Z"
}
}
}
Верификация подписи
Каждый запрос вебхука содержит заголовок SmartElektroHub-Signature.
Проверяйте его через HMAC-SHA256, чтобы убедиться, что запрос действительно
пришёл от SmartElektroHub.
Алгоритм проверки
- Разбейте заголовок по символу
,, получив значенияt(временная метка) иv1(подпись). - Сформируйте подписанную строку:
{t}.{тело_запроса} - Вычислите HMAC-SHA256 этой строки, используя
signingSecretкак ключ. - Сравните результат с
v1(сравнение должно быть защищено от тайминг-атак). - Отклоняйте запросы, где
tустарел более чем на 300 секунд.
Пример на Node.js
import crypto from 'node:crypto';
const SIGNING_SECRET = process.env.EH_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;
function verifyWebhook(rawBody, signatureHeader) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const timestamp = parseInt(parts.t, 10);
if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) {
throw new Error('Временная метка вебхука вне допустимого диапазона');
}
const signedPayload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', SIGNING_SECRET)
.update(signedPayload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) {
throw new Error('Подпись вебхука не совпадает');
}
}
Пример на Python
import hashlib, hmac, os, time
SIGNING_SECRET = os.environ["EH_WEBHOOK_SECRET"]
TOLERANCE = 300
def verify_webhook(raw_body: bytes, signature_header: str):
parts = dict(p.split("=", 1) for p in signature_header.split(","))
ts = int(parts["t"])
if abs(time.time() - ts) > TOLERANCE:
raise ValueError("Временная метка вебхука вне допустимого диапазона")
signed = f"{ts}.{raw_body.decode()}".encode()
expected = hmac.new(SIGNING_SECRET.encode(), signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts["v1"]):
raise ValueError("Подпись вебхука не совпадает")