Catodon implements the client-to-server half of ActivityPub (W3C REC §5–§6), following the SocialCG ActivityPub API Task Force's Basic Profile for ActivityPub API Servers. A generic ActivityPub app can sign an account in with OAuth, read its inbox and outbox, and post activities to its outbox. Each posted activity is carried out through the same code the Misskey and Mastodon APIs use, so it has the same effects, limits and policies as the same action taken in the web client.
This page is for developers of such apps and for administrators. The exact behaviour of every activity, including what Catodon's data model cannot express, is in packages/backend/src/server/activitypub-c2s/COMPATIBILITY.md.
Turning it on or off
The API is on by default. An administrator can turn it off in Control
Panel > Federation > Policy > ActivityPub client API (the meta setting
enableC2S). While it is off:
- actors publish no OAuth or C2S endpoints, and the OAuth server metadata leaves out the task force's scopes and the FEP-d8c2 flag;
- the C2S routes answer exactly as if they did not exist (404, or the web
client's page for
GET /users/:id/inbox,/liked,/lists/:listand/media/:id), and a bearer token on a route shared with federation changes nothing; - apps identified by a web address can still sign in, for Misskey permissions and basic Mastodon scopes only.
Discovery
An app starts from the account's actor (by WebFinger or its id). With the API on, the actor carries:
| Property | Value |
|---|---|
inbox, outbox | /users/<id>/inbox, /users/<id>/outbox |
liked | /users/<id>/liked (readable by the owner only) |
endpoints.oauthAuthorizationEndpoint | /oauth/authorize |
endpoints.oauthTokenEndpoint | /oauth/token |
endpoints.uploadMedia | /c2s/upload-media |
endpoints.proxyUrl | /c2s/proxy |
objectIDAsClientID | true (FEP-d8c2) |
The same endpoints are in the RFC 8414 document at
/.well-known/oauth-authorization-server, with
code_challenge_methods_supported: ["S256"],
token_endpoint_auth_methods_supported: ["none", "client_secret_post"],
client_id_metadata_document_supported: true,
activitypub_object_id_as_client_id: true and the scopes in
scopes_supported. Group (forum) and system actors publish none of this:
nobody signs in as them.
Signing in
OAuth 2.0 authorization code grant, with PKCE (S256) required. There is no
client secret and nothing to register: the client_id is an https: URL that
describes the app, fetched when the user is asked to authorize it. Three kinds
of document are understood:
- a Client ID Metadata Document: JSON with
client_id(equal to its own URL),redirect_uris,client_name,logo_uri,client_uri; - an ActivityPub
ApplicationorService(FEP-d8c2) whoseidis theclient_id, withredirectURI(a string or a list),nameornameMap,iconandsummary; - an IndieAuth page: an
h-appand<link rel="redirect_uri">, orLinkheaders.
The redirect_uri must match one the document lists exactly. The document is
fetched with the server's usual protections: private and loopback addresses are
refused unless the administrator allowed them, redirects are followed one at a
time and checked each time, and the document may be at most 1 MiB.
The user sees a consent page with the app's name and icon, the server the app
is published on, the one they will be sent back to, and the permissions asked
for. The permissions cannot be unticked one by one: the user accepts them all
or refuses. The redirect carries code, state and iss (RFC 9207). The code
is valid for five minutes and only once: exchanging it twice fails and revokes
the token the first exchange bought.
The token response:
{
"access_token": "…",
"token_type": "Bearer",
"scope": "https://swicg.github.io/activitypub-api/scopes#readinbox https://swicg.github.io/activitypub-api/scopes#createcontent …",
"created_at": 1790579372,
"activitypub_actor_id": "https://example.social/users/9xyz…",
"me": "https://example.social/@alice"
} scope is what was granted, which may be less than what was asked for.
Admin permissions (read:admin:*, write:admin:*) are never granted to an app
identified by a web address, whatever it asks for. Tokens do not expire, so no
refresh token is issued. Dynamic client
registration (RFC 7591) is not offered; a URL client_id covers that case. The
app then appears in the user's Settings > Connected apps, where it can be
revoked. Mastodon apps keep signing in through the Mastodon flow on the same
endpoints (a client_id that is not a URL); nothing changed for them.
Send the token as Authorization: Bearer <token>. It is never read from a
cookie or a query parameter.
Scopes
An app may ask for the task force's 28 scopes
(https://swicg.github.io/activitypub-api/scopes#<name>), for Misskey
permissions (read:account, write:notes, …) and for Mastodon's read,
write and follow. A token granted only task-force scopes can use nothing
but this API: they are not turned into Misskey permissions that would also open
the Misskey or Mastodon API.
| Scope | Allows |
|---|---|
readall | read your inbox, outbox, followers, following and liked (your own data only) |
readinbox, readoutbox, readfollowers, readfollowing, readliked | read that collection |
readlocal, readown, readany | read posts on this server, on the app's own server (its client_id host), or anywhere; also the reach of proxyUrl. readany includes the other two |
createcontent | create posts, including replies and quotes to posts on any server, blog posts and poll votes |
updatecontent, deletecontent | edit and delete your posts and blog posts |
follow | follow and unfollow |
managefollowers | accept, reject and remove followers |
managecollections | pin and unpin posts; create, rename and delete lists and change their members |
like, share | like (react) and repost anywhere, and take them back |
reactlocal, reactown, reactany | like, repost, reply to, quote and vote in posts within their reach, for an app without like, share or createcontent. reactany includes the other two |
block, flag | block and unblock; report |
addresspublic, addressfollowers, addressactor | address a post to the public, to your followers, or to individual actors. addressall includes all three. A post addressed to nobody needs none |
uploadfiles | upload media |
updateprofile | change your profile, including avatar and banner |
A token without any task-force scope (a Mastodon app token, a MiAuth token, a URL client that asked only for Misskey permissions) is limited by its Misskey permissions alone.
Endpoints
| Method and route | Scope | What it does |
|---|---|---|
POST /users/:id/outbox | per activity (below) | carry out an activity; 201 with Location and the resulting activity |
GET /users/:id/inbox | readinbox | the owner's home timeline, as activities; owner only |
GET /users/:id/outbox | readoutbox | with the owner's token, every post of any visibility |
GET /users/:id/liked | readliked | the posts the owner reacted to; owner only |
GET /users/:id/followers, /following | readfollowers, readfollowing | with the owner's token, whatever the profile's visibility setting |
GET /users/:id/collections/featured | readoutbox | with the owner's token, pinned posts of any visibility |
GET /users/:id/lists/:list | managecollections or readall | one of the owner's lists: its members; owner only |
GET /notes/:id, /notes/:id/activity | a read reach, or readoutbox for your own | any post the token's user may see |
GET /notes/:id/replies, /likes, /shares, /context | the same | a post's replies and thread as the token's user may see them, and its like and share counts |
GET /@user/blog/:slug | the same | a redirect to the blog post's id, for any blog post the token's user may see |
POST /c2s/upload-media | uploadfiles | multipart/form-data with file and object (JSON); 201 with the new Image, Video, Audio or Document, whose id is /media/<id> |
GET /media/:id | the uploaded object: to the owner, and to anyone once a public post attaches it | |
POST /c2s/proxy | readlocal, readown or readany | application/x-www-form-urlencoded id=<url>: the object, fetched signed as the account |
The outbox is addressed by the actor's id, as its outbox property names it.
Collections are paged with ?page=true and since_id / until_id, newest
first, 20 a page; followers and following use ?page=true&cursor=…, 10 a page;
replies (until_id) and the thread (since_id, oldest first) 50 a page. A
browser opening the inbox, liked or a list without a token gets the web
client's page; every other request without one gets 401.
Every answer to a token request carries Cache-Control: private, no-store and
Vary: Authorization.
The outbox takes application/ld+json; profile="https://www.w3.org/ns/activitystreams"
or application/activity+json, at most 256 KB. Anything else is 415.
Activities
A bare object is wrapped in a Create, with its addressing copied both ways.
Client-supplied ids are ignored. actor, and a created object's
attributedTo, must be absent or the account's own actor.
| Posted | Effect | Scope |
|---|---|---|
Create of Note (or Image, Video, Audio: a post with attachments) | a post. inReplyTo, the quote (quote, quoteUrl, quoteUri, _misskey_quote), summary (content warning), sensitive, attachment (files from uploadMedia), source (text/x.misskeymarkdown, text/markdown, text/plain) or HTML content, contentMap (language) | createcontent + address* |
Create of Question | a post with a poll (oneOf / anyOf, endTime) | createcontent + address* |
Create of a Note with only a name, in reply to a poll | a vote | createcontent, or a react* reaching the poll |
Create of Article | a blog post (public or followers only) | createcontent + address* |
Create of Collection / OrderedCollection | a private list | managecollections |
Update of your post, blog post, list or actor | a partial update: given properties replace, null deletes | updatecontent, managecollections, updateprofile |
Delete of your post, blog post or list | deletes it | deletecontent, managecollections |
Follow of an actor | follows (or requests to) | follow |
Accept / Reject of a Follow of you | answers the request; Reject of an accepted follower removes it | managefollowers |
Like, EmojiReact of a post | the default reaction, or the one in content | like, or a react* in reach |
Announce of a post | a repost (public, non-discoverable public or followers) | share, or a react* in reach, + address* |
Block of an actor | blocks | block |
Flag of an account and its posts | a report (content is the comment) | flag |
Add / Remove with your featured collection as target | pins / unpins | managecollections |
Add / Remove with your list as target | adds / removes a member | managecollections |
Remove with your followers as target | removes a follower | managefollowers |
Undo of your Follow, Like, Announce, Block | takes it back; by our id or embedded | the scope that made it |
| anything else | 422, with the supported list |
The response's Location is the resulting activity's id. Create,
Announce, Follow and Like ids dereference; the ids of Update, Delete,
Undo, votes and blocks do not yet. Details per activity:
COMPATIBILITY.md.
Errors
Every error from the C2S routes is application/problem+json (RFC 9457): type
(about:blank), title, status, detail, and code, the Misskey API's error
code or a C2S one (UNSUPPORTED_ADDRESSING, HIDDEN_RECIPIENTS_UNSUPPORTED,
UNSUPPORTED_ACTIVITY, NO_SUCH_OBJECT, …). A 422 says what could be expressed
instead (expressible, supported, properties).
| Status | When |
|---|---|
| 401 | no token or an invalid one; WWW-Authenticate: Bearer |
| 403 | the token's user is not the path's owner (NOT_OWNER), a missing scope (INSUFFICIENT_SCOPE, with the scope URI, also in WWW-Authenticate), a missing Misskey permission, a suspended account |
| 404 | not found, or not visible to this token (the two are not told apart) |
| 413, 415 | the body is too large, or not ActivityPub JSON |
| 422 | the activity cannot be carried out as asked |
| 429 | rate limited, with Retry-After |
The rate limits are those of the Misskey API endpoints each activity uses.
CORS
The C2S routes answer preflights and send
Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: GET, POST, OPTIONS,
Access-Control-Allow-Headers: Accept, Authorization, Content-Type and
Access-Control-Expose-Headers: Location, Vary, Retry-After plus the rate-limit
headers, so a browser app on any origin can use the API. Credentials are bearer
tokens, never cookies, which is what makes * safe.
Caveats
- A
Blockis delivered to the blocked account's server. ActivityPub says a server must not deliver it; Catodon keeps one behaviour for all its APIs, and the Misskey and Mastodon APIs deliver it for compatibility with Mastodon. - Addressing Misskey cannot represent is refused, not dropped. A post is
public, non-discoverable public, followers-only or direct. An individual actor on a
public, home or followers post must be mentioned in the text (or be the
author of the post replied to or quoted).
btoandbccmay only repeat addressees already intoorcc, because a recipient cannot be hidden. Another account's collection or a list cannot be addressed. Each of these is a 422 that says what is expressible. - A post addressed to nobody is visible to its author alone, not public.
- Content is stored as Misskey text. HTML
contentis converted, and the object is served back as Catodon renders it (<p>v1</p>forv1), with the text insource. Sendsourceto control it exactly. A post needs text, files or a poll; an empty one is refused. - A deleted post answers 404, not 410 with a
Tombstone. A post a moderator removed is aTombstone, its activity 404, and it is left out of every collection. - The inbox is synthesised from the home timeline; received activities are not stored, and notifications (follow requests, likes of your posts) are not in it.
- Activity types without an effect in Catodon (
Listen,Arrive, …) are refused with 422 rather than stored and delivered.
Known gaps
Found while trying the API with the ap command-line client
(evanp/ap) and the
socialweb.coop test suite:
- A post's
likesandsharescollections carry counts only, as Mastodon's do. A client that finds its ownLikeorAnnouncethrough them to undo it cannot; sendUndowith the id from theLocationof theLikeorAnnounce, or with the activity embedded ({"type": "Like", "object": "<post>"}). - There are no
pendingFollowers/pendingFollowingcollections, and the outbox lists posts and reposts, notFolloworLikeactivities. Follow requests are answered byAccept/Rejectof theFollowembedded or by its id.