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.
What it checks
| Area | Checks | Sources |
|---|---|---|
| Context | @context present and includes https://www.w3.org/ns/activitystreams | AS2 Core §2.1, Mastodon |
| Identity | id present, absolute and HTTPS; type present and an Activity Vocabulary type (or a known extension) | AP §3.1, Vocabulary |
| Actors | inbox 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 |
| Dates | published, updated, startTime, endTime, deleted are RFC 3339 with uppercase T and Z | AS2 Core §2.3 |
| Addressing | compacted 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 |
| Create | object 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
actoras URL - Has unique
id -
to/ccare URLs or arrays of URLs (AS2 allows either)
For Create
- Has
object - Object has
id - Object has
type - Object has
attributedTo -
attributedTomatchesactor
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
- Actor Inspector - Fetch and lint remote actors
- Signature Inspector - See the exact string a signature covers
- WebFinger Lookup - Test discovery
- Test Suites - Comprehensive testing