Skip to main content

Owncast Plugin APIs

The Owncast plugin runtime exposes a single global, owncast, with the host functions your plugin can call. Most methods require the matching permission in your manifest. A call without it never reaches Owncast: the host logs the denial and the call returns an empty or zero value. A few are ambient and need no permission: logging, timers, reading bundled assets, and owncast.config.get.

Plugin APIs require Owncast v0.3.0

Plugins require Owncast 0.3.0 or later.

Calls are shown for both SDKs, pick your language with the tabs. See JavaScript or Python for setup. (JavaScript method names are camelCase, Python uses snake_case, so sendAction becomes send_action, and so on.)

Logging

owncast.log.info(message), .warning(message), and .error(message)

Write an operator-visible entry to Owncast's server log. Owncast records the calling plugin's slug and the matching info, warning, or error severity. It replaces control characters with spaces so each entry stays on one line, then truncates messages longer than 4 KiB.

owncast.log.info('sync started');
owncast.log.warning('provider response is incomplete');
owncast.log.error('sync failed');

Ambient: no permission required. See the paired chat-logger examples for JavaScript and Python.

Chat

Building a chat bot, moderation tool, or filter? Start with Chat plugins.

owncast.chat.send(text)

Post a chat message. Sent as your plugin's bot identity (display name from bot.displayName or name in your manifest).

owncast.chat.send('hello chat');

Requires chat.send.

owncast.chat.sendAction(text)

Post an action-style ("/me") message.

owncast.chat.sendAction('is now live');

Requires chat.send.

owncast.chat.system(body)

Post a server-announcement message. No bot identity attached. The body renders inline as HTML, so use this for short, server-attributed notices like "the stream is starting in 5 minutes". Treat the body as untrusted-HTML output: don't interpolate user input into it without escaping.

Requires chat.send.

owncast.chat.sendTo(clientId, text)

Send a private message to a single connected client.

Requires chat.send.

owncast.chat.replyTo(msg, text)

Whisper a reply back to whoever sent a chat message. Pass the chat message from onChatMessage/filterChatMessage (or a bare client ID). Returns false if the sender's connection is unknown (no client ID), so you can fall back to a public send. Sugar over sendTo(clientId, text).

onChatMessage(msg) {
if (!owncast.chat.replyTo(msg, "got it")) {
owncast.chat.send("got it");
}
}

Requires chat.send.

owncast.chat.history(limit?)

Return the most recent chat messages, each with id, user, body, and timestamp. limit defaults to 50.

Requires chat.history.

owncast.chat.clients()

Return the list of currently-connected chat clients, each with id, userId?, displayName?, connectedAt?, userAgent?, ipAddress?, and messageCount. id is the per-connection client ID used by chat.kick.

Requires chat.history.

owncast.chat.deleteMessage(messageId)

Hide a chat message from viewers.

Requires chat.moderate.

owncast.chat.kick(clientId)

Disconnect a chat client.

Requires chat.moderate.

Chat identity

Every plugin has exactly one chat identity, the bot Owncast provisions when your plugin is installed. The display name is your manifest's bot.displayName if set, otherwise its name, with IsBot: true. Both send and sendAction post as this identity, through Owncast's normal chat pipeline (filters, rate limits, moderation). Plugins cannot post under arbitrary names or impersonate real users.

The bot user is keyed on the plugin's slug so the identity survives manifest edits to name or bot.displayName. If you need multiple chat personas, ship multiple plugins.

Users

owncast.users.list() and owncast.users.get(id)

Read the chat user list or a single user record.

const users = owncast.users.list();
const alice = owncast.users.get('u-alice');

Requires users.read.

owncast.users.setEnabled(id, enabled, reason?)

Enable or disable a chat user.

owncast.users.setEnabled('u-spammer', false, 'spam');

Requires users.moderate.

owncast.users.banIP(ip)

Ban an IP from joining chat.

owncast.users.banIP('203.0.113.42');

Requires users.moderate.

