Verified against RFC 7033 §5, Mastodon v4.7.3 cors.rb, live actors on mastodon.social, lemmy.ml, pixelfed.social and framatube.org (2026-10-15), Lemmy 0.19.20 person.json fixture on 2026-10-15.
Actor Inspector
Enter a handle or actor URL. Your browser looks it up with WebFinger, fetches the actor document directly from that server, and runs the Activity Linter rules on it, including the Content-Type check Mastodon applies.
:::note What a browser can't fetch
- CORS. RFC 7033 §5 requires WebFinger to send
Access-Control-Allow-Origin, so the lookup usually works. Actor endpoints have no such rule: Mastodon allows CORS on/users/:username, but many other servers don't, and the browser then hides the response. - Signed fetch. Servers in Mastodon's secure mode (mastodon.social among them) answer unsigned requests with 401. A browser can't sign as a server.
In both cases, fetch the actor with fedify lookup from the Fedify CLI, which signs its requests, and paste the JSON into the box below the tool.
:::
Inspecting ActivityPub actors helps debug federation issues and understand how different platforms represent users, groups, and services.
Fetching Actors
Command Line
mastodon.social runs in secure mode, so these unsigned requests get 401 for the actor (its outbox still answers). Use them against your own server or one without secure mode, or let fedify lookup @Gargron@mastodon.social sign the request for you.
# Fetch an actor with proper Accept header
curl -H "Accept: application/activity+json" \
"https://mastodon.social/users/Gargron" | jq .
# Alternative content type
curl -H "Accept: application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" \
"https://mastodon.social/users/Gargron" | jq .
JavaScript
async function fetchActor(url) {
const response = await fetch(url, {
headers: {
'Accept': 'application/activity+json, application/ld+json'
}
});
if (!response.ok) {
throw new Error(`Failed to fetch actor: ${response.status}`);
}
return response.json();
}
// Usage
const actor = await fetchActor('https://mastodon.social/users/Gargron');
console.log(actor);
Actor Structure
Typical Mastodon Actor
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"discoverable": "toot:discoverable",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers"
}
],
"id": "https://mastodon.social/users/Gargron",
"type": "Person",
"preferredUsername": "Gargron",
"name": "Eugen Rochko",
"summary": "<p>Developer of Mastodon</p>",
"inbox": "https://mastodon.social/users/Gargron/inbox",
"outbox": "https://mastodon.social/users/Gargron/outbox",
"followers": "https://mastodon.social/users/Gargron/followers",
"following": "https://mastodon.social/users/Gargron/following",
"publicKey": {
"id": "https://mastodon.social/users/Gargron#main-key",
"owner": "https://mastodon.social/users/Gargron",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
},
"endpoints": {
"sharedInbox": "https://mastodon.social/inbox"
}
}
Validation Checklist
┌────────────────────────────────────────────────────────────┐
│ ACTOR VALIDATION │
├────────────────────────────────────────────────────────────┤
│ │
│ Required Properties │
│ ✓ id - Unique URI │
│ ✓ type - Person, Group, Service, etc. │
│ ✓ inbox - URL to receive activities │
│ ✓ outbox - URL of published activities │
│ │
│ Recommended Properties │
│ ○ preferredUsername - The @handle │
│ ○ name - Display name │
│ ○ publicKey - For HTTP Signatures │
│ ○ followers - Followers collection │
│ ○ following - Following collection │
│ ○ endpoints.sharedInbox - For efficient delivery │
│ │
└────────────────────────────────────────────────────────────┘
Inspecting actors in code
Validation script
class ActorInspector {
constructor(actorUrl) {
this.url = actorUrl;
this.actor = null;
this.errors = [];
this.warnings = [];
}
async fetch() {
const response = await fetch(this.url, {
headers: { 'Accept': 'application/activity+json' }
});
if (!response.ok) {
this.errors.push(`Fetch failed: ${response.status}`);
return false;
}
this.actor = await response.json();
return true;
}
validate() {
const a = this.actor;
// Required fields
if (!a.id) this.errors.push('Missing required: id');
if (!a.type) this.errors.push('Missing required: type');
if (!a.inbox) this.errors.push('Missing required: inbox');
if (!a.outbox) this.errors.push('Missing required: outbox');
// Type validation
const validTypes = ['Person', 'Group', 'Organization', 'Application', 'Service'];
if (a.type && !validTypes.includes(a.type)) {
this.warnings.push(`Unusual actor type: ${a.type}`);
}
// URL validation
if (a.id && !a.id.startsWith('https://')) {
this.errors.push('Actor id must be HTTPS');
}
if (a.inbox && !a.inbox.startsWith('https://')) {
this.errors.push('Inbox must be HTTPS');
}
// Recommended fields
if (!a.preferredUsername) {
this.warnings.push('Missing recommended: preferredUsername');
}
if (!a.publicKey) {
this.warnings.push('Missing recommended: publicKey (needed for federation)');
}
if (!a.endpoints?.sharedInbox) {
this.warnings.push('Missing recommended: endpoints.sharedInbox');
}
// Public key validation
if (a.publicKey) {
if (!a.publicKey.id) this.errors.push('publicKey missing id');
if (!a.publicKey.owner) this.errors.push('publicKey missing owner');
if (!a.publicKey.publicKeyPem) this.errors.push('publicKey missing publicKeyPem');
if (a.publicKey.owner && a.publicKey.owner !== a.id) {
this.errors.push('publicKey.owner must match actor id');
}
}
return this.errors.length === 0;
}
report() {
console.log('=== Actor Inspector Report ===\n');
console.log(`URL: ${this.url}`);
console.log(`Type: ${this.actor?.type}`);
console.log(`Username: ${this.actor?.preferredUsername}`);
console.log(`Name: ${this.actor?.name || '(not set)'}`);
console.log('\n--- Endpoints ---');
console.log(`Inbox: ${this.actor?.inbox}`);
console.log(`Outbox: ${this.actor?.outbox}`);
console.log(`Shared Inbox: ${this.actor?.endpoints?.sharedInbox || '(not set)'}`);
if (this.errors.length > 0) {
console.log('\n--- Errors ---');
this.errors.forEach(e => console.log(`❌ ${e}`));
}
if (this.warnings.length > 0) {
console.log('\n--- Warnings ---');
this.warnings.forEach(w => console.log(`⚠️ ${w}`));
}
if (this.errors.length === 0 && this.warnings.length === 0) {
console.log('\n✅ All checks passed!');
}
}
}
// Usage
const inspector = new ActorInspector('https://mastodon.social/users/Gargron');
await inspector.fetch();
inspector.validate();
inspector.report();
Comparing Actors
Platform Differences
| Property | Mastodon | Lemmy | Pixelfed | PeerTube |
|---|---|---|---|---|
| type | Person | Person (users), Group (communities) | Person | Person (accounts), Group (channels) |
| preferredUsername | ✓ | ✓ | ✓ | ✓ |
| name | Display name | Display name | Display name | Channel name |
| summary | HTML bio | HTML bio | HTML bio | HTML description |
| icon | Avatar | Avatar | Avatar | Avatar |
| image | Header | Banner | not published | not observed |
| discoverable | ✓ | — | — | ✓ |
Observed on live actors (mastodon.social, lemmy.ml, pixelfed.social, framatube.org) on 2026-10-15; "not observed" means the sample didn't include it.
Fetch Multiple Actors
async function compareActors(urls) {
const actors = await Promise.all(
urls.map(async url => {
try {
const response = await fetch(url, {
headers: { 'Accept': 'application/activity+json' }
});
return { url, actor: await response.json() };
} catch (error) {
return { url, error: error.message };
}
})
);
// Compare properties
const properties = ['type', 'preferredUsername', 'name', 'inbox', 'outbox'];
console.log('Property comparison:');
properties.forEach(prop => {
console.log(`\n${prop}:`);
actors.forEach(({ url, actor, error }) => {
if (error) {
console.log(` ${url}: ERROR - ${error}`);
} else {
console.log(` ${url}: ${actor[prop] || '(not set)'}`);
}
});
});
}
Endpoint Testing
Test Inbox
# Note: Requires valid HTTP signature for real servers
curl -X POST \
-H "Content-Type: application/activity+json" \
-d '{"type": "Create", "actor": "...", "object": "..."}' \
"https://example.com/users/alice/inbox"
Test Outbox (Read)
# Fetch recent activities
curl -H "Accept: application/activity+json" \
"https://mastodon.social/users/Gargron/outbox" | jq .
Test Collections
# Followers (may be empty or restricted)
curl -H "Accept: application/activity+json" \
"https://mastodon.social/users/Gargron/followers" | jq .
# Following
curl -H "Accept: application/activity+json" \
"https://mastodon.social/users/Gargron/following" | jq .
Common Issues
406 Not Acceptable
Server doesn't recognize Accept header:
# Try alternative
curl -H "Accept: application/ld+json" "https://example.com/users/alice"
401/403 Unauthorized
Some endpoints require authentication:
// Outbox might be public, but inbox always requires signature
// Followers/following may be private
id vs url
The id and the url are different things and usually differ:
{
"id": "https://example.com/users/alice",
"url": "https://example.com/@alice"
}
The id is for ActivityPub, url is the human-readable profile page.
Related Tools
- WebFinger Lookup - Discover actor URLs
- HTTP signatures in 2026 - Signing, verifying and debugging 401s
- JSON-LD Playground - Validate JSON-LD