Verified against W3C ActivityPub REC, AS2 Core, RFC 7033, Mastodon v4.7.3 source (fetch_remote_actor_service.rb, json_ld_helper.rb, signed_request.rb, create.rb) on 2026-10-15.
Compliance Checklist
Use this checklist to verify your ActivityPub implementation meets the specification requirements and works with other Fediverse software.
Discovery
WebFinger (RFC 7033)
-
GET /.well-known/webfinger?resource=acct:user@domainanswers for your local users only - Response is
application/jrd+json -
subjectmatches the requested resource - A
rel="self"link withtype="application/activity+json"points to the actor - Sends
Access-Control-Allow-Origin— a MUST in RFC 7033 §5
Actor
Required
-
id— HTTPS URL that dereferences to this document (AP §3.1) -
type—Person,Group,Service,ApplicationorOrganization -
inboxandoutbox— MUST (AP §4.1); Mastodon rejects an actor without an inbox -
preferredUsername(or a FEP-2c59webfingerproperty) — Mastodon rejects the actor without one (source) -
publicKeywithid,ownerequal to the actorid, and an SPKIpublicKeyPem— without a key no one can verify your signatures
Recommended
-
followersandfollowing— SHOULD (AP §4.1) -
endpoints.sharedInbox— lets servers deliver once per server -
name,summary(HTML),icon,image,url(the human-readable profile page)
Content negotiation
- Actor URL returns JSON for
Accept: application/activity+json - Actor URL returns JSON for
Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams" - Response
Content-Typeisapplication/activity+json(orapplication/ld+jsonwith the Activity Streams profile) — Mastodon discards anything else (source) - Optional: HTML for browsers, with
Vary: Accept
Inbox
Receiving activities
- Accepts
POSTwithapplication/activity+jsonorapplication/ld+json - Returns
202 Accepted(or200) for activities it accepts, including types it ignores - Returns
401for missing or invalid signatures - Returns
400for malformed JSON
Verifying requests (tested code)
- The signature covers
(request-target),host,dateanddigest -
Digestis checked against the raw request body -
Dateis within a window (Mastodon: 5 minutes plus 1 hour of clock skew) - The key is fetched from
keyIdsafely (no private addresses — SSRF) and is listed by its owner actor - The key's owner is
activity.actor - Embedded objects come from the actor's origin (origin rules)
Activity handling
-
Create— store new content -
Update— only from the object's owner -
Delete— only from the object's owner (Deletion) -
Follow→ sendAcceptorReject -
Accept/Reject— only for aFollowyou actually sent -
Undo— reversesFollow,Like,Announce,Block -
Like,Announce -
Block— Mastodon sends it; stop delivering to the blocker
Outbox and delivery
-
GETreturns anOrderedCollection, reverse chronological, paginated - Every activity has
@context, a uniqueid,typeandactor - Delivery goes to each recipient inbox, using
sharedInboxwhen available - Requests are signed (draft-cavage
rsa-sha256is what most servers verify; see HTTP signatures in 2026) -
DigestandDateheaders are sent and signed - Failed deliveries are retried with backoff;
404/410are final (Delivery at Scale)
Collections
-
followersandfollowingexist - They are
OrderedCollectionorCollection, withtotalItems - Large collections are paginated (
first,next)
Objects
Notes and posts
-
typeisNote(orArticle,Question, …) -
idis a unique HTTPS URL on your server -
attributedTois the author -
contentis HTML -
publishedis an RFC 3339 date-time, e.g.2026-10-15T12:00:00Z(AS2 §2.3) -
to/ccaddress the right audience (a single value or an array) -
inReplyToon replies;tagwithMentionandHashtagentries
Interoperability
Mastodon
- Every addressed actor in a direct message has a
Mentiontag (otherwise Mastodon treats it as "limited") -
sensitiveandsummary(content warning) are honoured -
manuallyApprovesFollowersis respected for locked accounts - Optional Mastodon extensions from the
toot:namespace —Emoji,blurhash,discoverable,indexable— only if you use them (Mastodon's extensions)
Federation testing
- Can follow, and be followed by, a Mastodon account
- Posts appear on Mastodon, and Mastodon posts arrive
- Likes and boosts arrive
- Replies thread correctly (Threads)
Automated Testing
Test Script
async function runComplianceTests(domain, username) {
const results = {
passed: [],
failed: [],
warnings: []
};
// WebFinger test
try {
const wf = await testWebFinger(domain, username);
if (wf.success) {
results.passed.push('WebFinger');
} else {
results.failed.push(`WebFinger: ${wf.error}`);
}
} catch (e) {
results.failed.push(`WebFinger: ${e.message}`);
}
// Actor test
try {
const actor = await testActor(domain, username);
if (actor.success) {
results.passed.push('Actor');
} else {
results.failed.push(`Actor: ${actor.error}`);
}
} catch (e) {
results.failed.push(`Actor: ${e.message}`);
}
// Add more tests...
return results;
}
async function testWebFinger(domain, username) {
const url = `https://${domain}/.well-known/webfinger?resource=acct:${username}@${domain}`;
const res = await fetch(url);
if (res.status !== 200) {
return { success: false, error: `Status ${res.status}` };
}
const data = await res.json();
if (!data.subject) {
return { success: false, error: 'Missing subject' };
}
// Mastodon uses application/activity+json; some servers use the
// ld+json profile type instead, so accept both
const selfLink = data.links?.find(l =>
l.rel === 'self' && /^application\/(activity\+json|ld\+json)/.test(l.type || '')
);
if (!selfLink) {
return { success: false, error: 'Missing self link' };
}
return { success: true, actorUrl: selfLink.href };
}
async function testActor(domain, username) {
const wf = await testWebFinger(domain, username);
if (!wf.success) return wf;
// Unsigned: servers in secure mode (e.g. mastodon.social) answer 401
const res = await fetch(wf.actorUrl, {
headers: { 'Accept': 'application/activity+json' }
});
if (res.status !== 200) {
return { success: false, error: `Status ${res.status}` };
}
const actor = await res.json();
const required = ['id', 'type', 'inbox', 'outbox'];
const missing = required.filter(k => !actor[k]);
if (missing.length > 0) {
return { success: false, error: `Missing: ${missing.join(', ')}` };
}
return { success: true, actor };
}
Test Results Template
# ActivityPub Compliance Report
**Server:** example.com
**Date:** 2026-10-15
**Tester:** [your name]
## Summary
- Passed: 25/30
- Failed: 3/30
- Warnings: 2
## Results
### Discovery
- [x] WebFinger endpoint
- [x] Self link present
- [ ] CORS headers - Missing Access-Control-Allow-Origin
### Actor
- [x] Required properties
- [x] Public key
- [ ] Shared inbox - Not configured
### Inbox
- [x] Accepts activities
- [x] Signature verification
- [x] Activity handling
### Outbox
- [x] Returns collection
- [x] Delivery works
- [ ] Retry logic - Not implemented
## Recommendations
1. Add CORS headers to WebFinger
2. Configure shared inbox for efficiency
3. Implement delivery retry queue
Related Tools
- WebFinger Lookup - Test discovery
- Actor Inspector - Validate actors
- HTTP signatures in 2026 - Debugging 401s
- Troubleshooting by symptom - Troubleshooting