owncast.users.register({ authId, displayName?, scopes?, profileUrl?, handle?, public? })

Find or create an authenticated Owncast user for an external identity and return { userId }. Pass the provider's stable authId without adding your plugin slug. The host stores the slug separately as the identity provider, so plugins cannot collide with or spoof each other's users. displayName seeds a new user's name, and non-empty scopes such as ["MODERATOR"] are applied on each call.

The optional profileUrl, handle, and public fields describe a verified external identity. profileUrl must be empty or an absolute HTTP(S) URL. handle is the provider's verified label, such as a GitHub login or fediverse handle. Set public to true only after the viewer opts into public display. It defaults to false. These profile fields are captured when the identity is first registered. Later calls with the same authId return the existing user but do not change the stored profile fields.

const { userId } = owncast.users.register({
authId: 'github:583231',
displayName: 'octocat',
profileUrl: 'https://github.com/octocat',
handle: 'octocat',
public: false, // Set true only after the viewer opts in.
});

Requires users.register.

Authentication

These power a viewer-authentication gate. grantSession and endSession only work inside an onHttpRequest handler, because the host attaches the session cookie to the in-flight HTTP response.

owncast.auth.grantSession({ userId, ttl? })

Issue a signed session for an already-registered user (the userId from owncast.users.register). The host mints, signs, and attaches the session cookie to the current response; your plugin never sees the token, so it can't forge or leak it. ttl is an optional lifetime in seconds (0/omitted uses the host default of 24 hours).

const { userId } = owncast.users.register({ authId: 'shared', displayName: 'Guest' });
owncast.auth.grantSession({ userId });
return { status: 302, headers: { Location: returnTo } };

Requires auth.gate.

owncast.auth.endSession()

Clear the current viewer's session cookie on this response to sign them out. Your plugin still controls the redirect (and may bounce on to the provider's own logout).

owncast.auth.endSession();
return { status: 302, headers: { Location: '/' } };

Requires auth.gate.

Storage

owncast.kv.get(key) and owncast.kv.set(key, value)

Per-plugin key/value store, namespaced by your plugin's slug. Values are strings.

For richer types, use the JSON helpers (getJSON / setJSON, get_json / set_json in Python) instead of parsing and serializing yourself. The JSON getter returns the fallback when the key is unset or holds invalid JSON. Plugins cannot read each other's keys.

owncast.kv.set('count', '1');
const n = Number(owncast.kv.get('count') ?? '0');

owncast.kv.setJSON('prefs', { theme: 'dark' });
const prefs = owncast.kv.getJSON('prefs', {});

Requires storage.kv.

owncast.storage.upload(name, data)

Upload a file to Owncast's public file area. JavaScript accepts a Uint8Array or string. Python accepts bytes or str. Raw bytes are preserved, while strings are encoded as UTF-8. JavaScript returns { url } or null. Python returns a dict accessed as result["url"], or None.

Requires storage.upload.

owncast.fs.*

A private, sandboxed filesystem at data/plugin-storage/<your-slug>/files/. Unlike owncast.storage.upload, these files stay server-side: they're never served over HTTP. Paths are relative to your sandbox root. The host confines every path to your own directory (a plugin cannot read another plugin's files, and ../ or absolute paths collapse back inside the sandbox). Parent directories are created as needed on write.

JavaScriptPythonReturns
fs.read(path)fs.read(path)Uint8Array / bytes, or null / None if missing
fs.readText(path)fs.read_text(path)UTF-8 string / str, or null / None if missing
fs.write(path, data)fs.write(path, data){ error? }
fs.list(dir)fs.list(dir)entry names (a missing directory is empty)
fs.delete(path)fs.delete(path){ error? } for a file or empty directory
fs.exists(path)fs.exists(path)boolean

fs.read preserves the original bytes. fs.readText and fs.read_text decode UTF-8. Python replaces malformed byte sequences when decoding. fs.write preserves a JavaScript Uint8Array or Python bytes, and UTF-8 encodes strings. fs.write and fs.delete return {} on success. If the host rejects the operation, they return { error } with the reason.

