Skip to content
Omicron

Federation

ActivityPub endpoints

Every federation route Omicron exposes, and how to inspect them by hand.

These routes exist only when federation is enabled. When it is off, they are not mounted at all and return 404.

Discovery

Endpoint Purpose
GET /.well-known/webfinger?resource=acct:{user}@{domain} Resolves a handle to an actor URI
GET /.well-known/host-meta The WebFinger URL template, as XRD
GET /.well-known/host-meta.json The same, as JSON
GET /.well-known/nodeinfo Where the NodeInfo documents live
GET /nodeinfo/2.0, GET /nodeinfo/2.1 Instance metadata — software, version, usage counts

WebFinger is the entry point for “search for @alice@your-domain on Mastodon”. host-meta is the older step in front of it: some software asks for the WebFinger template rather than assuming the well-known path.

NodeInfo is what puts the instance on the map — FediDB, instances.social and fediverse.observer all start at /.well-known/nodeinfo, read the link out of it, and fetch the document it names. Both schema versions are served: 2.1 is the current one, and 2.0 is there because a crawler written against Mastodon (which only ever exposed 2.0) asks for that one by name.

The document is aggregate counts and nothing else — total accounts, accounts that signed in within the last 30 and 180 days, published local posts, responses — plus the software name, version, and the instance’s display name. No handle, title, or address is in it.

Actor

An account has two addresses, and they are not interchangeable:

Address For Serves
https://your-domain/users/alice Software The actor document, as JSON-LD
https://your-domain/@alice People The profile page, as HTML

/users/alice is the identity: it is the actor’s id, the self link WebFinger returns, and what every remote server stores and addresses. It never changes. /@alice is what a person opens, and it is what the actor publishes as its url and what WebFinger returns as rel="profile-page" — so a Mastodon user clicking through to a local author lands on the page rather than on a document meant for their server.

Each address answers for the other rather than refusing:

  • /users/alice asked for HTML redirects to /@alice.
  • /@alice asked for application/activity+json (or application/ld+json) redirects to /users/alice. The page also carries a <link rel="alternate" type="application/activity+json"> pointing there.

The redirect on /@alice only fires for an unambiguous ActivityPub request — one where an ActivityPub type is the client’s first preference. A browser, a crawler sending */*, and anything ranking text/html first all get the page.

Endpoint Purpose
GET /users/{identifier} The actor document — profile, keys, endpoints
GET /users/{identifier}/followers The followers collection
GET /users/{identifier}/outbox The actor’s public activity stream
GET /users/{identifier}/comments/{commentId} One local response as a Note
GET /users/{identifier}/posts/{postId}/replies The post’s responses as a collection
GET /users/{identifier}/lists/{listId} A public reading list as an object

The actor document carries the public key remote servers use to verify your signatures, and the inbox endpoints they deliver to.

The collections under /users/{identifier} are JSON-LD only. Ask one of them for HTML and you get 406 Not Acceptable — there is no page equivalent of an outbox to redirect you to.

Inboxes

Endpoint Purpose
POST /users/{identifier}/inbox Personal inbox
POST /inbox Shared inbox for activities addressed to many local users

Both are public and unauthenticated by protocol design; authenticity comes from HTTP signature verification, not from a session.

Protections on the inbox path:

  • Rate limited per source IP (RL_INBOX_MAX, default 300/min).
  • Size capped (INBOX_MAX_BODY_BYTES, default 1 MB) — rejected first by declared Content-Length with 413, then enforced again while buffering so a chunked or spoofed length cannot get around it.
  • The body is buffered and the request rebuilt from the exact same bytes, so the HTTP-Signature digest still verifies.

Inspecting by hand

# WebFinger
curl "https://your-domain/.well-known/webfinger?resource=acct:alice@your-domain"

# Actor document (content negotiation matters)
curl -H "Accept: application/activity+json" https://your-domain/users/alice

# Outbox
curl -H "Accept: application/activity+json" https://your-domain/users/alice/outbox

# Is federation on at all? Also: what a directory sees first
curl https://your-domain/.well-known/nodeinfo

# The document itself — software, version, usage counts
curl https://your-domain/nodeinfo/2.1

A working /.well-known/nodeinfo names both documents:

{
  "links": [
    {
      "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0",
      "href": "https://your-domain/nodeinfo/2.0",
      "type": "..."
    },
    {
      "rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
      "href": "https://your-domain/nodeinfo/2.1",
      "type": "..."
    }
  ]
}

Non-federation public routes

Not ActivityPub, but part of the public surface:

Endpoint Purpose
GET /healthz Liveness — {"status":"ok"}
GET /version Name, version, and the federation flag
GET /@{handle}/feed.xml Per-author RSS
GET /sitemap.xml, GET /robots.txt SEO surface

/healthz and /version are served by the backend, which is published on loopback only — reach them on the host, not from the internet.

What is never exposed

  • Drafts, and posts waiting for a scheduled time.
  • Private reading lists.
  • Content from suspended accounts.
  • Anything from a defederated domain (it is purged, not merely hidden).

Found a mistake?Edit this page on GitHub.