Webhooks
Webhooks let you receive real-time HTTP notifications whenever an event occurs
in your ManyCasts account — a stream goes live, a listener joins, an episode 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.manycasts.com/v2/webhooks \
-H "Authorization: Bearer mc_sk_••••••••••••••••" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourserver.example/webhooks/manycasts",
"events": ["stream.started", "stream.ended", "podcast.published"],
"description": "Production webhook"
}'
Response
{
"id": "wh_01jz4nmrw8bq5vkcpx2t3ea",
"object": "webhook",
"url": "https://yourserver.example/webhooks/manycasts",
"events": ["stream.started", "stream.ended", "podcast.published"],
"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 |
|---|---|
stream.started | RTMP connection received; stream is now live |
stream.ended | RTMP connection dropped; broadcast finished |
stream.error | Ingest error detected (codec mismatch, bitrate spike, etc.) |
listener.joined | A listener connected to a stream |
listener.left | A listener disconnected from a stream |
podcast.published | A podcast episode was published and available in the feed |
podcast.deleted | A published episode 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/manycasts
Content-Type: application/json
ManyCasts-Signature: t=1711460331,v1=3c8a2f…
ManyCasts-Event: stream.started
{
"id": "evt_01kp2xcm9fjr7tlqbu3nz8w",
"object": "event",
"type": "stream.started",
"created": 1711460331,
"livemode": true,
"data": {
"object": {
"id": "stm_01hx83nqp5fk2rjavt9c6e",
"title": "Evening Show",
"status": "live",
"listenerCount": 0,
"startedAt": "2026-03-26T14:38:51Z"
}
}
}
Signature verification
Every webhook request includes a ManyCasts-Signature header.
Verify it using HMAC-SHA256 to ensure the request is genuinely from ManyCasts.
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.MC_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/manycasts', express.raw({ type: 'application/json' }), (req, res) => {
verifyWebhook(req.body.toString(), req.headers['manycasts-signature']);
const event = JSON.parse(req.body);
// handle event…
res.sendStatus(200);
});
Python example
import hashlib
import hmac
import time
SIGNING_SECRET = os.environ["MC_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-уведомления в реальном времени при наступлении
событий в вашем аккаунте ManyCasts — когда поток выходит в эфир, слушатель
подключается, эпизод публикуется и так далее. На ваш эндпоинт поступает
POST-запрос с JSON-телом.
Создание вебхука
Вебхуки создаются в разделе Dashboard → Settings → Webhooks или через API:
curl -X POST https://api.manycasts.com/v2/webhooks \
-H "Authorization: Bearer mc_sk_••••••••••••••••" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourserver.example/webhooks/manycasts",
"events": ["stream.started", "stream.ended", "podcast.published"],
"description": "Продакшн-вебхук"
}'
Ответ
{
"id": "wh_01jz4nmrw8bq5vkcpx2t3ea",
"object": "webhook",
"url": "https://yourserver.example/webhooks/manycasts",
"events": ["stream.started", "stream.ended", "podcast.published"],
"signingSecret": "whsec_9f2k3hx7p4qm1rlnbva8czdt",
"status": "active",
"createdAt": "2026-03-26T14:32:11Z"
}
signingSecret сразу — он показывается только один раз
и не может быть получен позже. Используйте его для
верификации входящих запросов.
Типы событий
| Событие | Когда отправляется |
|---|---|
stream.started | Получено RTMP-соединение; поток вышел в эфир |
stream.ended | RTMP-соединение разорвано; вещание завершено |
stream.error | Ошибка ингеста (несоответствие кодека, скачок битрейта и т.п.) |
listener.joined | Слушатель подключился к потоку |
listener.left | Слушатель отключился от потока |
podcast.published | Эпизод подкаста опубликован и доступен в фиде |
podcast.deleted | Опубликованный эпизод удалён |
webhook.test | Тестовое событие из раздела Dashboard |
Структура payload
Все события имеют единый формат. Поле data содержит затронутый
объект в его текущем состоянии.
POST https://yourserver.example/webhooks/manycasts
Content-Type: application/json
ManyCasts-Signature: t=1711460331,v1=3c8a2f…
ManyCasts-Event: stream.started
{
"id": "evt_01kp2xcm9fjr7tlqbu3nz8w",
"object": "event",
"type": "stream.started",
"created": 1711460331,
"livemode": true,
"data": {
"object": {
"id": "stm_01hx83nqp5fk2rjavt9c6e",
"title": "Вечернее шоу",
"status": "live",
"listenerCount": 0,
"startedAt": "2026-03-26T14:38:51Z"
}
}
}
Верификация подписи
Каждый запрос вебхука содержит заголовок ManyCasts-Signature.
Проверяйте его через HMAC-SHA256, чтобы убедиться, что запрос действительно
пришёл от ManyCasts.
Алгоритм проверки
- Разбейте заголовок по символу
,, получив значенияt(временная метка) иv1(подпись). - Сформируйте подписанную строку:
{t}.{тело_запроса} - Вычислите HMAC-SHA256 этой строки, используя
signingSecretкак ключ. - Сравните результат с
v1(сравнение должно быть защищено от тайминг-атак). - Отклоняйте запросы, где
tустарел более чем на 300 секунд.
Пример на Node.js
import crypto from 'node:crypto';
const SIGNING_SECRET = process.env.MC_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["MC_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("Подпись вебхука не совпадает")