メインコンテンツへスキップ

ウェブフック(Webhooks)?

Owncastは、ストリーム上のイベントについてサードパーティのアプリケーション(チャットボットなど)に通知するためのHTTPウェブフックをサポートしています。 言い換えると:Owncastサーバーで何かが起こると、ウェブフックがイベントをあなたのコードに送信します。

以下は通知を受け取ることのできるイベントの一覧です。

イベントタイプウェブフックがトリガーされるとき...
チャットユーザーがチャットメッセージを送信したとき
名前変更ユーザーがユーザー名を変更したとき
ユーザー参加ユーザーがチャットに参加したとき
ユーザー退出ユーザーの最後のアクティブなチャット接続が切断されたとき
配信開始着信RTMPストリームが検出されたとき
配信停止着信RTMPストリームが切断されたとき(例: OBSが停止した場合)
配信タイトル更新配信のタイトルが更新されたとき
表示状態更新以前送信されたチャットメッセージの表示/非表示が切り替えられたとき(管理者/モデレーターによって設定)
FediverseフォローFediverseユーザーがあなたのサーバーをフォローしたとき

ウェブフックを受け取る方法

  1. Owncastサーバーで/admin/webhooksにアクセスしてください。
  2. Create Webhookをクリックしてください。
  3. このウェブフックを受信できるエンドポイントの、公開された完全なURLを入力してください。
  4. Keep or replace the pre-filled webhook secret. Owncast uses it to sign every delivery, and you'll use it to verify them. You can reveal or copy it later from the webhook list.
  5. 通知を受けたいイベントを選択してください。
  6. この新しいウェブフックを保存してください。

あなたのコード

  1. 任意の言語、任意の種類のウェブサーバーで、HTTPのPOSTリクエストを受け付けるエンドポイントを作成します。 ここがOwncastがイベントを送信する場所です。
  2. 各イベントペイロードには、どのイベントタイプかを示すtypeプロパティと、そのイベントの特定のプロパティを含むeventDataオブジェクトが含まれます。

ウェブフックリクエストの検証

Signed webhooks require Owncast v0.3.0

Owncast 0.3.0 signs every webhook delivery. Earlier releases send unsigned requests with no signature header.

Every webhook has a secret, created with the webhook in the admin. Each delivery includes an owncast-signature header:

owncast-signature: t=1718400000.s=5f8a1c...

t is the Unix timestamp when the request was signed. s is a hex-encoded HMAC-SHA256 signature.

To verify a delivery:

  1. Parse t and s from the header.
  2. Reject the request if t differs from the current time by more than 300 seconds. This blocks replayed deliveries.
  3. Compute HMAC-SHA256(secret, "<t>." + body), where body is the exact raw request body. Don't re-serialize the JSON, since any formatting difference changes the signature.
  4. Hex-encode the result and compare it to s using a constant-time comparison.

A Node.js example:

const crypto = require("crypto");

function verifyWebhook(signatureHeader, rawBody, secret) {
const parts = {};
for (const part of signatureHeader.split(".")) {
const [key, value] = part.trim().split("=");
if (key === "t" || key === "s") parts[key] = value;
}
if (!parts.t || !parts.s) return false;

// Reject replays outside a 5 minute window.
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;

const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");

if (parts.s.length !== expected.length) return false;
return crypto.timingSafeEqual(Buffer.from(parts.s), Buffer.from(expected));
}

Verification is optional. If you skip it, treat your endpoint as something anyone on the internet could call.

ウェブフックの概要

ウェブフックはHTTP POSTメソッドを利用してデータをエンドポイントにプッシュします。 ウェブフックのリクエストボディはプレーンなJSONです。 したがってリクエストのContentTypeヘッダーはapplication/jsonです。 各ウェブフックボディはシンプルなJSON構造に従います。

{
"type": "",
"eventData": {}
}

ここで

  • type はどの種類のイベントかを示します(上の表のタイプのうちの1つ)。
  • eventData はイベントに関するより詳細な情報を提供します。 eventDataの構造は各typeごとに異なります。

Every eventData also includes a status object describing the current stream state and a serverURL string identifying the server that sent the event. The one exception is FEDIVERSE_ENGAGEMENT_FOLLOW, which carries serverURL but no status.

各イベントタイプごとに期待されるeventDataの例は以下に示します。

ウェブフックの例

CHAT

{
"type": "CHAT",
"eventData": {
"status": {
"lastConnectTime": "2021-08-12T07:45:03.986220954Z",
"lastDisconnectTime": null,
"versionNumber": "0.2.5",
"streamTitle": "",
"viewerCount": 3,
"overallMaxViewerCount": 7,
"sessionMaxViewerCount": 4,
"online": true
},
"serverURL": "https://stream.example.com",
"user": {
"id": "qSRQpeM7R",
"displayName": "lazyDaisy",
"displayColor": 182,
"createdAt": "2021-08-12T07:51:37.470812684Z",
"previousNames": ["lazyDaisy"],
"nameChangedAt": "2022-09-19T12:33:59.42313245+02:00",
"isBot": false,
"authenticated": false
},
"timestamp": "2021-08-12T07:53:12.061982913Z",
"body": "\u003cp\u003ehello world \u003cimg class=\"emoji\" alt=\":beerparrot:\" title=\":beerparrot:\" src=\"/img/emoji/beerparrot.gif\"\u003e\u003c/p\u003e",
"rawBody": "hello world :beerparrot:",
"id": "j-rXteG7R",
"clientId": 2,
"visible": true
}
}
  • body is the message rendered to sanitized HTML. Markdown is converted and emoji shortcodes are replaced with <img> tags.
  • rawBody is the original message text exactly as the user typed it.

