Mastodon client apps¶
microblog.pub exposes a subset of the Mastodon client REST API, so you can read and post to your instance from existing Mastodon apps — Tusky, Fedilab, Ivory, Ice Cubes, the official Mastodon app… — instead of (or alongside) the built-in web UI.
This is not a second identity: it’s the same single actor, the same posts, the same followers. The app just becomes another window onto your existing instance.
Connecting an app¶
In the app, enter your instance’s domain (the same one you log into
/adminwith) wherever it asks for a server/instance.The app registers itself and redirects you to your instance’s login page — log in with your admin password, then approve the app’s access request.
You’re in. The app now talks to your instance exactly like it would to a real Mastodon server.
There’s nothing to enable server-side — the API is always mounted, and
registrations/logins go through the same OAuth2 flow as
IndieAuth, reusing your existing admin
credentials rather than a separate account system. “Log out” in the app calls
POST /oauth/revoke, which kills the token server-side too — it isn’t just
forgotten on the device.
What works¶
Timelines — home, local/federated public, and hashtag timelines (
/api/v1/timelines/home,/public,/tag/:hashtag), withmax_id/since_id/min_idpagination and aLinkheader, like real Mastodon. The hashtag timeline takes Mastodon’s multi-tag parameters (any[],all[],none[]), so clients that build saved searches out of several tags work.Statuses — read, create, edit, delete; replies, content warnings, sensitive/media attachments, polls (including voting), and per-post language. Editing keeps full history (
/api/v1/statuses/:id/history), so clients can show what a post looked like before each edit. An attachment’s id inmedia_attachmentsis its underlying media id, somedia_idson an edit round-trips the same idsGET/POSThanded the client (older cached ids in the{status_id}-{index}form are still accepted).media_attributes[][id]/[description]/[focus]on an edit updates an attachment’s alt text/focal point in place, in either JSON or form bodies, and works even withoutmedia_idsin the same request.Polls — create one on a status (
poll[options][],expires_in,multiple), read it (/api/v1/polls/:id) and vote (/api/v1/polls/:id/votes), in either form-encoded or JSON bodies. The limits/api/v1/instanceadvertises (4 options, 100 characters each, 5 minutes to ~30 days) are the limits actually enforced, so a client building its composer from them never hits a surprise rejection.votes_countcounts votes cast andvoters_countcounts people — they differ on a multiple-choice poll, and both are tracked for your own polls as answers federate in. One vote per poll, as in Mastodon: a second attempt is a422rather than a duplicate vote delivered to the poll’s author, and voting in your own poll is a422too. Not supported:poll[hide_totals]is accepted and ignored — there’s no ActivityPub field for it, so tallies would still be public to every other server and to this instance’s own web UI; hiding them only in this API would be a false promise rather than a feature.Interactions — favourite, reblog, bookmark, pin, with their “who favourited/reblogged this” endpoints.
Quote posts —
quote_idonPOST /api/v1/statuses, and aquotekey next toreblogin the status entity:{state, quoted_status}, withstateone ofpending,accepted,rejected,revoked,deletedorunauthorized, andquoted_statuspopulated only onceaccepted. Quoting your own post is auto-authorized; quoting anyone else’s sends a federatedQuoteRequest(FEP-044f) and the status starts outpendinguntil the remote server responds. Thequote_policyconfig key (public/followers/manual/nobody, defaultpublic) controls whether an incomingQuoteRequestfor one of your own posts is auto-accepted, requires being a follower, needs manual approval (a notification with accept/reject buttons, like a follow request), or is always declined. A stamp you’ve granted can be revoked from/admin, which sends aDeletefederating the revocation; the quote then reportsrevokedhere (or, for a quote of a remote post,unauthorized— the inbox side has no dedicated revoked state). Not supported: the client-APIPOST /api/v1/statuses/:id/quotes/:quoting_status_id/revokeendpoint (Mastodon 4.5.0), since no client acts on it while this instance advertises 4.3.0.Link previews — posts containing a link carry a Mastodon
card, built from the OpenGraph metadata this instance already scrapes for its own web UI, so clients render the same preview box. The thumbnail goes through the media proxy, like every other remote image.Direct messages — surfaced as Mastodon “conversations” (
/api/v1/conversations), grouped the same way theDirect messagesadmin page groups them, with mark-as-read support.Notifications — follows, favourites, reblogs, mentions, moves, new posts from an account you follow with
notifyset, edits to a post you favourited or boosted, and your own polls (or ones you voted in) ending; read state, per-type filtering, clear/dismiss, and an unread count (/api/v1/notifications/unread_count) for badge counts. A poll ending is the one notification with no activity to react to, so it’s found by a sweep — no extra process is needed, the existingoutgoing_workerruns it as part of its poll, and the notification row itself is the watermark, so an ended poll is never notified twice.Grouped notifications (Mastodon 4.3+,
/api/v2/notifications*) — the “12 people favourited your post” screen. Favourites and reblogs group per post, follows group per UTC calendar day; everything else (mentions, status, update, poll, move, follow requests) stays one notification per group, same as v1.GET /{group_key},POST /{group_key}/dismiss,GET /{group_key}/accountsand a groups-awareGET /unread_countround out the surface;grouped_types[]narrows which types group, matching the spec.Read-position sync —
/api/v1/markersis genuinely persisted (home and notifications timelines), so “resume where I left off” survives across devices and reinstalls.Accounts & social graph — profile lookup (including the batch
/api/v1/accounts?id[]=...form some clients use), your own and remote actors’ statuses/followers/following (boosts included in your own profile, same as everyone else’s), follow/unfollow, block/unblock, the list of accounts you’ve blocked (/api/v1/blocks, so blocks can be reviewed and undone from a client), personal notes on an account, and incoming follow request approve/reject, with a realfollow_requests_countbadge on your own profile. A follower can also be dropped without blocking them (/api/v1/accounts/:id/remove_from_followers) — their server is told with a Reject of the original follow, and they’re free to follow again. Opening a remote actor you don’t follow yet backfills their recent posts and follower/following/post counts on demand (fetched and cached, throttled), so their profile isn’t empty on first view.POST /followacceptsreblogsandnotify, reflected back asshowing_reblogs/notifyingon the relationship entity:reblogs=falsehides that account’s boosts from every timeline (retroactively — toggling it back on unhides them), andnotify=truegenerates astatusnotification for their new top-level posts. Re-POSTing/followon an existing follow only touches the flags actually sent, and never sends a secondFollowactivity.Mutes — mute/unmute an account, with the
notificationsanddurationoptions, plus the list of who you’ve muted (/api/v1/mutes) and themuting/muting_notificationsrelationship flags. A muted account disappears from every timeline (their boosts, and other people’s boosts of them, included) but keeps following you and stays reachable from their profile — nothing is federated, so they can’t tell.Domain blocks (
/api/v1/domain_blocks) — theblocked_servershostnames fromprofile.toml, sorted. Read-only: it’s static config, so there’s noPOST/DELETEto add or remove a domain block from a client.Conversation mute — mute/unmute the thread a status belongs to (
/api/v1/statuses/:id/mute/unmute), so replies to a noisy thread stop generating notifications. The status entity’smutedflag reflects it, and it survives replies that arrive after the mute, not just the ones that exist yet.Featured tags (
/api/v1/featured_tags) — hashtags pinned to your profile viafeatured_tagsinprofile.toml, shown with their post counts. Read-only: this mirrors the config file, so there’s noPOST/DELETEto add or remove one from a client. The same list is federated as the actor’stoot:featuredTagscollection.Search (
/api/v2/search) — accounts, statuses, and hashtags.Media uploads, including descriptions/alt text — images, video and audio. Video/audio gets a real duration, a poster frame (extracted with
ffmpeg, reused aspreview_urland the APicon), and a blurhash, the same as images. There’s no transcoding: a file that uploads cleanly must already be playable in mainstream browsers, so an instance-side compatibility check runs against the codec/container/chroma subsampling (not just the mime type) and rejects confidently-broken files — e.g. HEVC from an iPhone, or a QuickTime.mov— with a422naming the specific problem and what to do about it (typically: re-encode as H.264/AAC in an MP4).ffmpegis optional; without it, uploads are still accepted, just without duration, poster, blurhash or compatibility checking.supported_mime_typesand the size limits in/api/v1/instance’smedia_attachmentsare real and enforced, not just advisory. Uploads are still processed synchronously —POST /api/v2/medianever returns Mastodon’s206/still-processing shape, so a very large upload occupies the request for the whole transfer + probeposter extraction. Media also accepts a
focuscropping hint (x,y, each in[-1.0, 1.0]) on create and update, echoed back asmeta.focusand federated (both ways) as the Pleroma-stylefocalPointattachment extension.
Instance “about” extras —
/api/v1/instance/rules(empty, none configured),/extended_description(the same bio text as the instance description), the public/instance/domain_blockstransparency list (hostname, digest, reason — distinct from the authenticated/api/v1/domain_blocksabove), and/activity(12 weeks of post counts and login counts, so “about this server” screens have something to plot instead of blanks).Push notifications (
/api/v1/push/subscription,GET/POST/PUT/DELETE) — real Web Push, end-to-end encrypted (VAPID +aes128gcm), for mentions, favourites, boosts, follows, follow requests, new posts (status), favourited/boosted post edits (update) and poll endings (poll), honouring the same mute/conversation-mute filtering the in-app notification list applies.standard: true; thealertsmap advertises all ten Mastodon keys, but the admin-onlyadmin.sign_up/admin.reportare always inert — this instance has no admin surface to notify about.policy(all/followed/follower/none) is honoured. New subscriptions default every alert totrue(upstream Mastodon defaults themfalse, which leaves a fresh subscription silently inert until the client calls update — every real client sends explicit alerts anyway, so this instance opts for the less surprising default). Deployment note: delivery runs in a separatepush_workerprocess — seedocs/install.mdfor the supervisord entry. An install that skips wiring it up will still accept subscriptions and advertise a VAPID key, just never deliver anything.Scheduled posts —
POST /api/v1/statuseswithscheduled_atqueues the post instead of sending it, and returns Mastodon’sScheduledStatusentity;/api/v1/scheduled_statuseslists the queue, withGET/PUT/DELETEon a single entry (PUTchanges the publication time, the only field Mastodon makes editable). Everything an immediate post supports carries over — attachments, CW/sensitive, visibility, language, replies, polls — and is validated when you queue it, not when it comes due. Any time in the future is accepted, where upstream Mastodon insists on at least five minutes out. No extra process is needed: the existingoutgoing_workerpublishes due posts as part of its poll, so a queued post goes out within a couple of seconds of its time. If publishing fails (say an attachment was deleted in the meantime) it’s retried with a growing backoff and then left in the queue rather than disappearing — rescheduling it withPUTgives it a fresh set of attempts.Streaming API (
wss://…/api/v1/streaming, WebSocket only — no SSE) —user,user:notification,public,public:local,public:remote,hashtaganddirectstreams, deliveringupdate,status.update,delete,notificationandconversationevents. Unlike Web Push, this needs no separate process: the server runs as a single process/event loop, so a small in-process task polls committed rows (~1s interval,streaming_poll_interval) and fans out over the open sockets — the same filtering (mutes, visibility) the REST timelines apply, since it re-queries through the same functions rather than duplicating the logic.deleteandstatus.updateare best-effort over a bounded window (the newest ~500 statuses per table plus anything streamed since connecting) — a much older status, deleted, produces no frame; the client’s own list still updates on its next REST fetch. Not supported:liststreams (Lists are an empty stub, see below) and thepublic:*:mediavariants. One socket may hold at most 64 subscriptions (hashtagstreams carry a client-supplied tag, so the set needs a bound); a 65thsubscribegets an error frame and is ignored. Deployment note: the reverse proxy must forward the WebSocket upgrade on this path specifically — see thelocation /api/v1/streamingblock indocs/install.md’s nginx snippet.streaming_enabled = falseindata/profile.tomldisables the endpoint and removes the advertisement.
What doesn’t (single-user degradations)¶
microblog.pub is one instance, one actor — several Mastodon API areas exist for things a single-user server has no data for. These degrade gracefully (an empty list, or a harmless no-op) rather than erroring, so clients render an empty state instead of crashing:
Lists, filters, suggestions, the directory, trends, and familiar followers — always empty.
Federated peers (
/api/v1/instance/peers) — always empty. This one’s a deliberate privacy choice rather than a missing feature: the data exists, but publishing which servers you’ve federated with is worth opting out of.Notification requests / policy — this server never filters notifications, so the filtered-notifications queue (
/api/v1/notifications/requests) is always empty and the policy (/api/v2/notifications/policy) always reports “accept everything”; nothing is held back for approval.
Scopes¶
Standard Mastodon OAuth scopes are supported, including the granular
read:*/write:* forms — a token granted the top-level read/write/follow
scope satisfies any of the matching granular scopes underneath it, same as real
Mastodon. Most apps request a broad read write follow push by default; push
is a real scope here too, gating the push subscription endpoints above.
Troubleshooting¶
A client shows “not mocked”/network errors on first login: double-check you entered your bare domain (no
https://, no trailing slash) in the app’s “instance” field.Nothing shows up on first sync: some clients only backfill a page or two of history on first login; give it a pull-to-refresh.
Push notifications never arrive: confirm the
push_workerprocess is running (supervisorctl status) and checkdata/push.logfor delivery errors. If it’s running and logging clean 2xx/201 responses but nothing shows up on the device, double-check theserver_keyyour client subscribed with still matches/api/v1/instance’sconfiguration.vapid.public_key— a regenerated VAPID key invalidates every existing subscription, and the client needs to re-subscribe.Streaming never connects (client stuck “connecting…”): check that the reverse proxy has a dedicated location forwarding the WebSocket upgrade for
/api/v1/streamingspecifically — a genericproxy_passwithoutproxy_set_header Upgrade/Connectionwill accept the TCP connection and then hang, since ordinary HTTP proxying doesn’t forward the upgrade. Also checkproxy_read_timeoutis generous (an idle socket dying at the default 60s reads to the client as an unexplained disconnect).If something a real Mastodon client relies on 404s instead of degrading gracefully, that’s a gap worth reporting an issue for — the API surface above is what’s implemented today, not a hard ceiling.