Skip to main content

Verified against ActivityPub REC §6–§7, Mastodon v4.7.3 source (block_service, move.rb, flag) and docs.joinmastodon.org/spec/activitypub, FEP-7628 on 2026-10-15.

Activities

Activities are the verbs of ActivityPub — they represent actions that actors perform. When a user posts, follows someone, or likes content, those actions are represented as Activity objects.

Activity Structure​

All activities share a common structure:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "https://example.com/activities/1",
"actor": "https://example.com/users/alice",
"object": "https://example.com/notes/1",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/users/alice/followers"],
"published": "2024-01-15T10:30:00Z"
}

Core Properties​

PropertyRequiredDescription
typeYesActivity type
idYes (between servers)Unique identifier (AP §3.1); in C2S the server assigns it (AP §6)
actorYesWho performed the activity
objectUsuallyTarget of the activity
toNoPrimary recipients
ccNoSecondary recipients
publishedNoWhen performed

Content Activities​

Create​

Creates new content:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "https://example.com/activities/create-1",
"actor": "https://example.com/users/alice",
"object": {
"type": "Note",
"id": "https://example.com/notes/1",
"attributedTo": "https://example.com/users/alice",
"content": "Hello, world!",
"to": ["https://www.w3.org/ns/activitystreams#Public"]
},
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/users/alice/followers"]
}
note

When receiving a Create, verify that activity.actor matches object.attributedTo and that the object's id is on the actor's origin — see Origin model.

Update​

Modifies existing content:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Update",
"id": "https://example.com/activities/update-1",
"actor": "https://example.com/users/alice",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": {
"type": "Note",
"id": "https://example.com/notes/1",
"attributedTo": "https://example.com/users/alice",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"content": "Hello, world! (edited)",
"published": "2024-01-15T10:00:00Z",
"updated": "2024-01-15T11:00:00Z"
}
}

Only the original author (or server) should be able to Update objects.

:::warning Server-to-server Update is a full replacement Between servers, an Update carries the complete new object, not a patch: receivers replace their copy wholesale (AP §7.3). Partial updates only exist in the client-to-server API (AP §6.3). :::

Delete​

Removes content:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Delete",
"id": "https://example.com/activities/delete-1",
"actor": "https://example.com/users/alice",
"object": "https://example.com/notes/1",
"to": ["https://www.w3.org/ns/activitystreams#Public"]
}

When receiving Delete:

  1. Verify the actor owns the object (same origin as the object's id, and the object's attributedTo)
  2. Remove your copy, or replace it with a Tombstone

The origin server (the one that hosts the object) then answers future fetches with 410 Gone if it serves a Tombstone, or 404 Not Found otherwise (AP §6.4). Receivers don't serve the object, so they have nothing to return.

Social Activities​

Follow​

Request to follow an actor:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Follow",
"id": "https://example.com/activities/follow-1",
"actor": "https://example.com/users/alice",
"object": "https://other.example/users/bob"
}

The receiving server SHOULD respond with an Accept or Reject, which MAY be generated automatically (AP §7.5).

Accept​

Accepts a Follow (or other request):

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Accept",
"id": "https://other.example/activities/accept-1",
"actor": "https://other.example/users/bob",
"object": {
"type": "Follow",
"id": "https://example.com/activities/follow-1",
"actor": "https://example.com/users/alice",
"object": "https://other.example/users/bob"
}
}

Reject​

Rejects a Follow request:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Reject",
"id": "https://other.example/activities/reject-1",
"actor": "https://other.example/users/bob",
"object": {
"type": "Follow",
"id": "https://example.com/activities/follow-1",
"actor": "https://example.com/users/alice",
"object": "https://other.example/users/bob"
}
}

Reaction Activities​

Like​

Express appreciation:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Like",
"id": "https://example.com/activities/like-1",
"actor": "https://example.com/users/alice",
"object": "https://other.example/notes/1"
}

Announce (Boost/Reblog)​

Share content with your followers:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Announce",
"id": "https://example.com/activities/announce-1",
"actor": "https://example.com/users/alice",
"object": "https://other.example/notes/1",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": [
"https://other.example/users/bob",
"https://example.com/users/alice/followers"
]
}

Negative Activities​

Block​

Block an actor:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Block",
"id": "https://example.com/activities/block-1",
"actor": "https://example.com/users/alice",
"object": "https://other.example/users/spammer"
}
note

The spec says servers SHOULD NOT deliver Block to its object (AP §6.9), but Mastodon does send it to the blocked user's server, which uses it to hide the blocker's posts and disallow mentions (Mastodon docs, block_service.rb). Expect to receive Blocks, and don't treat delivery as guaranteed enforcement.

