Webhooks
Owncast supports HTTP Webhooks to notify third-party applications (such as chatbots) about events on the stream. In other words: Webhooks will send events to your code when things happen on your Owncast server.
The following is a list of events you can get notified about.
| Event Type | webhook triggers when ... |
|---|---|
| CHAT | user sends a chat message |
| NAME_CHANGE | user changes their username |
| USER_JOINED | user joins the chat |
| USER_PARTED | a user's last active chat connection disconnects |
| STREAM_STARTED | an incoming RTMP stream is detected |
| STREAM_STOPPED | an incoming RTMP stream disconnects (e.g. OBS stops) |
| STREAM_TITLE_UPDATED | the title of the stream is updated |
| VISIBILITY-UPDATE | a previously sent chat message becomes visible/invisible (set by an Administrator/Moderator) |
| FEDIVERSE_ENGAGEMENT_FOLLOW | a Fediverse user follows your server |
How to accept webhooks
- Visit
/admin/webhookson your owncast server. - Click
Create Webhook. - Put in the full public URL to an endpoint that can receive this webhook.
- 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.
- Select the events you want to be notified of.
- Save this new webhook.
Your code
- In any language, on any kind of web server, create an endpoint that accepts a HTTP
POSTrequest. This is where Owncast will be sending events. - Each event payload will have a
typeproperty that states which event type it is, and aneventDataobject that includes specific properties of that event.
Verifying webhook requests
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:
- Parse
tandsfrom the header. - Reject the request if
tdiffers from the current time by more than 300 seconds. This blocks replayed deliveries. - Compute
HMAC-SHA256(secret, "<t>." + body), wherebodyis the exact raw request body. Don't re-serialize the JSON, since any formatting difference changes the signature. - Hex-encode the result and compare it to
susing 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.
High level webhooks
Webhooks utilize the HTTP POST method to push data to an endpoint. The request body of the webhook is plain JSON.
Thus the ContentType header for the request is application/json. Each webhook body follows a simple JSON structure.
{
"type": "",
"eventData": {}
}
where
- type gives information about what kind of event it is (one of the types from the table above).
- eventData gives more information on the event. The structure of
eventDatais different for eachtype.
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.
Examples of what eventData to expect for each event type are below.
Webhook Examples
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
}
}
bodyis the message rendered to sanitized HTML. Markdown is converted and emoji shortcodes are replaced with<img>tags.rawBodyis the original message text exactly as the user typed it.
Note: the field user in the chat was introduced with v0.0.8. Before v0.0.8 a simple string field with the name author was used.
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 is sent 10 seconds after a user's last active chat connection disconnects. If the user reconnects during that time, the event is canceled. 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"]
}
}
idsis a list of IDs of messages that had their visibility changed.visibleis the new visibility of those messages.useris alwaysnullfor this event.
FEDIVERSE_ENGAGEMENT_FOLLOW
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.idis an Owncast-generated webhook event ID. It is not the Fediverse actor ID or follow request ID.eventData.nameis the display name of the follower.eventData.usernameis the fulluser@domainhandle.eventData.imageis the URL to the follower's avatar.- Unlike the other events,
eventDatadoes not include astatusobject.
clientId vs. user.id
When a user is connected from multiple devices (or multiple browsers) at the same time with the same username, Owncast differentiates between their sessions with a clientId. Users can have multiple clientIds - a single clientId represents a single connection to Owncast.
clientId is a number, whereas user.id can container uppercase, lowercase and numeric characters.
Test webhooks on a local development environment
- Start Owncast locally (e.g. via docker).
- Visit
localhost:8080/admin, authenticate with Username:adminand the default streaming key:abc123. - Navigate to the "Integration" menu-block on the left side, click "Webhooks", then "Create Webhook".
- Set the Webhook Address to point to your application/integration (something like:
http://localhost:8100/webhooks/incoming). - Select the types of events that you want to receive.
- Press "OK" to save the webhook.
- Start your integration/application listening on the previously configured address.
- Optionally, start an interception proxy (e.g. Burp) if you want to inspect the HTTP messages beforehand.
- Trigger events yourself (e.g. write a message to the chat, connect/disconnect your streaming software to Owncast).
Test webhooks before writing any code
If you want to test how webhooks work before you write any code, create a test endpoint at RequestCatcher, and add the URL it gives you as a webhook in your admin and see the requests come through.
Test webhooks from a production instance of Owncast
If you already have an Owncast instance running in production, listening to the world wide web, you might want to make use ngrok to tunnel HTTP requests to your local development environment.
Improve this page
See something missing or incorrect? Edit this page and improve the documentation for everyone.
Related Documents
- Live stream notificationsSend notifications when your stream goes live.
- Owncast Web APIsIntegrate external code with Owncast over HTTP, using webhooks to receive events and access-token APIs to send actions.
- Send requests to the Owncast APIUse an access token to send chat messages, set the stream title, and perform other actions over the Owncast API.
- Plugin EventsEvery event your plugin can subscribe to (chat, stream, fediverse, filter, HTTP, and more), with its payload shape.
- ActivityPub & The Fediverse ProtocolA protocol-level reference for the ActivityPub activities Owncast sends and receives, so you can build a Fediverse application that interoperates with Owncast servers.
- Show Custom Action Buttons On Your PageYou can display external user interfaces into Owncast by registering external actions.


