The server's VAPID key (PushKeys, made on first need, in Mongo) is the instance API's vapid.public_key and every app's vapid_key. /api/v1/push/subscription (POST, GET, PUT, DELETE) keeps one subscription per access token, so per persona; every notification kept (never a filtered one) is queued as a SendPush job for the subscriptions whose alert and policy admit it, and sent through the `push` client (the safe handler, https only) encrypted as RFC 8291 aes128gcm or the aesgcm draft with an ES256 VAPID JWT. 404/410 delete the subscription, 429/5xx/timeouts retry, other answers drop the push. Subscriptions end with their tokens (revocation, sessions ended, suspension, deletion, a restore); backups hold neither keys nor subscriptions. The walk of every Mastodon 4.3-4.5 client route (docs/MASTODON-API.md) added conversations/:id/unread, domain_blocks/preview, v2 notifications clear and PUT policy, /oauth/userinfo and stubs, so both instance APIs now say 4.5.0 (compatible; PrivaPub <release>). The pasture gets a push sink (pushsink.test) and scenarios/push.sh in batch A. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
7.4 KiB
The Mastodon client API: what PrivaPub answers
The walk behind the version PrivaPub advertises (completion program R9, 2026-10-18). Every client route of Mastodon 4.3.0,
4.4.0 and 4.5.0, as their config/routes/api.rb and config/routes.rb define them, set against PrivaPub's controllers
(PrivaPub/Api/Mastodon/Controllers). Since nothing a client relies on is missing, both instance APIs say
4.5.0 (compatible; PrivaPub <release>) (InstanceController.Version; Mastodon invariant 6 in CLAUDE.md), and
api_versions.mastodon is 7, 4.5's own.
- Served: the route does what Mastodon's does, within PrivaPub's rules (a token is one persona, per-persona state, located posts outside the Mastodon API).
- Stub: the route answers as Mastodon answers when the feature is off or empty here (an empty list,
{}, 403, 404 or 422 with Mastodon's message), so clients degrade instead of breaking (Mastodon invariant 5). Each is a decision, named. - Missing: no route. None of these is the client API.
When a route is added to Mastodon, add it here, and keep the advertised version honest.
Served
| Area | Routes |
|---|---|
| Statuses | GET /api/v1/statuses (id[]), POST, GET/PUT/DELETE /api/v1/statuses/:id, context, history, source, reblogged_by, favourited_by, reblog/unreblog, favourite/unfavourite, bookmark/unbookmark, mute/unmute, pin/unpin; 4.5's GET …/quotes, POST …/quotes/:id/revoke and PUT …/interaction_policy |
| Timelines | home, public, tag/:hashtag, list/:id |
| Streaming | /api/v1/streaming (WebSocket) and /api/v1/streaming/* (server-sent events), health: user, user:notification, public, public:local, public:remote, hashtag, hashtag:local, list, direct |
| Accounts | verify_credentials (4.4: the profile scope too), update_credentials, search, lookup, relationships, familiar_followers (empty for every account by design: R8), GET /api/v1/accounts (id[]), :id, :id/statuses, followers, following, lists, featured_tags, endorsements (4.4; to the persona itself), follow, unfollow, remove_from_followers, block, unblock, mute, unmute, pin/unpin and 4.4's endorse/unendorse, note |
| Relationships | blocks, mutes, domain_blocks (GET, POST, DELETE), domain_blocks/preview (R9: how many follows and followers the block would take), follow_requests with authorize and reject, endorsements |
| Notifications | v1 notifications (index, :id, clear, unread_count, :id/dismiss), notifications/requests (index, :id, accept, dismiss, the bulk accept and dismiss, merged), notifications/policy (GET, PUT, PATCH); v2 grouped notifications (index, :group_key, :group_key/accounts, :group_key/dismiss, unread_count, clear since R9), notifications/policy (GET, PATCH, PUT since R9) |
| Web Push | POST, GET, PUT, DELETE /api/v1/push/subscription (R9; CLAUDE.md, Web Push) |
| Conversations | conversations (index), DELETE :id, :id/read, :id/unread (R9) |
| Media and polls | POST /api/v1/media, /api/v2/media, GET/PUT/DELETE /api/v1/media/:id; polls/:id, polls/:id/votes |
| Lists, filters, tags | lists (CRUD, :id/accounts GET/POST/DELETE); v1 filters and v2 filters with keywords and statuses (both nested and by id); tags/:name with follow, unfollow and 4.4's feature, unfeature; followed_tags; featured_tags (index, create, destroy) and featured_tags/suggestions |
| Discovery | /api/v2/search, trends and trends/tags, trends/statuses, trends/links, directory, suggestions (v1 index and destroy, v2 index) |
| The persona's own | bookmarks, favourites, markers (GET, POST), preferences, scheduled_statuses (index, show, update, destroy), profile/avatar and profile/header (DELETE), reports |
| The server | v1 and v2 instance, instance/peers (empty by design: which servers this one talks to is not published), rules, domain_blocks (the published ones), extended_description, privacy_policy, 4.4's terms_of_service and terms_of_service/:date, languages, translation_languages (none: translation is cut), activity (empty unless the administrator publishes it), custom_emojis, announcements with dismiss and reactions |
| Apps and OAuth | POST /api/v1/apps, apps/verify_credentials, /oauth/authorize, /oauth/token, /oauth/revoke, /.well-known/oauth-authorization-server, 4.4's /oauth/userinfo (GET and POST, the profile scope; R9) |
Stubs
| Route | Answer | Why |
|---|---|---|
POST /api/v1/statuses/:id/translate |
422 Translation is not available on this server |
translation is cut (owner decision 2026-10-17) |
GET /api/v1/timelines/link |
[] |
4.3's timeline of a trending link: not built (trending links themselves are served) |
GET /api/v1/annual_reports, 4.4's GET /api/v1/annual_reports/:year |
{"annual_reports": [], "accounts": [], "statuses": []} |
no yearly reports are made |
4.4's POST /api/v1/annual_reports/:year/read |
{} |
there is none to read |
POST /api/v1/accounts |
403 This action is not allowed (Mastodon's answer with registrations closed) |
registration through the API is cut: logins are made by setup or invitation on /clientapi |
POST /api/v1/emails/confirmations |
403 This method is only available while the e-mail is awaiting confirmation |
no account here waits for a confirmation |
GET /api/v1/emails/check_confirmation |
true |
the same |
GET /api/v1/peers/search |
[] |
peers are not published, as instance/peers |
4.4's GET /api/v1_alpha/async_refreshes/:id |
404 Record not found |
PrivaPub never sends Mastodon-Async-Refresh: a thread's replies are read in the background and show up |
GET /api/v1/accounts/:id/identity_proofs |
[] |
gone from Mastodon long ago; still asked by old clients |
every verb under /api/v1/admin/**, GET/POST under /api/v2/admin/** |
403 This action is not allowed, signed in or not |
the Mastodon admin API is cut: moderation is on /clientapi |
Missing, and not the client API
| Route | Why it is not served |
|---|---|
GET /api/oembed |
embedding a post in another website; PrivaPub's public pages have no embed view |
/api/web/* (settings, embeds, push_subscriptions) |
Mastodon's own web interface, signed in by its session cookie; PrivaPub's client is decePub on /clientapi |
/oauth/authorized_applications |
Doorkeeper's HTML page of a user's applications, in Mastodon's web interface |
/invite/:code (JSON) |
Mastodon's app shows an invitation before registering through the API, which is cut; PrivaPub's invitations are /clientapi's |
/.well-known/host-meta |
OStatus-era discovery; every client and server here uses WebFinger directly |
POST /api/v1/polls |
in 4.3's route list with no action behind it; gone in 4.4 |
Beyond routes: the public:media streams (and their :local and :remote forms) are not offered; the media-only public
timelines (only_media) are, so a client polls them.
PrivaPub's own, on the same API
/api/v1/pleroma/statuses/:id/reactions (Pleroma's emoji reactions), /api/v1/statuses/:id/downvote and undownvote,
/api/v1/feeds and /api/v1/timelines/feed/:id (PieFed's and Lemmy's feeds), and /api/privapub/v1/* (provenance,
servers, CDNs, events' join and leave): Mastodon invariant 7.