Flag (Report)​

Report content or actors:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Flag",
"id": "https://example.com/activities/flag-1",
"actor": "https://example.com/actor",
"object": [
"https://other.example/users/abuser",
"https://other.example/notes/offensive"
],
"content": "This user is posting abusive content"
}

Send to the remote server's inbox (actor or shared inbox). Mastodon sends Flag from its instance actor rather than the reporting user, so the reporter stays anonymous to the remote server (Mastodon docs); do the same.

Undo​

Reverses a previous activity:

Undo Follow (Unfollow)​

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Undo",
"id": "https://example.com/activities/undo-1",
"actor": "https://example.com/users/alice",
"object": {
"type": "Follow",
"id": "https://example.com/activities/follow-1",
"actor": "https://example.com/users/alice",
"object": "https://other.example/users/bob"
}
}

Undo Like​

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Undo",
"id": "https://example.com/activities/undo-2",
"actor": "https://example.com/users/alice",
"object": {
"type": "Like",
"id": "https://example.com/activities/like-1",
"actor": "https://example.com/users/alice",
"object": "https://other.example/notes/1"
}
}

Undo Announce​

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Undo",
"id": "https://example.com/activities/undo-3",
"actor": "https://example.com/users/alice",
"object": {
"type": "Announce",
"id": "https://example.com/activities/announce-1"
}
}

Collection Activities​

Add​

Add an object to a collection:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Add",
"id": "https://example.com/activities/add-1",
"actor": "https://example.com/users/alice",
"object": "https://example.com/notes/1",
"target": "https://example.com/users/alice/collections/featured"
}

Mastodon uses this to pin posts to the actor's featured collection. When receiving Add/Remove, only accept it if the target collection belongs to the actor (AP §7.8).

Remove​

Remove an object from a collection:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Remove",
"id": "https://example.com/activities/remove-1",
"actor": "https://example.com/users/alice",
"object": "https://example.com/notes/1",
"target": "https://example.com/users/alice/collections/featured"
}

Move​

For account migration. Before sending, the new account must list the old one in its alsoKnownAs; receivers (including Mastodon) reject the Move otherwise (move.rb). FEP-7628 (FINAL) describes the full flow:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Move",
"id": "https://old.example/activities/move-1",
"actor": "https://old.example/users/alice",
"object": "https://old.example/users/alice",
"target": "https://new.example/users/alice"
}

Activity Processing​

Handling Incoming Activities​

Only dispatch an activity after the request has been authenticated: the HTTP signature verified over the raw body, and the signing key's owner confirmed to be activity.actor. That step is covered, with tested code, in Verifying incoming requests; the dispatcher below assumes it has already happened.

// `activity` has already passed verifyRequest() + parseActivityFrom()
// (see /docs/security/verifying-requests), so activity.actor is the signer.
async function handleActivity(activity) {
switch (activity.type) {
case 'Create':
return handleCreate(activity);
case 'Update':
return handleUpdate(activity);
case 'Delete':
return handleDelete(activity);
case 'Follow':
return handleFollow(activity);
case 'Accept':
return handleAccept(activity);
case 'Reject':
return handleReject(activity);
case 'Like':
return handleLike(activity);
case 'Announce':
return handleAnnounce(activity);
case 'Undo':
return handleUndo(activity);
default:
console.log('Unknown activity type:', activity.type);
return { status: 202 };
}
}

Side Effects​

Activities trigger side effects:

ActivitySide Effects
CreateStore object, notify mentioned users
UpdateUpdate stored object, notify
DeleteRemove object, cascade to local copies
FollowAdd pending follow or auto-accept
AcceptAdd to following, start delivery
LikeIncrement counter, notify author
AnnounceAdd boost, notify author
UndoReverse the original activity's effects

Idempotency​

Processing the same activity twice must have the same result as processing it once: servers retry deliveries, and the same activity can arrive through several inboxes or relays. Deduplicate on the activity id, and for activities that should exist at most once per pair — Like, Announce, Follow — also on (actor, object). For a Like, one row per (actor, object) covers both:

// `activity` has passed verification, so activity.actor is the signer
async function handleLike(activity) {
const objectId = typeof activity.object === 'string' ? activity.object : activity.object?.id;
if (!objectId) return;

// Unique index on (actor, object) makes this safe under concurrency too
await db.likes.upsert(
{ actor: activity.actor, object: objectId },
{ activityId: activity.id, createdAt: new Date() }
);
}

The inbox still answers 202 Accepted (or 200) either way; a duplicate is not an error.

Next Steps​