Python SDK
The Python SDK, owncast-plugin-py, lets you author Owncast plugins in Python. You write ordinary Python with decorators. A build step turns it into a single installable plugin that runs sandboxed inside the Owncast server: the same .ocpkg format and full feature set as the JavaScript SDK, so a Python plugin is a first-class peer of a JS one.
The plugin SDKs are brand-new in Owncast 0.3.0 and the API is still evolving. If you hit a bug or have a suggestion, please open an issue or chat live with the community.
This page is the Python-specific layer: install, the @plugin decorators, the owncast-plugin-py CLI, and testing. Handlers, APIs, permissions, and the manifest work the same in both SDKs and have their own reference pages.
How it maps to the reference docs
The shared reference names handlers and APIs in their canonical (camelCase) form. To read it as Python, apply one rule: decorators, host methods, and payload attribute access are snake_case. Raw wire dictionaries (msg.raw) and scenario JSON keep their camelCase wire names. Quick orientation:
| In the reference | In Python |
|---|---|
| Define a handler | a @plugin.* decorated function |
Handler for an event (e.g. chat.message.received) | @plugin.on_chat_message |
Call a host API (e.g. owncast.chat.sendAction) | owncast.chat.send_action(text): snake_case |
Payload fields (e.g. msg.user.displayName) | msg.user.display_name, msg.client_id. msg.raw for the raw dict |
Filter result (filter.pass()) | filter.pass_() (trailing _: pass is a keyword). Also filter.modify(...) / filter.drop(reason) |
| Subscribe to a custom event | @plugin.on("my.event") |
| Build / test your plugin | owncast-plugin-py package / owncast-plugin-py test |
Prerequisites
- An Owncast server you can administer, version 0.3.0 or newer.
- Python 3.8 or newer.
Install
Scaffold a project with new, passing the slug. uvx runs the scaffolder straight from PyPI without installing anything:
uvx owncast-plugin-py new my-plugin
cd my-plugin
Install the SDK to get the owncast-plugin-py CLI on your PATH for the build, test, serve, and package steps:
uv tool install owncast-plugin-py # or: pip install owncast-plugin-py
You get a ready-to-build directory:
my-plugin/
├── plugin.manifest.json name, slug, version, permissions
├── README.md how to build, test, package, and install it
├── INSTRUCTIONS.md optional, rendered as a tab in the admin
├── AGENTS.md notes for AI coding agents
├── .agents/ a bundled skill for AI coding agents
├── src/plugin.py your code, with a sample handler
└── __tests__/*.test.json a sample scenario test
Write a plugin
Import plugin, owncast, and filter, and register handlers with decorators. Each decorator subscribes to one event. The SDK derives the manifest's subscription list from which handlers you define.
from owncast_plugin import plugin, owncast, filter
@plugin.on_chat_message
def greet(msg):
name = msg.user.display_name if msg.user else "someone"
owncast.chat.send(f"{name} said: {msg.body}")
@plugin.filter_chat_message
def block_spam(msg):
return filter.drop("spam") if "spam" in msg.body else filter.pass_()
The module exports five things:
plugin: the decorator registry.@plugin.on_chat_message,@plugin.filter_chat_message,@plugin.on_stream_started,@plugin.on_tick,@plugin.on_fediverse_follow, and the rest mirror the runtime events in the handlers reference. Two take a key:@plugin.on("custom.event")for plugin-emitted events and@plugin.on_tab_content("slug")/@plugin.on_page_content("slug")for dynamic viewer-page HTML. For tab content, the decorator argument matches amanifest.tabsobject key. For extra page content, it matchesmanifest.extraPageContent.slug. Two take no key:@plugin.on_page_stylesand@plugin.on_page_scriptsreturn CSS and JavaScript injected into the viewer page at request time, gated onui.modify.owncast: the host API namespace. Method names aresnake_case(owncast.chat.send_action,owncast.kv.get_json). Each call is gated by the matching permission you declare in your manifest. See the APIs reference.filter, filter results returned from afilter_chat_messagehandler:filter.pass_()(trailing underscore,passis a Python keyword),filter.modify(...),filter.drop(reason).auth_check: verdict helpers for the@plugin.on_auth_checkhandler of anauth.gateplugin:auth_check.ok(),auth_check.refresh(ttl=...),auth_check.deny(reason).CommandContext: what a declared command'srun()receives:.msg,.user,.command,.invoked_as,.args, and.arg_string, plusreply(text)andreply_privately(text)helpers. Import it for type hints.
Payloads are attribute objects with snake_case accessors over the wire JSON (msg.body, msg.user.display_name, msg.client_id). Use msg.raw for the underlying dict. Host calls that return JSON objects come back as the same attribute objects (owncast.server.info().name). Lists come back as Python lists.
Two more Python idioms worth knowing, both documented in full (with Python examples) on the subject pages:
- HTTP routing: plugins with
http.servedeclare routes with decorators:@plugin.get/post/put/delete/patch(path),@plugin.route(path, methods=[...]),@plugin.on_http_request(path), and a bare@plugin.on_http_requestcatch-all. A handler returns adict({status, body, headers}), astr(→ 200), orNone(→ 204). See Serving HTTP. - Chat commands:
plugin.commands({...})declares commands with aliases, moderator gating, and per-user cooldowns. The built-in!helplists them automatically. See Chat commands.
The CLI
Installing the SDK gives you owncast-plugin-py. Building and packaging bundle your source and need no compiler. The test, serve, and package commands fetch the prebuilt host binaries on first use (package runs its install-time load check through the test binary):
| Command | What it does |
|---|---|
owncast-plugin-py new my-plugin | Scaffold a new plugin project in ./my-plugin |
owncast-plugin-py build | Build src/plugin.py (without packaging) |
owncast-plugin-py test | Build, then run the __tests__/ scenarios |
owncast-plugin-py serve | Local dev server (-p/--port to change the port, defaults to 8080) |
owncast-plugin-py package | Build + bundle → <slug>.ocpkg: the file you ship |
owncast-plugin-py package # produces my-plugin.ocpkg
owncast-plugin-py test
owncast-plugin-py serve # POST /_dev/chat to drive event handlers
All four run against the current directory. The positional project argument defaults to ., so inside the project you pass nothing. From elsewhere, pass the project directory: owncast-plugin-py package my-plugin. The .ocpkg is the single distribution artifact. See Packaging & distribution for what goes inside and how to install it.
Constraints to know
A few things about how Python plugins are built shape how you write them. You import owncast_plugin normally for editor support and unit tests. The build takes care of the rest.
- Pure-Python only, and no
pip. There is nopip installstep: you add third-party code by copying its (pure-Python) source into your project. Dependencies with C extensions (numpy, pandas, and the like) won't load. See Third-party libraries. For outbound HTTP useowncast.http.fetch, notrequests. - Don't shadow standard-library names. A top-level
def json(...)(or any other stdlib name) shadows the real module and can break the build, and a module file named after a stdlib module (src/json.py) is ignored in favor of the real one. Name themjson_responseand the like. - The entry can't use relative imports. In
src/plugin.py, import your own modules absolutely (from helpers import ...), notfrom . import helpers. A relative import there fails the build, though relative imports inside a package's own modules are fine. snake_casein the code you write, in contrast to the JS SDK's camelCase:send_action,get_json,msg.user.display_name,filter.pass_(). Raw wire dictionaries (msg.raw) and scenario JSON stay camelCase.
Third-party libraries
There is no pip install and no requirements.txt. A third-party library works only if it is pure Python and you copy its source into src/, where it becomes one of your own modules.
Installing a package into a virtualenv has no effect on what ships, and import requests fails at runtime. To use a library, copy its .py source into src/ (a single module or a package directory) and import it.
- C extensions never work. numpy, pandas, lxml, Pydantic v2, and anything else with compiled code won't load.
- You own the whole tree. If a library you copy in imports other third-party packages, copy those too, or choose a smaller one.
- Use
owncast.http.fetchfor outbound HTTP, notrequests.
The standard library is available, as long as the module is pure Python (json, re, datetime, base64, and the like).
For example, the page-content-demo example needs Mustache templating. Rather than copy in a templating package, it ships a small Mustache-subset renderer of its own.
Testing
Tests are __tests__/*.test.json scenario files run with owncast-plugin-py test. The format is identical to the JS SDK's, so a Python port of a plugin can reuse the JS version's test scenarios verbatim. Each scenario dispatches events / HTTP requests and asserts on observed side effects (chatSends, kv writes, HTTP responses, …).
[
{
"name": "echoes the message",
"events": [
{
"event": "chat.message.received",
"payload": { "user": { "id": "u1", "displayName": "alice" }, "body": "hi" }
}
],
"expect": { "chatSends": ["alice said: hi"] }
}
]
The full scenario data model (step types, given state, expect assertions) is on the Testing page. Note the scenario JSON uses the wire field names (camelCase: displayName, clientId), since it describes host events, not your Python code.
Status
The runtime, the owncast-plugin-py CLI (scaffold, build, test, serve, package), the full host API, HTTP routing, and .ocpkg packaging all work today. All of the JS example plugins have Python counterparts under examples/python/.
Where to go next
- Handlers reference: every event you can subscribe to (read names as
snake_case). - APIs reference: every
owncast.*method and the permission it needs. - Testing: the full scenario data model.
- Packaging & distribution: building the
.ocpkgand installing it. - Python example plugins: one per feature, each a complete starting point you can copy.
- SDK source: the
owncast-plugin-pypackage and toolchain.
Improve this page
See something missing or incorrect? Edit this page and improve the documentation for everyone.
Gabe KangasRelated Documents
- JavaScript SDKAuthor Owncast plugins in JavaScript or TypeScript with @owncast/plugin-sdk: scaffolding, the definePlugin API, the CLI, and the scenario test harness.
- Extend Owncast with pluginsWrite plugins that run sandboxed inside Owncast to react to chat, post to the fediverse, serve HTTP endpoints, and add UI.
- Plugin quickstartScaffold a new Owncast plugin in JavaScript or Python, build it, package it, and install it on your server.
- Testing pluginsDrive your built plugin through the real Owncast runtime with mocked side effects. Scenario tests, assertions, fixtures, HTTP auth.
- Configuration via PluginsLet admins configure your plugin with typed settings. Owncast renders the form, you read the values at runtime with owncast.config.get.
- Packaging & publishing pluginsBundle your plugin into a .ocpkg, install it on a server, and list it in the public plugin directory.