Skip to main content

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 following collection 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 discoveryGET /.well-known/nodeinfo:

{
"links": [
{
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
"href": "https://owncast.example.com/nodeinfo/2.0"
}
]
}

NodeInfo 2.0GET /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.name is always owncast. This is the most reliable way to detect that you are talking to an Owncast server.
  • usage.users.total is always 1 and openRegistrations is always false — an Owncast instance is a single-actor server, not a multi-user community.
  • usage.localPosts is 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_enabled reflects whether Owncast's built-in chat is enabled.
  • metadata.federation is the Owncast-specific block:
    • username is the actor's preferred username (default live). Combined with the host, this gives you the acct: handle without a separate WebFinger round-trip.
    • featured_streams indicates participation in the featured-streams / mini-directory flow (see Stream pings below). A value of 1 means the server advertises its live status to followers via periodic Offer activities.

x-nodeinfo2GET /.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 APIGET /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.
  • following is requested at {actor}/following but always returns 404 — Owncast never exposes a following list.
  • manuallyApprovesFollowers reflects whether the server is in private federation mode. When true, follows are not auto-accepted.
  • discoverable is always true (using the toot: 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 Signature header with keyId="{actor}#main-key", algorithm="rsa-sha256", and the signed headers list.
  • The signed headers cover (request-target), host, date, and digest.
  • A Digest header containing the SHA-256 digest of the request body.
  • Content-Type: application/activity+json and a User-Agent of 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:

  1. It parses keyId and algorithm from your Signature header. The keyId must be an https:// URL.
  2. It resolves your actor and fetches your public key.
  3. 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.
  4. It verifies the signature, trying the stated algorithm and then falling back to rsa-sha256 and rsa-sha512.
  5. It verifies the Digest header against the request body.
  6. If your request carries a parseable Date header, 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 unparseable Date skips 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.

ActivityObjectWhenSent to
CreateNoteThe stream goes live (the "go live" message); other public postsFollowers (+ Public)
UpdateServiceThe server profile (name, avatar, summary, etc.) changesFollowers
Followactor IRIAn operator follows another Owncast server (featured-streams flow)The target server
Offerserver URLPeriodically while live, as a stream "ping"Directory followers
Acceptinbound FollowIn response to a received FollowThe follower
Rejectinbound FollowWhen the operator removes a directory that was listing this serverThat directory
Leaveserver URLThe stream ends (the offline counterpart to Offer)Directory followers
UndoFollowAn operator unfeatures an Owncast server they previously followedThe target server
Accept / Rejectinbound QuoteRequestIn response to a received QuoteRequestThe 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

  • Update of the Service actor is sent to followers when the server's profile metadata changes, so remote caches refresh.
  • Follow is sent when an operator follows another Owncast server. The server then expects an Accept (or Reject) back.
  • Accept is sent automatically in response to an inbound Follow when 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.

ActivityHandling
FollowStores 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.
UndoFollowRemoves the follower.
LikeRecords an engagement against a local object. Emits FediverseEngagementLike.
AnnounceBoost/repost of a local object. Records an engagement and emits FediverseEngagementRepost.
AcceptFollowMarks a remote Owncast server we followed as having accepted (featured-streams flow).
RejectFollowMarks our follow of a remote server as rejected.
OfferA 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.
LeaveThe offline counterpart to Offer: marks the remote Owncast server's stream offline.
UpdatePersonUpdates stored metadata (display name, inbox, shared inbox, avatar) for an existing follower. Updates with any other object type are ignored.
CreateNoteAccepted 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.
QuoteRequestA 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. Like and Announce activities 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)

Inbound fediverse posts require Owncast v0.3.0

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)

Quote requests require Owncast v0.3.0

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:

  1. Send a Follow that sets the ns#directory marker (see the custom namespace) to the Owncast server's actor. The operator approves it by hand, then expect an Accept.
  2. Receive periodic Offer activities (with Owncast metadata) while the server is live.
  3. Receive a Leave when 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.

PropertyTypeMeaning
https://owncast.online/ns#streamStatusstring"live" or "offline". Always present on server-to-server activities.
https://owncast.online/ns#streamTitlestringCurrent stream title, when set.
https://owncast.online/ns#streamDescriptionstringServer summary / description.
https://owncast.online/ns#serverNamestringHuman-readable server name.
https://owncast.online/ns#logoUrlstringAbsolute URL to the server logo.
https://owncast.online/ns#thumbnailUrlstringAbsolute URL to the current stream thumbnail.
https://owncast.online/ns#streamTagsarray of stringsServer metadata tags.
https://owncast.online/ns#directorybooleanSet 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:

  1. Send a signed Follow that sets https://owncast.online/ns#directory to true (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. A Follow without the marker is treated as an ordinary fan follow: it may be auto-accepted, but it will never receive the Offer/Leave pings.
  2. While a server is live it posts an Offer to 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.
  3. When the stream ends cleanly the server posts a Leave. Mark the entry offline.
  4. If the server's operator removes your directory from their side, the server posts a Reject of your original Follow. 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 http or https before 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/Note go-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.

PathMethodPurpose
/.well-known/webfingerGETResolve acct: → actor IRI
/.well-known/host-metaGETXRD pointer to WebFinger
/.well-known/nodeinfoGETNodeInfo discovery document
/nodeinfo/2.0GETNodeInfo 2.0 server metadata
/.well-known/x-nodeinfo2GETx-nodeinfo2 server metadata
/api/v1/instanceGETMastodon-compatible instance description
/federation/user/{username}GETThe Service actor document
/federation/user/{username}/inboxPOSTDeliver activities to the server
/federation/user/{username}/outboxGETCollection of activities the server has sent
/federation/user/{username}/followersGETPaginated followers collection
/federation/user/{username}/followingGETAlways 404 (no following list)
/federation/{object-id}GETFetch a single stored ActivityPub object

Building a compatible application — checklist

To follow and consume an Owncast stream from your own application:

  1. Resolve the handle with WebFinger (acct:live@host) to get the actor IRI, then fetch the actor with Accept: application/activity+json.
  2. Publish your own actor with a publicKey, served over HTTPS, with a reachable inbox.
  3. Send a signed Follow to the actor's inbox. Sign (request-target) host date digest with RSA and include a SHA-256 Digest.
  4. Handle the Accept that Owncast posts back to your inbox (public mode) — or wait for manual approval (private mode).
  5. Listen for go-live posts: Create/Note activities arriving in your inbox tell you the stream started; the alternate/application/x-mpegURL WebFinger link gives you the HLS URL to play.
  6. Optionally act as a directory: set https://owncast.online/ns#directory to true on your Follow, have the operator approve it, then consume the Offer/Leave pings and the https://owncast.online/ns#* metadata for real-time liveness and richer directory entries.
  7. 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.

Contributors to this documentation

Related Documents