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.

ManyCasts retries failed webhook deliveries up to 5 times with exponential back-off (1 min → 5 min → 30 min → 2 h → 8 h). After all retries fail, the event is marked as undelivered and visible in the Dashboard.

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"
}
Save the signingSecret immediately — it is shown only once and cannot be retrieved later. Use it to verify incoming payloads.

Event types

EventTrigger
stream.startedRTMP connection received; stream is now live
stream.endedRTMP connection dropped; broadcast finished
stream.errorIngest error detected (codec mismatch, bitrate spike, etc.)
listener.joinedA listener connected to a stream
listener.leftA listener disconnected from a stream
podcast.publishedA podcast episode was published and available in the feed
podcast.deletedA published episode was deleted
webhook.testSent 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

  1. Split the header on , to get the t (timestamp) and v1 (signature) values.
  2. Construct the signed payload: {t}.{raw_request_body}
  3. Compute HMAC-SHA256 of the signed payload using the signingSecret as key.
  4. Compare your result against the v1 value (constant-time comparison).
  5. Reject requests where t is 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-телом.

ManyCasts повторяет неудавшиеся доставки до 5 раз с экспоненциальной задержкой (1 мин → 5 мин → 30 мин → 2 ч → 8 ч). После исчерпания попыток событие помечается как недоставленное и отображается в Dashboard.

Создание вебхука

Вебхуки создаются в разделе 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.endedRTMP-соединение разорвано; вещание завершено
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.

Алгоритм проверки

  1. Разбейте заголовок по символу ,, получив значения t (временная метка) и v1 (подпись).
  2. Сформируйте подписанную строку: {t}.{тело_запроса}
  3. Вычислите HMAC-SHA256 этой строки, используя signingSecret как ключ.
  4. Сравните результат с v1 (сравнение должно быть защищено от тайминг-атак).
  5. Отклоняйте запросы, где 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("Подпись вебхука не совпадает")