注: チャットのuserフィールドはv0.0.8で導入されました。 v0.0.8以前はauthorという名前の単純な文字列フィールドが使用されていました。

NAME_CHANGE

{
"type": "NAME_CHANGE",
"eventData": {
"status": {
"lastConnectTime": "2021-08-12T07:45:03.986220954Z",
"lastDisconnectTime": null,
"versionNumber": "0.2.5",
"streamTitle": "",
"viewerCount": 3,
"overallMaxViewerCount": 7,
"sessionMaxViewerCount": 4,
"online": true
},
"serverURL": "https://stream.example.com",
"id": "GsxeK6MIg",
"timestamp": "2022-09-19T12:33:59.423278816+02:00",
"user": {
"id": "qSRQpeM7R",
"displayName": "NotSoLazyDaisy",
"displayColor": 182,
"createdAt": "2021-08-12T07:51:37.470812684Z",
"previousNames": ["lazyDaisy"],
"nameChangedAt": "2022-09-19T12:33:59.423278816+02:00",
"isBot": false,
"authenticated": false
},
"newName": "NotSoLazyDaisy"
}
}

USER_JOINED

{
"type": "USER_JOINED",
"eventData": {
"status": {
"lastConnectTime": "2021-08-12T07:45:03.986220954Z",
"lastDisconnectTime": null,
"versionNumber": "0.2.5",
"streamTitle": "",
"viewerCount": 3,
"overallMaxViewerCount": 7,
"sessionMaxViewerCount": 4,
"online": true
},
"serverURL": "https://stream.example.com",
"id": "wAgcTeM7g",
"timestamp": "2021-08-12T08:19:28.921355401Z",
"user": {
"id": "yFgco6M7R",
"displayName": "laughing-cray",
"displayColor": 257,
"createdAt": "2021-08-12T08:19:28.759651178Z",
"previousNames": ["laughing-cray"],
"nameChangedAt": "0001-01-01T00:00:00Z",
"isBot": false,
"authenticated": false
}
}
}

USER_PARTED

USER_PARTEDはユーザーの最後のアクティブなチャット接続が切断されてから10秒後に送信されます。 その間にユーザーが再接続した場合、イベントはキャンセルされます。 Disabling visible join and part messages only hides the message in chat. The webhook is still sent.

{
"type": "USER_PARTED",
"eventData": {
"status": {
"lastConnectTime": "2021-08-12T07:45:03.986220954Z",
"lastDisconnectTime": null,
"versionNumber": "0.2.5",
"streamTitle": "",
"viewerCount": 3,
"overallMaxViewerCount": 7,
"sessionMaxViewerCount": 4,
"online": true
},
"serverURL": "https://stream.example.com",
"id": "Ws4gTeM7R",
"timestamp": "2021-08-12T08:20:01.061982913Z",
"user": {
"id": "yFgco6M7R",
"displayName": "laughing-cray",
"displayColor": 257,
"createdAt": "2021-08-12T08:19:28.759651178Z",
"previousNames": ["laughing-cray"],
"nameChangedAt": "0001-01-01T00:00:00Z",
"isBot": false,
"authenticated": false
}
}
}

STREAM_STARTED

{
"type": "STREAM_STARTED",
"eventData": {
"id": "WtokptnVR",
"name": "Owncast",
"serverURL": "https://stream.example.com",
"status": {
"lastConnectTime": "2022-09-19T12:30:26.97907142+02:00",
"lastDisconnectTime": null,
"versionNumber": "0.2.5",
"streamTitle": "",
"viewerCount": 0,
"overallMaxViewerCount": 7,
"sessionMaxViewerCount": 0,
"online": true
},
"streamTitle": "",
"summary": "Welcome to your new Owncast server! This description can be changed in the admin. Visit https://owncast.online/docs/configuration/ to learn more.",
"timestamp": "2022-09-19T12:30:26.97907142+02:00"
}
}

STREAM_STOPPED

{
"type": "STREAM_STOPPED",
"eventData": {
"id": "YP-aptn4g",
"name": "Owncast",
"serverURL": "https://stream.example.com",
"status": {
"lastConnectTime": "2022-09-19T12:30:26.97907142+02:00",
"lastDisconnectTime": "2022-09-19T12:40:21.205872269+02:00",
"versionNumber": "0.2.5",
"streamTitle": "",
"viewerCount": 0,
"overallMaxViewerCount": 7,
"sessionMaxViewerCount": 2,
"online": false
},
"streamTitle": "",
"summary": "Welcome to your new Owncast server! This description can be changed in the admin. Visit https://owncast.online/docs/configuration/ to learn more.",
"timestamp": "2022-09-19T12:40:21.205872269+02:00"
}
}