owncast.fs.write('notes/log.txt', 'hello');
const text = owncast.fs.readText('notes/log.txt');

const data = new Uint8Array([0xff, 0x00, 0x80]);
owncast.fs.write('cache/data.bin', data);
const stored = owncast.fs.read('cache/data.bin');
if (stored) owncast.storage.upload('data.bin', stored);

Requires storage.fs.

owncast.sql.*

One private SQLite database per plugin, at data/plugin-storage/<your-slug>/db/plugin.db, separate from Owncast's own database and from the storage.fs sandbox. The sandbox is rooted at files/, so db/ is not a path owncast.fs.* refuses but one it cannot express, and the filesystem quota walk covers files/ only, so the two quotas stay independent. Reach for this over storage.kv when you need to sort, filter, or aggregate rather than just remember a value.

MethodReturns
sql.exec(sql, params?){ rowsAffected, lastInsertId }
sql.query(sql, params?)rows as objects keyed by column name
sql.queryRow(sql, params?)the first row object, or null when nothing matched

In Python queryRow is query_row, rows come back as dicts, and query_row returns None when nothing matched. An error throws in JavaScript and raises RuntimeError in Python. Parameters are null/None, booleans, numbers, or strings. Anything else is refused.

owncast.sql.exec(`CREATE TABLE IF NOT EXISTS chatters (
user_id TEXT PRIMARY KEY,
messages INTEGER NOT NULL DEFAULT 0
)`);

owncast.sql.exec(
`INSERT INTO chatters (user_id, messages) VALUES (?, 1)
ON CONFLICT (user_id) DO UPDATE SET messages = messages + 1`,
[msg.user.id],
);

const top = owncast.sql.query(
'SELECT user_id, messages FROM chatters ORDER BY messages DESC LIMIT ?',
[5],
);
const mine = owncast.sql.queryRow('SELECT messages FROM chatters WHERE user_id = ?', [msg.user.id]);

Each exec call runs as one host-owned transaction. A multi-statement batch commits whole or leaves the database untouched, so a schema migration can't half-apply. A plugin cannot leave a transaction open across calls, so there's nothing to clean up either.

query never hands back a silently short result. A query that overruns the row cap or the result budget is an error telling you to add a LIMIT, so write the bound you actually want when a table grows with your audience. queryRow reads a single row, which keeps it cheap on a table query is too big for.

LimitValue
Encoded request64 KiB total JSON
Bound parameters64 per call
Returned column value1 MiB
Encoded query result1 MiB
Rows returned10000
Call duration2 seconds
Database size128 MiB

Ordinary SQL is unaffected: DDL, DML, indexes, views, triggers, ORDER BY, recursive CTEs, subqueries, UNION, and the json1 functions all work. Refused in every host: ATTACH, DETACH, every PRAGMA (reads included), temporary-schema DDL both as keywords (CREATE TEMP TABLE / INDEX / TRIGGER / VIEW) and schema-qualified (CREATE TABLE temp.x), load_extension(), VACUUM and VACUUM INTO, and transaction controls (BEGIN, COMMIT, END, ROLLBACK, SAVEPOINT, and RELEASE). exec already owns the transaction around the whole batch.

Owncat cautions youJavaScript loses precision above 2^53

Parameters and results cross the host boundary as JSON. Python can bind and read exact 64-bit SQLite integers. JavaScript loses unsafe integers before JSON.stringify on writes and during JSON.parse on reads. Store values above Number.MAX_SAFE_INTEGER (2^53 - 1) as TEXT when a JavaScript plugin needs them to remain exact.

Requires storage.sql. For a worked example, the chat-leaderboard plugin covers schema creation in one atomic exec, an ON CONFLICT upsert, a bounded ranked query, and a single-row read, in both JavaScript and Python. It contrasts with message-counter, which keeps the same counts in storage.kv and cannot rank.

Config

owncast.config.get(key, fallback?)

