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
| Property | Required | Description |
|---|---|---|
type | Yes | Activity type |
id | Yes (between servers) | Unique identifier (AP §3.1); in C2S the server assigns it (AP §6) |
actor | Yes | Who performed the activity |
object | Usually | Target of the activity |
to | No | Primary recipients |
cc | No | Secondary recipients |
published | No | When 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"]
}
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:
- Verify the actor owns the object (same origin as the object's
id, and the object'sattributedTo) - 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"
}
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:
| Activity | Side Effects |
|---|---|
| Create | Store object, notify mentioned users |
| Update | Update stored object, notify |
| Delete | Remove object, cascade to local copies |
| Follow | Add pending follow or auto-accept |
| Accept | Add to following, start delivery |
| Like | Increment counter, notify author |
| Announce | Add boost, notify author |
| Undo | Reverse 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
- Collections - Organizing activities
- Delivery - Sending activities
- Handling Activities - Implementation