Skip to main content

Verified against AS2 Core and Vocabulary, ActivityPub REC, Mastodon v4.7.3 source (fetch_remote_actor_service.rb, process_account_service.rb, activity.rb, create.rb, json_ld_helper.rb, keypair.rb), ajv 8.20 / ajv-formats 3.0 on 2026-10-15.

Activity Linter

Paste an actor, activity or object to check it against the Activity Streams 2.0 and ActivityPub specs and against the rules Mastodon applies when it receives one. Every finding links the spec section or source line it comes from.

Runs entirely in your browser. Nothing you paste is sent anywhere.

What it checks​

AreaChecksSources
Context@context present and includes https://www.w3.org/ns/activitystreamsAS2 Core §2.1, Mastodon
Identityid present, absolute and HTTPS; type present and an Activity Vocabulary type (or a known extension)AP §3.1, Vocabulary
Actorsinbox and outbox (MUST), followers/following (SHOULD), preferredUsername or webfinger, a publicKey whose owner is the actor, with an SPKI PEM (PKCS#1 gets a warning: Mastodon accepts it, WebCrypto can't import it)AP §4.1, Mastodon actor fetch and account processing
Datespublished, updated, startTime, endTime, deleted are RFC 3339 with uppercase T and ZAS2 Core §2.3
Addressingcompacted Public forms, bto/bcc left in, direct messages without Mention tags (Mastodon treats them as "limited" and sends no notification)AP §5.6, Mastodon docs
Createobject of a type Mastodon turns into a status, on the actor's host, attributed to the actor; a bare object URI gets a hint (Mastodon fetches it, at the cost of an extra request)Mastodon create.rb, activity.rb, dereference_object!

It does not check signatures (see the Signature Inspector), fetch anything, or expand JSON-LD; terms are matched by their compacted names, the way most implementations read them. The rules live in src/components/tools/lib and their tests in tests/tools, in the site repository.

To lint a live actor by handle, use the Actor Inspector. Other tools that fetch and parse objects the way a server does: BrowserPub and fedify lookup from the Fedify CLI.

Validating in code​

Basic JSON Schema Validation​

const Ajv = require('ajv');
const addFormats = require('ajv-formats');

const ajv = new Ajv();
addFormats(ajv); // without this, Ajv 8 ignores "format": "uri"

const activitySchema = {
type: 'object',
required: ['@context', 'type', 'actor'],
properties: {
'@context': {
oneOf: [
{ type: 'string' },
{ type: 'array' }
]
},
type: { type: 'string' },
id: { type: 'string', format: 'uri' },
actor: {
oneOf: [
{ type: 'string', format: 'uri' },
{ type: 'object' }
]
},
object: {},
// AS2 allows a single value or an array
to: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
cc: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] }
}
};

const validate = ajv.compile(activitySchema); // compile once

function validateActivity(activity) {
const valid = validate(activity);

if (!valid) {
console.log('Validation errors:', validate.errors);
}

return valid;
}

Type-Specific Validation​

function validateByType(activity) {
const errors = [];

// Common checks
if (!activity['@context']) {
errors.push('Missing @context');
}

if (!activity.type) {
errors.push('Missing type');
}

if (!activity.actor) {
errors.push('Missing actor');
}

// Type-specific checks
switch (activity.type) {
case 'Create':
if (!activity.object) {
errors.push('Create requires object');
}
if (activity.object && activity.object.attributedTo !== activity.actor) {
errors.push('Create object.attributedTo should match actor');
}
break;

case 'Follow':
if (!activity.object) {
errors.push('Follow requires object (target actor)');
}
break;

case 'Like':
case 'Announce':
if (!activity.object) {
errors.push(`${activity.type} requires object`);
}
break;

case 'Delete':
if (!activity.object) {
errors.push('Delete requires object');
}
break;

case 'Undo':
if (!activity.object) {
errors.push('Undo requires object (activity to undo)');
}
break;
}

return {
valid: errors.length === 0,
errors
};
}

Common Validation Errors​

Missing @context​

// ❌ Wrong
{
"type": "Note",
"content": "Hello"
}

// ✅ Correct
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"content": "Hello"
}

Invalid Actor URL​

// ❌ Wrong - not a URL
{
"actor": "alice"
}

// ✅ Correct
{
"actor": "https://example.com/users/alice"
}

Missing Required Properties​

// ❌ Wrong - Create without object
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"actor": "https://example.com/users/alice"
}

// ✅ Correct
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"actor": "https://example.com/users/alice",
"object": {
"type": "Note",
"content": "Hello!"
}
}

Attribution Mismatch​

// ❌ Wrong - actor doesn't match attributedTo
{
"type": "Create",
"actor": "https://example.com/users/alice",
"object": {
"type": "Note",
"attributedTo": "https://example.com/users/bob",
"content": "Hello"
}
}

Validation Checklist​

For All Activities​

  • Has @context
  • Has valid type
  • Has actor as URL
  • Has unique id
  • to/cc are URLs or arrays of URLs (AS2 allows either)

For Create​

  • Has object
  • Object has id
  • Object has type
  • Object has attributedTo
  • attributedTo matches actor

For Follow​

  • Has object (target actor URL)
  • Object is a valid actor URL

For Undo​

  • Has object (activity being undone)
  • Actor matches original activity actor

Testing Tools​

curl​

# Against your own development server only: real servers (Mastodon included)
# reject unsigned POSTs to an inbox
curl -X POST https://example.com/inbox \
-H "Content-Type: application/activity+json" \
-d '{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"actor": "https://example.com/users/alice",
"object": {
"type": "Note",
"content": "Test"
}
}'

See Also​