Read one of your plugin's manifest-declared config settings. Returns the admin-set value when present, otherwise the declared default, already parsed to its declared type. For an unknown key (or one with no value) it returns fallback.

const cooldownMs = owncast.config.get('cooldownMs', 2000);

Ambient: no permission required. Prefer this over building a bespoke settings page and key/value plumbing for simple knobs. (The config key is whatever you named it in the manifest, and it isn't translated per language.)

Network

owncast.http.fetch(url, opts?)

Synchronous outbound HTTP request. opts carries method, headers, and body. The result is { status, headers, body }. Only hosts listed in your manifest's network.allowedHosts are reachable. Everything else returns an error.

const res = owncast.http.fetch('https://api.example.com/status');
if (res.status === 200) {
const data = JSON.parse(res.body);
}

Requires network.fetch and a matching entry in network.allowedHosts. Use this for outbound HTTP rather than your language's own HTTP client (in Python, don't use requests: it won't compile into a plugin).

See Manifest reference: network for allowlist syntax.

Plugin-to-plugin events

owncast.events.emit(eventType, payload)

Emit a custom event that other plugins can subscribe to. Subscribing plugins receive it through their custom-event handler: see the handlers reference. Namespace your event types with your plugin name to avoid collisions.

owncast.events.emit('my-plugin.thing-happened', { id: 123 });

Requires events.emit.

Stream and server state

owncast.stream.current()

The current live stream state: { online, title?, summary?, viewers, startedAt?, latencyLevel? }.

Requires server.read.

owncast.stream.broadcaster()

Inbound encode telemetry for the current connection: { remoteAddr?, codecs?, resolution?, framerate?, bitrates? }. Read-only, and zero-valued when no broadcast is connected. For changing video output, see the video configuration group below.

Requires server.read.

owncast.server.info()

Static server info: { name?, url?, summary?, welcomeMessage?, version? }.

const name = owncast.server.info().name;

Requires server.read.

owncast.server.socials()

The streamer's configured social links, each { platform, url, icon? }.

Requires server.read.

owncast.server.emotes()

The server's custom chat emotes: the same set the public /api/emoji endpoint serves, each { name, url }. Useful for rendering or filtering :code: emotes server-side.

Requires server.read.

owncast.server.federation()

Whether fediverse federation is enabled and under what handle: { enabled, username?, isPrivate? }. username is omitted when unset, and isPrivate is present only when true.

Requires server.read.

owncast.server.tags()

The streamer's configured tags, as a list of strings.

Requires server.read.

Video and transcoding configuration

owncast.videoConfig.read()

Read the output and transcoding configuration: { latencyLevel, codec, variants }, where each variant is { width, height, framerate, videoBitrate, audioBitrate, isPassthrough }.

const cfg = owncast.videoConfig.read();

Requires videoconfig.read.

owncast.videoConfig.write(partial)

Update video configuration. Pass a partial object. Only the fields you include are changed. Changes apply on the next stream start: the host does not restart an active broadcast.

owncast.videoConfig.write({ latencyLevel: 2 });

Requires videoconfig.write. This is high-trust. Admins should grant sparingly.

Notifications

owncast.notifications.discord(text)

Send a Discord notification through the streamer's configured webhook.

Requires notifications.send.

owncast.notifications.browserPush({ title, body, url? })

Push to subscribed browsers.

owncast.notifications.browserPush({ title: 'Live now', body: 'Come say hi', url: '/' });

Requires notifications.send.

Send a fediverse-formatted notification (renders as a post to followers).

Requires notifications.send.

Fediverse

owncast.fediverse.post(text)

Make a public post to the fediverse from the Owncast account.

Returns { url } on success (currently with an empty url: Owncast publishes the note but does not yet return its URL), or null when the host rejects the call.

Requires fediverse.post. High-trust: a fediverse post goes out under the streamer's own handle and can't be silently revoked. Admins should grant sparingly.

Action buttons (runtime)

owncast.actions.add(button | buttons[])

Append one or more action buttons to your plugin's manifest set without a reload. Each button takes the same fields as a manifest.actions entry (title, plus url/openExternally or inline html). The host validates each entry with the same rules as manifest.actions and persists the result so additions survive a reload.

owncast.actions.add({ title: 'Donate', url: '/plugins/my-plugin/donate', openExternally: true });

Requires ui.modify.

owncast.actions.clear()

Drop every runtime-added action button. Manifest-declared actions remain.

Requires ui.modify.

Full coverage in UI: Action buttons.

Realtime push (Server-Sent Events)

owncast.sse.send(channel, event, data)

Push a Server-Sent-Event to every browser connected to your plugin's /_sse/<channel> endpoint.

  • channel: which stream to push to. Use "" for the default channel.
  • event: the event name the browser listens for. Use "" for the default message event.
  • data: payload. Strings are sent as-is. Anything else is JSON-encoded for you.
owncast.sse.send('alerts', 'donation', { from: 'alice', amount: 5 });

Fire-and-forget. The call returns immediately and never blocks. Slow clients drop frames rather than stalling your plugin.

Requires http.sse.

Full coverage in Serving HTTP: Realtime updates.

Timers

Schedule deferred and repeating work. Timers are ambient, no permission required, and are cleared automatically when your plugin is disabled. (Python: set_timeout, set_interval, clear.)

owncast.timer.setTimeout(fn, ms)

Run fn once after ms milliseconds. Returns an id.

owncast.timer.setInterval(fn, ms)

Run fn repeatedly every ms milliseconds. Returns an id.

owncast.timer.clear(id)

Cancel a pending timeout or interval by the id either call returned.

Bundled assets

Read files you shipped in your plugin's assets/ directory. Ambient: no permission required. (Python: read, read_text.)

owncast.assets.read(path) and owncast.assets.readText(path)

Read a file bundled under assets/, relative to that directory. read returns the original bytes as a JavaScript Uint8Array or Python bytes. readText and Python's read_text decode the bytes as UTF-8. Python replaces malformed byte sequences when decoding. Missing files return null in JavaScript or None in Python.

const image = owncast.assets.read('badge.png');
const template = owncast.assets.readText('template.html');

Complete API reference

Method names below are the JavaScript (camelCase) form. The Python equivalents are snake_case (sendActionsend_action, banIPban_ip, videoConfigvideo_config, and so on).

APIPermission
owncast.log.info / .warning / .errornone (ambient)
owncast.chat.sendchat.send
owncast.chat.sendActionchat.send
owncast.chat.sendTochat.send
owncast.chat.systemchat.send
owncast.chat.replyTochat.send
owncast.chat.historychat.history
owncast.chat.clientschat.history
owncast.chat.deleteMessagechat.moderate
owncast.chat.kickchat.moderate
owncast.users.list / .getusers.read
owncast.users.setEnabled / .banIPusers.moderate
owncast.users.registerusers.register
owncast.auth.grantSession / .endSessionauth.gate
owncast.kv.get / .set / .getJSON / .setJSONstorage.kv
owncast.storage.uploadstorage.upload
owncast.fs.read / .readText / .write / .list / .delete / .existsstorage.fs
owncast.sql.exec / .query / .queryRowstorage.sql
owncast.http.fetchnetwork.fetch
owncast.events.emitevents.emit
owncast.stream.currentserver.read
owncast.stream.broadcasterserver.read
owncast.server.info / .socials / .emotes / .federation / .tagsserver.read
owncast.videoConfig.readvideoconfig.read
owncast.videoConfig.writevideoconfig.write
owncast.notifications.discord / .browserPush / .fediversenotifications.send
owncast.fediverse.postfediverse.post
owncast.actions.add / .clearui.modify
owncast.timer.setTimeout / .setInterval / .clearnone (ambient)
owncast.assets.read / .readTextnone (ambient)
owncast.config.getnone (ambient)
owncast.sse.sendhttp.sse

Improve this page

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

Contributors to this documentation
O
Owncast
Gabe KangasGabe Kangas

Related Documents