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/aliceasked for HTML redirects to/@alice./@aliceasked forapplication/activity+json(orapplication/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 declaredContent-Lengthwith413, 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.