Baudrate exposes an ActivityPub-compatible API for federation and programmatic
access. All endpoints live under /ap/ (objects and actors) or
/.well-known/ (discovery). No separate REST API exists — the AP endpoints
are the public API.
Base URL: https://<your-instance>
- Global Behavior
- Content Negotiation
- Discovery
- Actors
- Objects
- Collections
- Inboxes
- Error Responses
- Rate Limits
- Baudrate Extensions
| Aspect | Detail |
|---|---|
| Federation kill switch | Setting ap_federation_enabled (admin panel). When disabled, all /ap/* endpoints return 404. Discovery endpoints remain available. |
| CORS | Access-Control-Allow-Origin: * on all GET /ap/* responses. OPTIONS returns 204 with allowed methods GET, HEAD, OPTIONS. |
| Vary | Content-negotiated endpoints include Vary: Accept for proper cache behavior. |
| Authorized fetch | Optional setting ap_authorized_fetch. When enabled, unsigned GET requests to /ap/* return 401. Discovery endpoints are exempt. |
| Domain filtering | blocklist mode (default): reject domains in ap_domain_blocklist. allowlist mode: only accept domains in ap_domain_allowlist (empty list blocks all). |
| Payload size | Inbox POST bodies capped at 256 KB (413 Payload Too Large). Content bodies capped at 64 KB. |
| JSON-LD contexts | https://www.w3.org/ns/activitystreams and https://w3id.org/security/v1 |
Endpoints that represent both a web page and an AP object (actors, articles)
perform content negotiation on the Accept header:
| Accept header | Response |
|---|---|
application/activity+json |
JSON-LD (AP object) |
application/ld+json |
JSON-LD (AP object) |
application/json |
JSON-LD (AP object) |
text/html or other |
302 redirect to web UI |
Machine-only endpoints (collections, inboxes, discovery) always return JSON.
Resolve local actors by acct: URI per RFC 7033.
GET /.well-known/webfinger?resource=acct:alice@example.com
Content-Type: application/jrd+json
Auth: None (exempt from authorized fetch)
Rate limit: 120 req/min per IP
Query parameters:
| Parameter | Required | Description |
|---|---|---|
resource |
Yes | acct:username@host for users, acct:slug@host for boards (also accepts acct:!slug@host with Lemmy-compatible ! prefix) |
Example response:
{
"subject": "acct:alice@example.com",
"aliases": ["https://example.com/ap/users/alice"],
"links": [
{
"rel": "self",
"type": "application/activity+json",
"href": "https://example.com/ap/users/alice"
},
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": "https://example.com/@alice"
}
]
}Errors:
| Status | Condition |
|---|---|
| 400 | Missing resource parameter or invalid format |
| 404 | User/board not found, or board is private (min_role_to_view != "guest") |
Board WebFinger example:
GET /.well-known/webfinger?resource=acct:general@example.com
{
"subject": "acct:general@example.com",
"aliases": ["https://example.com/ap/boards/general"],
"links": [
{
"rel": "self",
"type": "application/activity+json",
"href": "https://example.com/ap/boards/general"
}
],
"properties": {
"https://www.w3.org/ns/activitystreams#type": "Group"
}
}Notes:
- Only federated boards (
min_role_to_view == "guest"andap_enabled == true) are discoverable - Board WebFinger uses the bare slug in
subject(matchingpreferredUsername) for Mastodon compatibility - The
propertiesfield withtype: "Group"follows the Lemmy convention for actor type disambiguation - Queries with
!prefix (acct:!general@example.com) are accepted for Lemmy backward compatibility
Instance metadata per NodeInfo 2.1.
GET /.well-known/nodeinfo
Content-Type: application/json
Auth: None (exempt from authorized fetch)
Returns a links array pointing to the full NodeInfo document:
{
"links": [
{
"rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
"href": "https://example.com/nodeinfo/2.1"
}
]
}GET /nodeinfo/2.1
Content-Type: application/json
Auth: None (exempt from authorized fetch)
{
"version": "2.1",
"software": {
"name": "baudrate",
"version": "0.1.0",
"repository": "https://github.com/hiroshiyui/baudrate"
},
"protocols": ["activitypub"],
"openRegistrations": true,
"usage": {
"users": { "total": 42 },
"localPosts": 128
},
"metadata": {
"nodeName": "My Forum"
}
}GET /ap/users/:username
Content-Type: application/activity+json (content-negotiated)
Auth: HTTP Signature required if authorized fetch is enabled
Rate limit: 120 req/min per IP
Path validation: Username matches [a-zA-Z0-9_]+
Example response:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"id": "https://example.com/ap/users/alice",
"type": "Person",
"preferredUsername": "alice",
"summary": "User's signature text",
"inbox": "https://example.com/ap/users/alice/inbox",
"outbox": "https://example.com/ap/users/alice/outbox",
"followers": "https://example.com/ap/users/alice/followers",
"url": "https://example.com/@alice",
"published": "2026-01-15T10:30:00Z",
"icon": {
"type": "Image",
"mediaType": "image/webp",
"url": "https://example.com/uploads/avatars/abc123/48.webp"
},
"endpoints": {
"sharedInbox": "https://example.com/ap/inbox"
},
"publicKey": {
"id": "https://example.com/ap/users/alice#main-key",
"owner": "https://example.com/ap/users/alice",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
}
}Fields:
| Field | Type | Description |
|---|---|---|
type |
string | Always "Person" |
preferredUsername |
string | The username |
summary |
string | User's signature/bio (optional) |
published |
ISO 8601 | Account creation timestamp |
icon |
Image | Avatar as WebP (optional, present if user has avatar) |
publicKey |
object | RSA-SHA256 public key for HTTP Signature verification |
endpoints.sharedInbox |
URI | Shared inbox URL |
Errors:
| Status | Condition |
|---|---|
| 401 | Authorized fetch enabled and no valid HTTP Signature |
| 404 | User not found |
GET /ap/boards/:slug
Content-Type: application/activity+json (content-negotiated)
Auth: HTTP Signature required if authorized fetch is enabled
Rate limit: 120 req/min per IP
Path validation: Slug matches [a-z0-9]+(?:-[a-z0-9]+)*
Access control: Returns 404 if min_role_to_view != "guest" or ap_enabled != true.
Example response:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"id": "https://example.com/ap/boards/general",
"type": "Group",
"preferredUsername": "general",
"name": "General Discussion",
"summary": "A board for general topics",
"inbox": "https://example.com/ap/boards/general/inbox",
"outbox": "https://example.com/ap/boards/general/outbox",
"followers": "https://example.com/ap/boards/general/followers",
"url": "https://example.com/boards/general",
"baudrate:parentBoard": "https://example.com/ap/boards/community",
"baudrate:subBoards": [
"https://example.com/ap/boards/general-offtopic"
],
"endpoints": {
"sharedInbox": "https://example.com/ap/inbox"
},
"publicKey": {
"id": "https://example.com/ap/boards/general#main-key",
"owner": "https://example.com/ap/boards/general",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
}
}Fields:
| Field | Type | Description |
|---|---|---|
type |
string | Always "Group" |
preferredUsername |
string | Board slug |
name |
string | Board display name |
summary |
string | Board description |
baudrate:parentBoard |
URI | Parent board actor URI (optional, see Baudrate Extensions) |
baudrate:subBoards |
array of URIs | Sub-board actor URIs (optional, only public AP-enabled children) |
Errors:
| Status | Condition |
|---|---|
| 401 | Authorized fetch enabled and no valid HTTP Signature |
| 404 | Board not found, private, or AP disabled |
GET /ap/site
Content-Type: application/activity+json (content-negotiated)
Auth: HTTP Signature required if authorized fetch is enabled
Rate limit: 120 req/min per IP
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"id": "https://example.com/ap/site",
"type": "Organization",
"preferredUsername": "site",
"name": "My Forum",
"inbox": "https://example.com/ap/site/inbox",
"outbox": "https://example.com/ap/site/outbox",
"followers": "https://example.com/ap/site/followers",
"url": "https://example.com",
"endpoints": {
"sharedInbox": "https://example.com/ap/inbox"
},
"publicKey": {
"id": "https://example.com/ap/site#main-key",
"owner": "https://example.com/ap/site",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
}
}GET /ap/articles/:slug
Content-Type: application/activity+json (content-negotiated)
Auth: HTTP Signature required if authorized fetch is enabled
Rate limit: 120 req/min per IP
Path validation: Slug matches [a-z0-9]+(?:-[a-z0-9]+)*
Access control: Returns 404 if the article only belongs to private boards.
Example response:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/ap/articles/hello-world-a1b2c3",
"type": "Article",
"name": "Hello World",
"summary": "This is a plain-text preview of the article body...",
"content": "<p>This is the <strong>rendered HTML</strong> content.</p>",
"mediaType": "text/html",
"source": {
"content": "This is the **rendered HTML** content.",
"mediaType": "text/markdown"
},
"attributedTo": "https://example.com/ap/users/alice",
"published": "2026-02-20T08:00:00Z",
"updated": "2026-02-21T12:30:00Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://example.com/ap/boards/general"],
"audience": ["https://example.com/ap/boards/general"],
"url": "https://example.com/articles/hello-world-a1b2c3",
"replies": "https://example.com/ap/articles/hello-world-a1b2c3/replies",
"baudrate:pinned": false,
"baudrate:locked": false,
"baudrate:commentCount": 5,
"baudrate:likeCount": 12,
"tag": [
{
"type": "Hashtag",
"name": "#elixir",
"href": "https://example.com/tags/elixir"
}
]
}Fields:
| Field | Type | Description |
|---|---|---|
type |
string | Always "Article" |
name |
string | Article title |
summary |
string | Plain-text preview (max 500 chars, markdown stripped) |
content |
string | HTML rendered from Markdown body |
mediaType |
string | Always "text/html" |
source |
object | Original Markdown body with mediaType: "text/markdown" |
attributedTo |
URI | Author's Person actor URI |
published |
ISO 8601 | Creation timestamp |
updated |
ISO 8601 | Last modification timestamp |
to |
array | Always ["https://www.w3.org/ns/activitystreams#Public"] |
cc |
array of URIs | Board actor URIs the article belongs to |
audience |
array of URIs | Same as cc |
url |
URI | Web UI URL for the article |
replies |
URI | Replies collection endpoint |
tag |
array | Hashtag objects extracted from body (optional, omitted if empty) |
baudrate:pinned |
boolean | Whether the article is pinned in its board |
baudrate:locked |
boolean | Whether the article is locked from new comments |
baudrate:commentCount |
integer | Number of comments |
baudrate:likeCount |
integer | Number of likes |
Hashtag extraction:
- Pattern:
#[a-zA-Z][a-zA-Z0-9_]*(1-64 chars after#) - Code blocks and inline code are excluded
- Case-insensitive deduplication, case-preserving output
- Links to
/tags/:hashtag(lowercase)
Errors:
| Status | Condition |
|---|---|
| 401 | Authorized fetch enabled and no valid HTTP Signature |
| 404 | Article not found or only in private boards |
All paginated collections follow the same scheme:
- Without
?page: Returns anOrderedCollectionroot withtotalItemsandfirstlink - With
?page=N: Returns anOrderedCollectionPagewith up to 20 items - Page numbering: 1-based (
?page=1is the first page) - Navigation: Pages include
prev/nextlinks where applicable
Root collection example:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/ap/users/alice/outbox",
"type": "OrderedCollection",
"totalItems": 42,
"first": "https://example.com/ap/users/alice/outbox?page=1"
}Collection page example:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/ap/users/alice/outbox?page=2",
"type": "OrderedCollectionPage",
"partOf": "https://example.com/ap/users/alice/outbox",
"prev": "https://example.com/ap/users/alice/outbox?page=1",
"next": "https://example.com/ap/users/alice/outbox?page=3",
"orderedItems": [ ... ]
}GET /ap/users/:username/outbox
GET /ap/users/:username/outbox?page=1
Auth: HTTP Signature required if authorized fetch is enabled Rate limit: 120 req/min per IP
Returns Create activities wrapping Article objects. Only includes articles
in public boards.
Item structure:
{
"type": "Create",
"actor": "https://example.com/ap/users/alice",
"published": "2026-02-20T08:00:00Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": { ... }
}GET /ap/boards/:slug/outbox
GET /ap/boards/:slug/outbox?page=1
Auth: HTTP Signature required if authorized fetch is enabled Rate limit: 120 req/min per IP Access control: Returns 404 if board is private or AP disabled.
Returns Announce activities for articles posted to the board.
Item structure:
{
"type": "Announce",
"actor": "https://example.com/ap/boards/general",
"published": "2026-02-20T08:00:00Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": "https://example.com/ap/articles/hello-world-a1b2c3"
}GET /ap/users/:username/followers
GET /ap/users/:username/followers?page=1
Auth: HTTP Signature required if authorized fetch is enabled Rate limit: 120 req/min per IP
Items are remote actor URIs (strings).
GET /ap/boards/:slug/followers
GET /ap/boards/:slug/followers?page=1
Auth: HTTP Signature required if authorized fetch is enabled Rate limit: 120 req/min per IP Access control: Returns 404 if board is private or AP disabled.
Items are remote actor URIs (strings).
GET /ap/users/:username/following
GET /ap/users/:username/following?page=1
Auth: HTTP Signature required if authorized fetch is enabled Rate limit: 120 req/min per IP
Paginated OrderedCollection of actor URIs the user follows (accepted follows only).
Items are remote actor URIs (strings) and local user actor URIs.
GET /ap/boards/:slug/following
Auth: HTTP Signature required if authorized fetch is enabled Rate limit: 120 req/min per IP Access control: Returns 404 if board is private or AP disabled.
Returns an empty OrderedCollection (boards do not follow other actors).
GET /ap/boards
Auth: HTTP Signature required if authorized fetch is enabled Rate limit: 120 req/min per IP
Returns an OrderedCollection of all public, AP-enabled boards. Not
paginated — all boards are returned in a single response.
Item structure:
{
"id": "https://example.com/ap/boards/general",
"type": "Group",
"name": "General Discussion",
"summary": "A board for general topics",
"url": "https://example.com/boards/general"
}GET /ap/articles/:slug/replies
Auth: HTTP Signature required if authorized fetch is enabled Rate limit: 120 req/min per IP Access control: Returns 404 if article is only in private boards.
Returns an OrderedCollection of comments as Note objects. Not paginated
— all comments are returned in a single response.
Item structure:
{
"type": "Note",
"id": "https://remote.example/comments/abc123",
"content": "<p>Great article!</p>",
"attributedTo": "https://example.com/ap/users/bob",
"inReplyTo": "https://example.com/ap/articles/hello-world-a1b2c3",
"published": "2026-02-20T09:15:00Z"
}GET /ap/search?q=elixir
GET /ap/search?q=elixir&page=1
Auth: HTTP Signature required if authorized fetch is enabled Rate limit: 120 req/min per IP
Full-text search across articles in public boards. Returns a paginated
OrderedCollection of Article objects.
Query parameters:
| Parameter | Required | Description |
|---|---|---|
q |
Yes | Search query (minimum 1 byte) |
page |
No | Page number (1-based) |
Errors:
| Status | Condition |
|---|---|
| 400 | Missing q parameter |
All inbox endpoints accept POST requests with ActivityPub activities.
POST /ap/inbox
Accepts activities targeting any local actor.
POST /ap/users/:username/inbox
Accepts activities targeting a specific user. Returns 404 if user not found.
POST /ap/boards/:slug/inbox
Accepts activities targeting a specific board. Returns 404 if board is private or AP disabled.
All inbox POST requests require a valid HTTP Signature per draft-cavage-http-signatures.
Required Content-Type (one of):
application/activity+jsonapplication/ld+jsonapplication/json
Charset parameters are accepted (e.g., application/json; charset=utf-8).
Other content types return 415 Unsupported Media Type.
Signature header format:
Signature: keyId="https://remote.example/users/bob#main-key",
algorithm="rsa-sha256",
headers="(request-target) host date digest",
signature="<base64>"
Required signed headers:
| Header | Description |
|---|---|
(request-target) |
Pseudo-header: post /ap/inbox |
host |
Request host |
date |
RFC 7231 HTTP date (validated within +/-30 seconds) |
digest |
SHA-256=<base64> of request body |
Algorithm: rsa-sha256 (also accepts hs2019)
Key resolution: The keyId value is used to resolve the remote actor.
The actor's publicKey.publicKeyPem field provides the RSA public key for
verification.
| Activity | Object type | Effect |
|---|---|---|
Follow |
actor URI | Creates follower relationship; auto-accepted with Accept(Follow) |
Undo |
Follow |
Removes follower relationship |
Create |
Article or Page |
Creates remote article in target board |
Create |
Note (public) |
Creates comment on local article (resolved via inReplyTo) |
Create |
Note (DM) |
Creates direct message (see DM Detection) |
Like |
article or comment URI | Records a like on the article or comment |
Undo |
Like |
Removes the like (from both article and comment likes) |
Announce |
article or comment URI, or embedded object | Records a boost/share (creates article/comment boost for local content) |
Undo |
Announce |
Removes the boost (from announces and article/comment boosts) |
Update |
Article, Page, or Note |
Updates remote content (authorship verified) |
Update |
Person or Group |
Refreshes cached remote actor profile |
Delete |
content URI or Tombstone |
Soft-deletes matching article, comment, or DM (authorship verified) |
Delete |
actor URI | Removes all follower relationships for the deleted actor |
Flag |
array of URIs | Creates a moderation report |
Block |
actor URI | Logged for informational purposes |
Undo |
Block |
Logged for informational purposes |
Unrecognized activity types are logged and ignored (no error returned).
Idempotency: Duplicate activities (same ap_id) are silently accepted
without error.
An incoming Create(Note) is treated as a direct message when all of the
following conditions are met:
https://www.w3.org/ns/activitystreams#Publicis NOT intoorcc- No
/followerscollection URIs appear intoorcc - At least one local user actor URI appears in
to
DMs are routed to Messaging.receive_remote_dm/3 instead of being stored as
comments.
All AP endpoints return errors as JSON:
{
"error": "Not Found"
}| Status | Meaning |
|---|---|
| 400 | Bad request (missing required parameter, invalid JSON) |
| 401 | Unauthorized (invalid HTTP Signature, or authorized fetch enabled without signature) |
| 404 | Not found (resource doesn't exist, private, or federation disabled) |
| 413 | Payload too large (inbox POST body exceeds 256 KB) |
| 415 | Unsupported media type (inbox POST with non-AP content type) |
| 422 | Unprocessable entity (activity validation or processing error) |
| 429 | Too many requests (rate limit exceeded) |
| Scope | Limit | Window |
|---|---|---|
| All AP endpoints | 120 requests | 1 minute per IP |
| Inbox POST | 60 requests | 1 minute per remote domain |
Rate-limited responses return 429 Too Many Requests with:
{
"error": "Too Many Requests"
}Failure mode: Rate limiting fails open — if the rate-limit backend (ETS) encounters an error, the request is allowed through.
Baudrate extends standard ActivityPub objects with custom properties under the
baudrate: namespace prefix.
| Property | Type | Description |
|---|---|---|
baudrate:pinned |
boolean | Article is pinned to the top of its board |
baudrate:locked |
boolean | Article is locked (new comments disabled) |
baudrate:commentCount |
integer | Total number of comments |
baudrate:likeCount |
integer | Total number of likes |
| Property | Type | Description |
|---|---|---|
baudrate:parentBoard |
URI | Parent board's actor URI (for hierarchical boards) |
baudrate:subBoards |
array of URIs | Child board actor URIs (only public, AP-enabled children included) |
Baudrate handles several compatibility concerns with popular Fediverse software:
attributedToarrays — Extracts the first binary URI (Mastodon may send arrays)- Content warnings —
sensitive: true+summaryfields are prepended as[CW: summary]to the body - Lemmy
Pageobjects — Treated identically toArticleforCreateandUpdate - Lemmy
Announcewith embedded objects — Extracts the inneridfield (not just bare URIs) - Board WebFinger — Uses bare slug in
subject(matchingpreferredUsername) for Mastodon compatibility; includespropertieswithtype: "Group"for Lemmy-compatible disambiguation; accepts!prefix in queries for backward compatibility - Mastodon HTML classes —
<span>tags with safe classes (h-card,hashtag,mention,invisible) are preserved through the HTML sanitizer - Cross-post deduplication — The same remote article arriving via multiple board inboxes is linked to all boards (not duplicated)
All /ap/* endpoints respond to OPTIONS with:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, OPTIONS
Access-Control-Allow-Headers: accept, content-type