跳至主要内容

Webhooks(Webhook 回调)

Owncast 支持 HTTP Webhooks,用于在流上发生事件时通知第三方应用(例如聊天机器人)。 换句话说:当你的 Owncast 服务器发生事件时,Webhooks 会将事件发送到你的代码。

下面是你可以接收通知的事件列表。

事件类型webhook 在...时触发
聊天用户发送聊天消息
用户名更改用户更改用户名
用户加入用户加入聊天
用户离开用户的最后一个活动聊天连接断开
流开始检测到传入的 RTMP 流
流停止传入的 RTMP 流断开(例如 OBS 停止)
流标题已更新流的标题已更新
可见性更新先前发送的聊天消息变为可见/不可见(由管理员/版主设置)
Fediverse 关注一位 Fediverse 用户关注了你的服务器

如何接收 Webhooks

  1. 在你的 Owncast 服务器上访问 /admin/webhooks
  2. 点击 Create Webhook
  3. 输入可以接收该 webhook 的端点的完整公网 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. 保存此新 webhook。

你的代码

  1. 在任何语言、任何类型的 Web 服务器上,创建一个接受 HTTP POST 请求的端点。 这是 Owncast 发送事件的目标。
  2. 每个事件负载都有一个 type 属性,指明事件类型,以及一个 eventData 对象,包含该事件的具体属性。

验证 webhook 请求

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.

Webhooks 概览

Webhooks 使用 HTTP POST 方法将数据推送到端点。 webhook 的请求体是纯 JSON。 因此请求的 ContentType 头为 application/json。 每个 webhook 请求体遵循一个简单的 JSON 结构。

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

其中

  • type 提供有关事件类型的信息(来自上表的某一种类型)。
  • 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 样例。

Webhook 示例

聊天

{
"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 的简单字符串字段。

用户名更改

{
"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"
}
}

用户加入

{
"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 会在用户最后一个活动聊天连接断开 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
}
}
}

流开始

{
"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"
}
}

流停止

{
"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"
}
}

流标题已更新

{
"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"
}
}

可见性更新

{
"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 关注

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 生成的 webhook 事件 ID。 它不是 Fediverse 的 actor ID 或关注请求 ID。
  • eventData.name 是关注者的显示名称。
  • eventData.username 是完整的 user@domain 标识。
  • eventData.image 是关注者头像的 URL。
  • Unlike the other events, eventData does not include a status object.

clientId 与 user.id

当用户使用相同用户名从多个设备(或多个浏览器)同时连接时,Owncast 使用 clientId 来区分它们的会话。 用户可以拥有多个 clientId——单个 clientId 表示一次对 Owncast 的连接。

clientId 是一个数字,而 user.id 可以包含大写、小写和数字字符。

在本地开发环境测试 webhooks

  1. 在本地启动 Owncast(例如通过 docker)。
  2. 访问 localhost:8080/admin,使用用户名 admin 和默认流密钥 abc123 进行认证。
  3. 在左侧的 “Integration” 菜单块中,点击 “Webhooks”,然后点击 “Create Webhook”。
  4. 将 Webhook 地址设置为指向你的应用/集成(例如:http://localhost:8100/webhooks/incoming)。
  5. 选择你想接收的事件类型。
  6. 按 “OK” 保存 webhook。
  7. 启动你的集成/应用,使其在先前配置的地址上监听。
    1. 可选:如果你想事先检查 HTTP 消息,可以启动拦截代理(例如 Burp)。
  8. 自行触发事件(例如向聊天写入消息,或将你的推流软件连接/断开到 Owncast)。

在编写任何代码之前测试 webhooks

如果你想在编写代码之前测试 webhooks 的工作方式,可以在 RequestCatcher 创建一个测试端点,然后在管理后台将其提供的 URL 添加为 webhook,查看请求是否通过。

从生产环境的 Owncast 实例测试 webhooks

如果你已经有一个在生产环境中运行并对全网监听的 Owncast 实例,你可能想使用 ngrok 将 HTTP 请求隧道到本地开发环境。


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