Skip to main content

Client-to-Server (C2S)

The Client-to-Server protocol defines how client applications interact with ActivityPub servers. While most Fediverse software uses custom APIs, understanding C2S helps you build spec-compliant implementations.

Overview

┌─────────────────┐                    ┌─────────────────┐
│ │ POST /outbox │ │
│ Client │───────────────────►│ Server │
│ (Mobile App) │ │ (Your Server) │
│ │◄───────────────────│ │
│ │ GET /inbox │ │
└─────────────────┘ └─────────────────┘

Authentication

C2S requires authentication. The spec suggests OAuth 2.0:

POST /users/alice/outbox HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/activity+json

Posting Activities

Submit to Outbox

Clients POST activities to the actor's outbox:

POST /users/alice/outbox HTTP/1.1
Host: example.com
Authorization: Bearer <token>
Content-Type: application/activity+json

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"object": {
"type": "Note",
"content": "Hello, world!"
}
}

Server Processing

The server:

  1. Validates the activity
  2. Assigns an id to the activity
  3. Assigns an id to the object (if new)
  4. Sets the actor property
  5. Adds to the outbox collection
  6. Delivers to recipients (S2S)

Response

HTTP/1.1 201 Created
Location: https://example.com/activities/123

Activity Handling

Create

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"object": {
"type": "Note",
"content": "My new post",
"to": ["https://www.w3.org/ns/activitystreams#Public"]
}
}

Server adds:

  • id to activity
  • id to object
  • actor property
  • published timestamp
  • attributedTo on object

Like

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Like",
"object": "https://other.example/notes/456"
}

Follow

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Follow",
"object": "https://other.example/users/bob"
}

Update

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Update",
"object": {
"id": "https://example.com/notes/123",
"type": "Note",
"content": "Updated content"
}
}

Delete

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Delete",
"object": "https://example.com/notes/123"
}

Undo

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Undo",
"object": "https://example.com/activities/like-456"
}

Reading Activities

Get Inbox

GET /users/alice/inbox HTTP/1.1
Host: example.com
Authorization: Bearer <token>
Accept: application/activity+json

Response:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"id": "https://example.com/users/alice/inbox",
"totalItems": 42,
"first": "https://example.com/users/alice/inbox?page=true"
}

Get Outbox

GET /users/alice/outbox HTTP/1.1
Host: example.com
Accept: application/activity+json

Direct Object Posting

For convenience, clients can POST objects directly:

POST /users/alice/outbox HTTP/1.1

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

Server wraps in a Create activity automatically.

Addressing

Public Post

{
"type": "Note",
"content": "Hello everyone!",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/users/alice/followers"]
}

Followers Only

{
"type": "Note",
"content": "Just for my followers",
"to": ["https://example.com/users/alice/followers"]
}

Direct Message

{
"type": "Note",
"content": "Private message",
"to": ["https://other.example/users/bob"]
}

Uploaded Media

Upload Endpoint

Some servers provide a media upload endpoint:

POST /api/v1/media HTTP/1.1
Host: example.com
Authorization: Bearer <token>
Content-Type: multipart/form-data

[binary image data]

Response:

{
"id": "123",
"type": "Document",
"url": "https://example.com/media/123.jpg",
"mediaType": "image/jpeg"
}

Attach to Post

{
"type": "Note",
"content": "Check out this photo!",
"attachment": [
{
"type": "Document",
"url": "https://example.com/media/123.jpg",
"mediaType": "image/jpeg"
}
]
}

Implementation Example

Express.js Outbox Handler

app.post('/users/:username/outbox',
authenticate,
async (req, res) => {
const username = req.params.username;
const actor = `https://example.com/users/${username}`;

// Verify authenticated user matches
if (req.user.actor !== actor) {
return res.status(403).json({ error: 'Forbidden' });
}

let activity = req.body;

// Wrap bare objects in Create
if (!isActivityType(activity.type)) {
activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Create',
object: activity
};
}

// Assign IDs and properties
activity.id = `https://example.com/activities/${uuid()}`;
activity.actor = actor;
activity.published = new Date().toISOString();

if (activity.object && typeof activity.object === 'object') {
if (!activity.object.id) {
activity.object.id = `https://example.com/objects/${uuid()}`;
}
activity.object.attributedTo = actor;
}

// Store activity
await db.activities.insert(activity);

// Deliver to recipients (async)
deliverActivity(activity);

res.status(201)
.location(activity.id)
.json(activity);
}
);

C2S vs Custom APIs

AspectC2SCustom API (Mastodon)
StandardizedYesNo
ToolingLimitedExtensive
FeaturesBasicPlatform-specific
AdoptionLowHigh

When to Use C2S

  • Building a generic ActivityPub client
  • Interoperability is priority
  • Following spec strictly

When to Use Custom API

  • Platform-specific features
  • Better tooling available
  • Faster development

Implementations Supporting C2S

PlatformC2S Support
PleromaFull
MastodonPartial (read-only)
PixelfedNo
LemmyNo

Next Steps