STREAM_TITLE_UPDATED

{
"type": "STREAM_TITLE_UPDATED",
"eventData": {
"id": "DmeikEf4Rz",
"name": "New Owncast Server",
"serverURL": "https://stream.example.com",
"status": {
"lastConnectTime": null,
"lastDisconnectTime": "2024-10-24T22:35:05Z",
"versionNumber": "0.1.3",
"streamTitle": "Test stream title change",
"viewerCount": 0,
"overallMaxViewerCount": 7,
"sessionMaxViewerCount": 2,
"online": false
},
"streamTitle": "Test stream title change",
"summary": "This is a new live video streaming server powered by Owncast.",
"timestamp": "2023-03-27T21:50:10.121391094-07:00"
}
}

VISIBILITY-UPDATE

{
"type": "VISIBILITY-UPDATE",
"eventData": {
"status": {
"lastConnectTime": "2022-09-19T12:30:26.97907142+02:00",
"lastDisconnectTime": null,
"versionNumber": "0.2.5",
"streamTitle": "",
"viewerCount": 3,
"overallMaxViewerCount": 7,
"sessionMaxViewerCount": 4,
"online": true
},
"serverURL": "https://stream.example.com",
"id": "zqGupt7VR",
"timestamp": "2022-09-19T12:44:28.225779601+02:00",
"user": null,
"visible": false,
"ids": ["-Zzltt74g", "rvd2ppn4g"]
}
}
  • ids is a list of IDs of messages that had their visibility changed.
  • visible is the new visibility of those messages.
  • user is always null for this event.

FEDIVERSE_ENGAGEMENT_FOLLOW

Webhook server URLs require Owncast v0.3.0

Owncast 0.3.0 adds the serverURL field to this event. Earlier releases send only id, timestamp, name, username, and image.

{
"type": "FEDIVERSE_ENGAGEMENT_FOLLOW",
"eventData": {
"id": "AqilY4hDR",
"timestamp": "2026-04-13T19:17:12.528099886Z",
"name": "Test Follower",
"username": "testfollower@fake-mastodon.example.com",
"image": "https://fake-mastodon.example.com/avatars/testfollower.png",
"serverURL": "https://stream.example.com"
}
}
  • eventData.idはOwncastによって生成されたウェブフックイベントのIDです。 これはFediverseのアクターIDやフォローリクエストIDではありません。
  • eventData.nameはフォロワーの表示名です。
  • eventData.usernameは完全なuser@domainハンドルです。
  • eventData.imageはフォロワーのアバターへのURLです。
  • Unlike the other events, eventData does not include a status object.

clientId vs. user.id

同じユーザー名で複数のデバイス(または複数のブラウザ)から同時に接続している場合、OwncastはセッションをclientIdで区別します。 ユーザーは複数のclientIdを持つことができます。単一のclientIdはOwncastへの単一の接続を表します。

clientIdは数値ですが、user.idは大文字、小文字、数字の文字を含むことができます。

ローカル開発環境でウェブフックをテストする

  1. ローカルでOwncastを起動します(例: docker経由)。
  2. localhost:8080/adminにアクセスし、ユーザー名adminとデフォルトのストリーミングキーabc123で認証します。
  3. 左側の「Integration」メニューブロックに移動し、「Webhooks」をクリックしてから「Create Webhook」をクリックします。
  4. Webhookアドレスをアプリケーション/統合先(例: http://localhost:8100/webhooks/incoming)に設定します。
  5. 受信したいイベントの種類を選択します。
  6. 「OK」を押してウェブフックを保存します。
  7. 先ほど設定したアドレスで統合/アプリケーションを起動して受信を待ちます。
    1. 必要に応じて、HTTPメッセージを事前に確認したい場合はインターセプトプロキシ(例: Burp)を起動します。
  8. 自分でイベントを発生させます(例: チャットにメッセージを書く、ストリーミングソフトをOwncastに接続/切断する)。

コードを書く前にウェブフックをテストする

コードを書く前にウェブフックの動作を確認したい場合は、RequestCatcherでテストエンドポイントを作成し、管理画面にそのURLをウェブフックとして登録してリクエストが届くのを確認してください。

本番環境のOwncastインスタンスからウェブフックをテストする

既に本番環境でインターネットに公開されているOwncastインスタンスがある場合、HTTPリクエストをローカル開発環境にトンネルするためにngrokを利用すると便利です。


Improve this page

See something missing or incorrect? Edit this page and improve the documentation for everyone.

Contributors to this documentation
Gabe KangasGabe Kangastaintedcyphertaintedcypher
T
Tournesol
D
Dev Gupta
L
Lili
mtabrizmtabriz
R
Raffael Rehberger