Verified against Mastodon v4.7.3 source (signature verification, inbox, Create/Accept/Follow/Delete handlers, WebFinger client, delivery worker); Mastodon v4.5.4 release notes; Lemmy 0.19.20 fixtures on 2026-10-15.
Troubleshooting by Symptom
Start from what you see, not from the spec. Each section lists the likely causes, most common first, with how to confirm each and how to fix it. Mastodon behaviour is cited to its v4.7.3 source; most other servers are stricter or looser in similar places.
Before you start
Get the error text. When Mastodon rejects a signed request it returns the reason as JSON in the response body, for example {"error":"Signed request date outside acceptable time window"} (signature_verification.rb L85-90). Log the status code and body of every outgoing delivery.
202 Accepted does not mean "processed". Mastodon's inbox only verifies the signature, queues the payload and returns 202; parsing and storing happen later in a Sidekiq job (inboxes_controller.rb L12-17, L92-94). Activities dropped at that stage leave no trace on your side.
Read the receiving server's logs when you run it (or ask its admin). On a standard Mastodon install the processes run as the mastodon-web and mastodon-sidekiq systemd units (dist/), logging at info by default (production.rb L55):
# Rejected Create/Announce payloads are logged at info level
journalctl -u mastodon-sidekiq --since "1 hour ago" | grep "Rejected"
journalctl -u mastodon-web --since "1 hour ago" | grep -i "inbox"
Look at your own objects the way a server does:
# Actor / object: must be 200 with an ActivityPub media type
curl -s -D- -H 'Accept: application/activity+json' https://your.example/users/alice | head -20
# WebFinger
curl -s 'https://your.example/.well-known/webfinger?resource=acct:alice@your.example'
BrowserPub and fedify lookup from the Fedify CLI fetch and render objects the way a federating server would. fedify inbox starts a throwaway public actor and inbox so you can follow your server and see exactly what it sends; fedify tunnel exposes a local dev server over HTTPS (see Testing & debugging tools).
Developing on localhost? Mastodon refuses to fetch private or loopback addresses: you'll see Requests to private network addresses are disallowed (tried to query …) (signature_verification.rb L109-110). Use a public tunnel.
"401 from Mastodon" when delivering
Read the error field of the response body; it maps directly to a cause.
| Error message | Cause | Fix |
|---|---|---|
Request not signed | No Signature header | Sign every POST (HTTP signatures) |
Mastodon requires the Digest header to be signed when doing a POST request | digest not in the signed headers list | Sign (request-target) host date digest |
Digest header missing | Signed digest but didn't send the header | Send Digest: SHA-256=… |
Invalid Digest value. Computed SHA-256 digest: X; given: Y | Digest computed over a different body than the bytes sent — usually re-serialising the JSON after hashing it | Hash the exact bytes you send (signed_request.rb L63-79) |
Signed request date outside acceptable time window | Date too old or in the future. A draft-cavage signature is valid for 5 minutes after its date, plus 1 hour of clock skew (signed_request.rb L256-263) | Sign with the current time, sync your clock (NTP), and don't sign once then retry hours later |
Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported) | Unknown algorithm value (L244-246) | Use rsa-sha256 or hs2019 |
Public key not found for key … / Unable to fetch key JSON at … | Mastodon couldn't fetch your keyId: wrong URL, 404, or served with a non-ActivityPub content type | Make keyId dereferenceable and served as application/activity+json |
Actor … has neither 'preferredUsername' nor `webfinger` …, Webfinger response for … does not loop back to … | Fetching your key triggers a full actor fetch, including the WebFinger round-trip; those errors surface as 401s (signature_verification.rb L102-112) | Fix the actor/WebFinger — see Mastodon can't find or show my profile |
publicKey id for … does not correspond to … | The key document's owner actor doesn't list that key (fetch_remote_key_service.rb L25-28) | publicKey.id on the actor must equal the keyId you sign with |
Verification failed for … | Key found and refreshed once, but the signature still doesn't match (L60-68) | Rebuild the signing string exactly: lower-case header names, (request-target) is post /path?query, host matches the URL you POST to, headers in the order listed |
Mastodon refetches your key once before failing, so a rotated key fixes itself if your actor already serves the new key (signature_verification.rb L60-66). Mastodon retries 401s (up to 16 times with a growing back-off) but treats most other 4xx responses as permanent and drops the delivery (delivery_worker.rb L11-21, L61-79).
Full background, including RFC 9421: HTTP signatures in 2026.
"503 when Mastodon fetches my key"
Since Mastodon 4.5.4, a temporary failure to fetch the signer's actor or key returns 503 instead of 401 (v4.5.4 release notes). The body is one of (signature_verification.rb L74-82):
Failed to fetch remote data: …— connection error, timeout or TLS failure reaching your server.Failed to fetch remote data (got unexpected reply from server)— your actor/key URL returned an error status.Fetching attempt skipped because of recent connection failure— after a single connection or TLS failure from your IP, Mastodon stops trying for 5 minutes (signature_verification.rb L10-11, L120-127).
Fix: make your actor URL reliably reachable over valid HTTPS, then wait out the 5-minute cool-off before retesting. A 503 is retried by the sender, so nothing is lost if the outage is short.
"Follow stays pending"
A Mastodon user follows my actor, and it never completes
- You never sent an
Accept. Mastodon only completes the follow when it receives anAcceptfrom the followed actor. - The
Acceptdoesn't reference the Follow. Mastodon looks up the pending request byobjectid — theidof theFollowit sent you, exactly (activity.rb L148-150, accept.rb L4-26). If you embed theFollowobject instead, itsactormust be the Mastodon account. A freshly minted Follow id won't match. - The
Acceptis signed by a different actor than itsactorproperty. Mastodon drops activities whoseactorisn't the signer, unless they carry a valid LD signature or integrity proof (process_activity_service.rb L36-58). - The delivery itself failed — check the response body as in 401 from Mastodon.
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://your.example/activities/accept/1",
"type": "Accept",
"actor": "https://your.example/users/alice",
"object": "https://mastodon.example/2b8e…"
}
Here object is the id of the Follow you received. Send it to the follower's inbox.
My actor follows a Mastodon user, and it stays "requested"
- The account is locked. Mastodon waits for its owner to approve (follow.rb L32-38).
- Mastodon sent an
Acceptand your inbox rejected or ignored it. Mastodon sendsAccept { object: <your Follow> }to your actor's personalinbox(authorize_follow_service.rb L20-26), signed by the followed account. Your inbox must verify that signature (see Verifying incoming requests) and matchobject.idagainst the Follow you sent. - You got a
Reject. Mastodon rejects follows when the target blocks you or your domain, has moved, or is the instance actor (follow.rb L18-21). - Your
Follow'sobjectisn't the account's actor URI. Use theidfrom the actor document, not a profile page URL. - Mastodon stopped delivering to you. After deliveries to your server are exhausted on 7 different days, Mastodon marks your domain unavailable and stops sending (delivery_failure_tracker.rb L6-19, L111-113). Any successfully verified request from your server to its inbox clears this (inboxes_controller.rb L73-80).
Mastodon can't find or show my profile
Searching @alice@your.example triggers a WebFinger lookup, then an actor fetch. Every step must pass:
- WebFinger has no usable
selflink. The JRD needssubjectand a link withrel: "self"andtypeeitherapplication/activity+jsonorapplication/ld+json; profile="https://www.w3.org/ns/activitystreams"(webfinger.rb L9, L38-46). Errors:Missing subject in response for …,Missing self link in response for …. - The actor doesn't come back as ActivityPub. Mastodon requires HTTP 200 with
Content-Type: application/activity+json(orapplication/ld+jsonwith the ActivityStreams profile); anything else — includingapplication/json— is treated as no document (json_ld_helper.rb L272-282, L351-361). - The actor's
idisn't the URL it was fetched from. Mastodon refetches byidand discards documents whoseiddoesn't match (json_ld_helper.rb L242-252). - Missing
@context, wrongtype, or no handle. The actor must includehttps://www.w3.org/ns/activitystreamsin@context, be one ofApplication,Group,Organization,Person,Service, and havepreferredUsername(or an FEP-2c59webfingerproperty) (fetch_remote_actor_service.rb L9, L30-34). Usernames must match[a-z0-9_]+([.-]+[a-z0-9_]+)*(account.rb L75-78). - WebFinger doesn't "loop back". Mastodon WebFingers
preferredUsername@<actor host>(or the FEP-2c59webfingervalue) and checks that theselflink equals the actorid; otherwise:Webfinger response for … does not loop back to …(process_account_service.rb L133-182). With split domains (actor onap.example.com, handle@alice@example.com), the actor host's WebFinger may answer withsubject: acct:alice@example.com; Mastodon then queriesexample.com, which must return the same subject and aselflink to the same actor. Settingwebfinger: "alice@example.com"on the actor skips the first hop. - Your domain is blocked by that server's admin. Signed requests from blocked domains get 403 (signature_verification.rb L95-97).
- The search isn't resolving remotely. Mastodon only resolves remote accounts for authenticated searches with
resolveset; anonymous resolve requests get401(api/v2/search_controller.rb L12-14, L38-40). Search for the full@user@domainor the actor URL while logged in.
Profile changes don't show up immediately: Mastodon re-fetches a known remote account after about a day (account.rb L71, L284-286). Send an Update of your actor to your followers to refresh it straight away.
Guide: WebFinger implementation, Mastodon compatibility.
My posts don't show up for Mastodon followers
The delivery got 202, but the post never appears. Check the receiving Sidekiq log for Rejected Create activity <id> from <actor> (activity.rb L188-191). Causes, in order:
- The object type isn't supported. Mastodon stores
NoteandQuestion, and convertsArticle,Page,Image,Video,AudioandEventinto simplified statuses (activity.rb L8-10). Anything else is rejected. - The object
idis on a different host from the actor (create.rb L17-19). - The post isn't "related to local activity". A delivered public or unlisted post is only kept if a local account follows you, it replies to a local or followed account's post, it addresses a local account, or it came through a relay. Followers-only posts need a local follower or addressee; direct messages need a local addressee (create.rb L464-485).
- No ActivityStreams
@contexton the activity (process_activity_service.rb L34). - Signer ≠
actor— e.g. a shared bot key signing for many actors (process_activity_service.rb L36-58). - A Tombstone exists for that id. Once you've deleted an id, Mastodon won't accept a
Createfor it again (create.rb L17-19). Use new ids. - Payload over 1 MB returns
413from the inbox (inboxes_controller.rb L21-23, activity.rb L8).
Check visibility too: to: as:Public makes a post public, cc: as:Public unlisted, followers collection only means followers-only (Mastodon compatibility).
My actor shows on Mastodon, but older posts are missing
This is expected. Mastodon does not import a remote actor's outbox. It shows only posts it already has: ones delivered to it after someone there followed you, or ones a user fetched by pasting the post URL into search. It loads your featured (pinned) collection when processing your actor (process_account_service.rb L93-95). The profile links to your server for the full history.
To get a post onto a server, deliver it to a follower there, or have someone search for its URL. The object must be fetchable: HTTP 200, ActivityPub content type, and id equal to the URL.
"Replies don't thread"
inReplyTodoesn't hold the parent'sid. Mastodon matches the parent by URI (an object with anidalso works) (create.rb L434-448). A link to an HTML page, or a fragment of a different URL, won't match.- Mastodon can't fetch the parent. When it doesn't have the parent, it queues a
ThreadResolveWorkerto fetch it (create.rb L382-386, thread_resolve_worker.rb). The parent must be publicly fetchable as described above. If your server requires signed fetches, it must accept requests signed by Mastodon's instance actor; Mastodon signs fetches on behalf of that actor by default (json_ld_helper.rb L272-275). - You aren't delivering the reply to the parent's author. Include the parent author in
cc, add aMentiontag for them, and deliver to their inbox. A reply to a local post is what lets the receiving server keep it (see above) and forward it to its followers (create.rb L491-495). - Reply collections for backfill. Mastodon reads an object's
repliescollection to pull in replies it missed (create.rb L400-411), so publish arepliescollection for your posts.
"Mentions don't notify"
A Mastodon user gets a notification only when the mention is both tagged and addressed:
- Add a
Mentiontag whosehrefis the mentioned actor'sid:{ "type": "Mention", "href": "https://mastodon.example/users/bob", "name": "@bob@mastodon.example" }(create.rb L255-266). - Address them in
toorcc. Tagged accounts that aren't in the audience are silenced: they get no notification (create.rb L153-155). Addressed accounts with noMentiontag get a silent mention: they can see the post but aren't notified (create.rb L140-145). - Deliver to their inbox. The mention only reaches them if their server receives the post.
Putting a link to @bob in content does nothing on its own.
"Delete didn't propagate"
- The
Deletedidn't go everywhere the post went. Each server only learns about a delete if it's delivered there. Record where you delivered each post, or deliver theDeleteto all followers' inboxes and anyone mentioned. - The
objectid doesn't match. Mastodon looks the status up byuriand author (delete.rb L19-56). Send the post's exactid(aTombstonewith thatidis fine). - The
Deletedoesn't come from the author. Mastodon only removes a status when the signing actor wrote it, and only records a tombstone for ids on the actor's own host (delete.rb L19-56). - Your fetch endpoint still serves the post. After deleting, return
410 Gone(or404) for the object URL, so servers that re-fetch it see that it's gone.
A Delete that arrives before its Create is remembered for 6 hours, and the late Create is discarded (activity.rb L98-104, create.rb L32). Deleting an actor uses Delete with object equal to the actor's own id (delete.rb L4-17). If an actor Mastodon has never seen sends a Delete or Update of itself, Mastodon returns 202 and skips it (inboxes_controller.rb L29-38).
"Images / attachments don't appear"
- The attachment uses
href. Mastodon treats any attachment with anhrefas a link (preview card), not media. Media must useurl(create.rb L300-311). - Unsupported
mediaType. Images:image/jpeg,png,gif,heic,heif,webp,avif. Video:video/webm,mp4,quicktime,ogg. Plus a list of audio types (media_attachment.rb L64-68). Unsupported types become a link, not inline media. - More than 4 attachments. The rest are ignored (status.rb L62, create.rb L314).
- Too large. Images over 16 MB, video and audio over 99 MB (media_attachment.rb L46-47).
- The file URL isn't publicly fetchable. Mastodon downloads media with a plain, unsigned GET (remotable.rb L24), so media must not require HTTP signatures, cookies or private IPs. Failed downloads are retried later (create.rb L328-337).
- The receiving server rejects media from your domain (an admin "reject media" domain block) (create.rb L454-458).
Add name (alt text), and optionally blurhash and focalPoint. See Media attachments.
"Lemmy community posts aren't appearing"
- You never completed the follow. Follow the community's
Groupactor and wait for itsAccept(Following a community). Lemmy only delivers to subscribed instances, and only from the moment you subscribe. Earlier posts are in the community'soutbox. - You're looking for
Create, but Lemmy sendsAnnounce. The community wraps every post, comment, vote, edit and delete in anAnnouncesigned by the community (Community announce, fixture). Unwrap the inner activity. Re-fetch it from its own origin before trusting it, because the announcer isn't its author (Handling community announces). - Posts are
Page, notNote. Link posts carry the link inattachment[{type: "Link", href}](Posts). - Your inbox rejected the community's signature. Check your own logs for verification failures on requests from the community actor.
Response codes at a glance
How Mastodon reacts to the status your inbox returns (delivery_worker.rb, json_ld_helper.rb L377-379):
| Your response | Mastodon's delivery worker |
|---|---|
| 2xx | Success |
| 401, 408, 429, 5xx | Retried with back-off, up to 16 times |
| Other 4xx (400, 403, 404, 410, …) and 501 | Treated as permanent and dropped |
When you receive deliveries, return 202 once a request is accepted for processing. Return 401 for bad signatures and 400 for malformed JSON. Don't return 5xx for things a retry can't fix.
General debugging workflow
- Find the failing step: WebFinger, actor fetch, key fetch, inbox POST, or async processing.
- Capture the exact request and response: headers, raw body bytes, status, and the response body.
- Reproduce against a known-good peer. Fetch a real Mastodon actor and post with
fedify lookupor BrowserPub and compare field by field with yours. Usefedify inboxto capture what your server sends. - Check the receiving side's logs if you control it, or a test instance you run yourself.
Log the raw body (bytes, not re-serialised JSON) next to the headers for both incoming and outgoing requests. Most signature and digest bugs are visible in that diff.
Low-level checks:
# Verbose request/response, including TLS
curl -v -H 'Accept: application/activity+json' https://your.example/users/alice
# DNS and certificate
dig your.example
openssl s_client -connect your.example:443 -servername your.example </dev/null
Online helpers: webfinger.net for WebFinger, the JSON-LD playground for expansion, and RequestBin to inspect raw requests.