ActivityPub & The Fediverse Protocol
This page documents the ActivityPub implementation inside Owncast at the protocol level: which activities a server sends, which it receives, how it identifies itself, and how it signs and verifies requests. It is aimed at developers who want to build a Fediverse application that interoperates with Owncast, whether that means following an Owncast server from another platform, consuming its live notifications, or building tooling that understands Owncast's custom extensions.
If you are an Owncast operator and just want to turn federation on, see The Fediverse and Enabling social features instead. This page assumes familiarity with ActivityPub, ActivityStreams 2.0, JSON-LD, and HTTP Signatures.
Mental model
An Owncast server federates as a single actor of type Service. There is one account per server (default username live), and it represents the stream itself rather than a person. Compared to a general-purpose social server, the model is intentionally narrow:
- The actor sends posts to its followers (most importantly, a "going live" notification) and a periodic stream "ping".
- The actor receives follows, likes, boosts (announces), replies and mentions, quote requests, and a handful of server-to-server activities. Inbound posts are only surfaced to the operator and plugins: they are never added to a timeline and never re-federated.
- There is exactly one user, no open registration, and the
followingcollection is always empty.
All federation endpoints return 405 Method Not Allowed when federation is disabled, so check that first if a server appears unreachable.
Discovery
A remote application locates and describes an Owncast actor through the standard well-known discovery mechanisms.
WebFinger
GET /.well-known/webfinger?resource=acct:{username}@{host}
The resource must be an acct: URI whose host matches the server's configured host (otherwise the request is rejected with 501/400). The response is served as application/jrd+json:
{
"subject": "acct:live@owncast.example.com",
"aliases": ["https://owncast.example.com/federation/user/live"],
"links": [
{
"rel": "self",
"type": "application/activity+json",
"href": "https://owncast.example.com/federation/user/live"
},
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": "https://owncast.example.com/federation/user/live"
},
{
"rel": "http://webfinger.net/rel/avatar",
"type": "image/png",
"href": "https://owncast.example.com/logo/external"
},
{
"rel": "alternate",
"type": "application/x-mpegURL",
"href": "https://owncast.example.com/hls/stream.m3u8"
}
]
}
The self link is the canonical actor IRI. Note the Owncast-specific alternate link of type application/x-mpegURL: it points directly at the HLS playlist for the stream, which lets clients discover the live video without scraping the web UI.
host-meta
GET /.well-known/host-meta
Returns an XRD document pointing back at the WebFinger endpoint, for clients that bootstrap from host-meta:
<?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" type="application/json"
template="https://owncast.example.com/.well-known/webfinger?resource={uri}"/>
</XRD>
NodeInfo
Owncast exposes server-level metadata through NodeInfo so that Fediverse crawlers, directories, and statistics sites can describe the instance.
NodeInfo discovery — GET /.well-known/nodeinfo:
{
"links": [
{
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
"href": "https://owncast.example.com/nodeinfo/2.0"
}
]
}
NodeInfo 2.0 — GET /nodeinfo/2.0:
{
"version": "2.0",
"software": {
"name": "owncast",
"version": "0.2.x"
},
"protocols": ["activitypub"],
"services": {
"inbound": [],
"outbound": []
},
"usage": {
"users": {
"total": 1,
"activeMonth": 1,
"activeHalfyear": 1
},
"localPosts": 42
},
"openRegistrations": false,
"metadata": {
"chat_enabled": true,
"federation": {
"username": "live",
"featured_streams": 1
}
}
}
Most of this is standard NodeInfo, with a few Owncast-specific signals worth calling out:
software.nameis alwaysowncast. This is the most reliable way to detect that you are talking to an Owncast server.usage.users.totalis always1andopenRegistrationsis alwaysfalse— an Owncast instance is a single-actor server, not a multi-user community.usage.localPostsis the count of activities the server has sent (go-live notifications and other public messages), which is a useful proxy for how active the stream is.metadata.chat_enabledreflects whether Owncast's built-in chat is enabled.metadata.federationis the Owncast-specific block:usernameis the actor's preferred username (defaultlive). Combined with the host, this gives you theacct:handle without a separate WebFinger round-trip.featured_streamsindicates participation in the featured-streams / mini-directory flow (see Stream pings below). A value of1means the server advertises its live status to followers via periodicOfferactivities.
x-nodeinfo2 — GET /.well-known/x-nodeinfo2 provides the same information in the alternate x-nodeinfo2 shape used by some directories, including an organization block (name, contact) and an activeWeek user figure. Here services.inbound/services.outbound are both ["activitypub"].
Mastodon instance API — GET /api/v1/instance returns a Mastodon-compatible instance description (uri, title, short_description, description, version, thumbnail, stats, and registration flags) so Mastodon-aware tooling can render a familiar instance card. stats.user_count is 1, stats.status_count is the local post count, and registrations/approval/invites are all disabled.
The actor
GET /federation/user/{username}
Accept: application/activity+json
Requesting the actor IRI with an ActivityStreams Accept header returns the actor document. Owncast represents itself as an ActivityStreams Service (not a Person). The shape is:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"type": "Service",
"id": "https://owncast.example.com/federation/user/live",
"preferredUsername": "live",
"name": "My Owncast Server",
"summary": "Server description / bio",
"url": "https://owncast.example.com/federation/user/live",
"published": "2023-01-01T00:00:00Z",
"manuallyApprovesFollowers": false,
"discoverable": true,
"inbox": "https://owncast.example.com/federation/user/live/inbox",
"outbox": "https://owncast.example.com/federation/user/live/outbox",
"followers": "https://owncast.example.com/federation/user/live/followers",
"icon": {
"type": "Image",
"mediaType": "image/png",
"url": "https://owncast.example.com/logo/external?uc=..."
},
"image": {
"type": "Image",
"url": "https://owncast.example.com/logo/external?uc=..."
},
"tag": [
{
"type": "Hashtag",
"name": "#owncast",
"href": "https://owncast.directory/tags/owncast"
}
],
"attachment": [
{
"type": "PropertyValue",
"name": "Website",
"value": "<a href=\"...\">...</a>"
}
],
"publicKey": {
"id": "https://owncast.example.com/federation/user/live#main-key",
"owner": "https://owncast.example.com/federation/user/live",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
}
}
Key points for an interoperating implementation:
- Actor IRI layout is
{server}/federation/user/{username}, and the collections hang off it:{actor}/inbox,{actor}/outbox,{actor}/followers. followingis requested at{actor}/followingbut always returns404— Owncast never exposes a following list.manuallyApprovesFollowersreflects whether the server is in private federation mode. Whentrue, follows are not auto-accepted.discoverableis alwaystrue(using thetoot:namespace semantics).- The public key lives at
{actor}#main-key, is an RSA-2048 key in PEM (PKIX) form, and is what you use to verify the server's HTTP signatures.
HTTP Signatures
Owncast both signs its outbound requests and verifies inbound ones using the "Signature" HTTP header scheme (draft-cavage HTTP signatures, as used across the Fediverse).
Verifying requests Owncast sends to you
When Owncast POSTs an activity to your inbox it includes:
- A
Signatureheader withkeyId="{actor}#main-key",algorithm="rsa-sha256", and the signedheaderslist. - The signed headers cover
(request-target),host,date, anddigest. - A
Digestheader containing the SHA-256 digest of the request body. Content-Type: application/activity+jsonand aUser-Agentof the form{version}; https://owncast.online.
To verify: fetch the actor at keyId, read publicKey.publicKeyPem, and verify both the signature and the body digest.
Signing requests you send to Owncast
Owncast verifies the signature on every activity delivered to its inbox:
- It parses
keyIdandalgorithmfrom yourSignatureheader. ThekeyIdmust be anhttps://URL. - It resolves your actor and fetches your public key.
- It checks that your key's owning domain is not on the instance's blocked-domains list and that the actor itself is not blocked.
- It verifies the signature, trying the stated algorithm and then falling back to
rsa-sha256andrsa-sha512. - It verifies the
Digestheader against the request body. - If your request carries a parseable
Dateheader, it must be close to the server's clock: a date more than 1 hour in the past or more than 1 hour in the future is rejected. This bounds replay of captured, validly-signed requests. A missing or unparseableDateskips the check.
In practice this means: sign (request-target) host date digest with an RSA key, publish that key in your actor's publicKey field, include a SHA-256 Digest, and serve your actor over HTTPS.
Activities Owncast sends (outbound)
All outbound activities originate from the server actor and are delivered to follower inboxes (preferring sharedInbox where a follower advertises one). Public activities are addressed to https://www.w3.org/ns/activitystreams#Public in to with the followers collection in cc; in private mode they are addressed only to the followers collection.
| Activity | Object | When | Sent to |
|---|---|---|---|
Create | Note | The stream goes live (the "go live" message); other public posts | Followers (+ Public) |
Update | Service | The server profile (name, avatar, summary, etc.) changes | Followers |
Follow | actor IRI | An operator follows another Owncast server (featured-streams flow) | The target server |
Offer | server URL | Periodically while live, as a stream "ping" | Directory followers |
Accept | inbound Follow | In response to a received Follow | The follower |
Reject | inbound Follow | When the operator removes a directory that was listing this server | That directory |
Leave | server URL | The stream ends (the offline counterpart to Offer) | Directory followers |
Undo | Follow | An operator unfeatures an Owncast server they previously followed | The target server |
Accept / Reject | inbound QuoteRequest | In response to a received QuoteRequest | The requester |
Create / Note — going live
The most important activity. When the stream goes live, Owncast sends a Create wrapping a Note. The Note contains HTML content (the configurable go-live message, stream title, hashtag links, and a link back to the server), Hashtag tags, and — when available — an Image attachment with the stream preview (preview.gif or thumbnail.jpg). If the server is marked NSFW, the note carries sensitive: true. Hashtags link to https://owncast.directory/tags/{tag}, and an #owncast hashtag is always appended.
This is the activity most consumers care about: subscribe by following the actor, then watch the inbox for Create/Note activities to know when a stream starts.
Offer / stream ping (outbound)
This is an Owncast extension that supports the featured-streams / mini-directory feature. While live, the server periodically sends an Offer activity whose object is the server URL, carrying Owncast custom metadata (stream status, title, description, server name, logo, tags). It lets a receiving directory keep its list of live streams fresh without polling. The matching offline signal is the Leave activity, sent when the stream ends. Owncast sends Offer and Leave only to followers that identified themselves as a directory (see the custom namespace), never to ordinary fan followers.
Update, Follow, Accept
Updateof theServiceactor is sent to followers when the server's profile metadata changes, so remote caches refresh.Followis sent when an operator follows another Owncast server. The server then expects anAccept(orReject) back.Acceptis sent automatically in response to an inboundFollowwhen the server is in public (auto-approve) mode.
Activities Owncast receives (inbound)
Deliver these by POSTing a signed activity to the actor's inbox. The inbox returns 202 Accepted immediately and processes the activity asynchronously, so a 202 only means the activity was queued, not that it was acted on. Owncast signature-verifies and dispatches each one from that queue.
| Activity | Handling |
|---|---|
Follow | Stores the follower; auto-approves and returns Accept in public mode (held for approval in private mode). A follow carrying the ns#directory marker is always held for manual approval regardless of mode, and does not emit the follow event. Otherwise emits a FediverseEngagementFollow event. |
Undo → Follow | Removes the follower. |
Like | Records an engagement against a local object. Emits FediverseEngagementLike. |
Announce | Boost/repost of a local object. Records an engagement and emits FediverseEngagementRepost. |
Accept → Follow | Marks a remote Owncast server we followed as having accepted (featured-streams flow). |
Reject → Follow | Marks our follow of a remote server as rejected. |
Offer | A stream ping from another Owncast server. If it carries streamStatus: "live" Owncast marks that server online in its federated-servers table and stores the streamed metadata. |
Leave | The offline counterpart to Offer: marks the remote Owncast server's stream offline. |
Update → Person | Updates stored metadata (display name, inbox, shared inbox, avatar) for an existing follower. Updates with any other object type are ignored. |
Create → Note | Accepted when the object is a single Note attributed to the signing actor and the note is either a reply to a post this server published or addressed to the actor (a mention). Raised as an event for the operator and plugins, not added to any timeline. See inbound posts. |
QuoteRequest | A FEP-044f request to quote one of the server's posts. Accepted only for locally authored posts while federation is public and quotes are enabled. Answered with Accept or Reject. See quote requests. |
Two important guards:
- Engagement age limit.
LikeandAnnounceactivities are only recorded if the referenced object is no more than 36 hours old. Older engagements are ignored. This keeps engagement notifications tied to recent streams. - Blocking & SSRF. Inbound activities from blocked domains/actors are rejected during signature verification. Outbound deliveries refuse non-HTTPS and internal/loopback inbox URLs.
Inbound posts (Create)
Earlier releases rejected every inbound Create. Owncast 0.3.0 accepts the narrow cases described here.
Owncast accepts a Create whose object is exactly one Note attributed to the same actor that sent it. It keeps two kinds of notes: replies to a post the server itself published, and notes that address the actor directly (mentions). Everything else is ignored. An accepted post is raised as a reply or mention event for the operator's integrations and plugins. It is not added to a timeline, is not shown to viewers, and is never re-federated, so there is still no public conversation surface on an Owncast server.
Quote requests (FEP-044f)
QuoteRequest handling is new in Owncast 0.3.0.
A remote user asking permission to quote one of the server's posts sends a FEP-044f QuoteRequest whose object is the post being quoted and whose instrument is the quote post itself. Owncast accepts the request only when the quoted object is a post this server authored, federation is in public mode, and the operator has quoting enabled. On accept it stores a QuoteAuthorization stamp as a dereferenceable object and replies with an Accept whose result is the stamp's IRI, so any server can fetch the stamp to verify the quote was approved. Every other case gets a Reject, which clears the pending quote on the remote end.
Server-to-server activities
Offer, Leave, Accept, and Reject together form the Owncast-to-Owncast "featured streams" protocol. If you are building a directory or aggregator that wants to participate, the pattern is:
- Send a
Followthat sets thens#directorymarker (see the custom namespace) to the Owncast server's actor. The operator approves it by hand, then expect anAccept. - Receive periodic
Offeractivities (with Owncast metadata) while the server is live. - Receive a
Leavewhen the stream ends.
You can equally consume only the standard Create/Note go-live posts if you do not need real-time liveness pings.
Owncast custom namespace
Owncast adds a small set of custom JSON-LD properties under the namespace https://owncast.online/ns#. The stream-metadata properties appear as extra top-level fields on Offer (and related server-to-server) activities and let a receiver populate a directory entry from a single activity. The ns#directory marker appears on a Follow and identifies the sender as a directory. All are optional and safe to ignore if you only care about standard ActivityPub.
| Property | Type | Meaning |
|---|---|---|
https://owncast.online/ns#streamStatus | string | "live" or "offline". Always present on server-to-server activities. |
https://owncast.online/ns#streamTitle | string | Current stream title, when set. |
https://owncast.online/ns#streamDescription | string | Server summary / description. |
https://owncast.online/ns#serverName | string | Human-readable server name. |
https://owncast.online/ns#logoUrl | string | Absolute URL to the server logo. |
https://owncast.online/ns#thumbnailUrl | string | Absolute URL to the current stream thumbnail. |
https://owncast.online/ns#streamTags | array of strings | Server metadata tags. |
https://owncast.online/ns#directory | boolean | Set to true on a Follow to identify the sender as a directory. |
A directory identifies itself by setting ns#directory to true on the Follow it sends. That marker, and only that marker, makes Owncast treat the follow as a directory listing: it holds the follow for the operator to approve, and once approved, delivers the Offer and Leave stream pings to that follower. The stream-metadata fields above are descriptive only and do not, on their own, identify a directory.
Building a directory of Owncast streams
The server-to-server activities that power Owncast's own featured streams feature are open for you to consume. If you want to run a directory or aggregator that tracks which Owncast servers are live, you follow each server the way any Fediverse actor would and then react to the liveness signals it sends.
For a complete, runnable example reference, see the owncast-directory-example repository. It is a small Python application that implements everything in this section: a published actor, the ns#directory follow, the Offer/Leave/Reject handling, and a web page that lists the live servers. Treat it as a starting point rather than a production service.
You need a published actor and signed requests, the same as any follower (see HTTP Signatures). From there:
- Send a signed
Followthat setshttps://owncast.online/ns#directorytotrue(see the custom namespace) to each server's actor. That marker identifies you as a directory, which is what makes the server deliver its stream pings to you, and it makes being listed opt-in: an Owncast server always holds a directory follow for its operator to approve by hand, no matter how the server's federation privacy is configured. You will not receive any status until the operator approves, so expect entries to stay pending until each one opts in. AFollowwithout the marker is treated as an ordinary fan follow: it may be auto-accepted, but it will never receive theOffer/Leavepings. - While a server is live it posts an
Offerto your inbox roughly every 5 minutes, carrying the Owncast custom metadata: stream status, title, description, server name, logo, thumbnail, and tags. Create or refresh that server's directory entry from those fields. - When the stream ends cleanly the server posts a
Leave. Mark the entry offline. - If the server's operator removes your directory from their side, the server posts a
Rejectof your originalFollow. Drop the entry: you are no longer authorized to list that server, and it will stop sending you pings.
There is no built-in flow for an Owncast server to request a spot in your directory, so assembling the list is your side's job. A simple way to let operators opt in is to put a submission form on your directory where an operator enters their server URL. You and your directory decide which submissions to list and which to turn away. When you accept one, follow that server the same way as above. The operator approves the follow, which a submitter will be expecting to do, and the follow, accept, and ping flow lists their stream.
Treat the pings as a heartbeat. If a server stops sending Offer activities without a Leave, because it crashed, lost connectivity, or was firewalled, nothing actively tells you it went down. Expire any entry you have not heard from in a couple of ping intervals. Owncast's own directory marks a peer offline after two missed pings, about 11 minutes, and runs that staleness check once a minute.
A few things worth getting right:
- The metadata fields come from the remote server, so treat them as untrusted input. Clamp lengths and confirm that any URL is
httporhttpsbefore you render it. The value you can trust is the server URL you chose to follow, not the display name the server sends. - The thumbnail and logo URLs are stable, so the browser will cache them. Append a changing cache-busting query when you refresh an entry if you want the preview to stay current.
- You do not have to use the pings at all. If you only need to know that a server went live, rather than keep a running view of who is live right now, follow the actor and watch for the standard
Create/Notego-live posts like any other Fediverse consumer.
To have your service recognized as a directory, set https://owncast.online/ns#directory to true on the Follow you send. A server that sees it holds the follow for its operator and, once approved, sends you its stream pings.
Endpoint reference
All paths are relative to the server's base URL. Every endpoint returns 405 when federation is disabled.
| Path | Method | Purpose |
|---|---|---|
/.well-known/webfinger | GET | Resolve acct: → actor IRI |
/.well-known/host-meta | GET | XRD pointer to WebFinger |
/.well-known/nodeinfo | GET | NodeInfo discovery document |
/nodeinfo/2.0 | GET | NodeInfo 2.0 server metadata |
/.well-known/x-nodeinfo2 | GET | x-nodeinfo2 server metadata |
/api/v1/instance | GET | Mastodon-compatible instance description |
/federation/user/{username} | GET | The Service actor document |
/federation/user/{username}/inbox | POST | Deliver activities to the server |
/federation/user/{username}/outbox | GET | Collection of activities the server has sent |
/federation/user/{username}/followers | GET | Paginated followers collection |
/federation/user/{username}/following | GET | Always 404 (no following list) |
/federation/{object-id} | GET | Fetch a single stored ActivityPub object |
Building a compatible application — checklist
To follow and consume an Owncast stream from your own application:
- Resolve the handle with WebFinger (
acct:live@host) to get the actor IRI, then fetch the actor withAccept: application/activity+json. - Publish your own actor with a
publicKey, served over HTTPS, with a reachableinbox. - Send a signed
Followto the actor's inbox. Sign(request-target) host date digestwith RSA and include a SHA-256Digest. - Handle the
Acceptthat Owncast posts back to your inbox (public mode) — or wait for manual approval (private mode). - Listen for go-live posts:
Create/Noteactivities arriving in your inbox tell you the stream started; thealternate/application/x-mpegURLWebFinger link gives you the HLS URL to play. - Optionally act as a directory: set
https://owncast.online/ns#directorytotrueon yourFollow, have the operator approve it, then consume theOffer/Leavepings and thehttps://owncast.online/ns#*metadata for real-time liveness and richer directory entries. - Verify the signature on everything Owncast sends you against the actor's
#main-key.
Owncast accepts replies and mentions only as notifications for the operator and plugins: they are never threaded, displayed, or re-federated, and the server exposes no following list. Design your integration around following + notifications + likes/boosts rather than two-way conversation.
Improve this page
See something missing or incorrect? Edit this page and improve the documentation for everyone.
Related Documents
- The FediverseAllow people to follow your server, know when you go live, share and interact with your stream.
- WebhooksLearn how to set up and use webhooks to get notified about events on your Owncast server.
- 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.
- Show Custom Action Buttons On Your PageYou can display external user interfaces into Owncast by registering external actions.
