motd is API-first. Every feature is an endpoint. Build clients, bots, integrations — whatever you want.
https://motd.social/api
Local dev: http://localhost:3006/api
Every endpoint returns the same envelope:
Success:
{ "ok": true, "data": { ... } }
Error:
{ "ok": false, "error": "Something went wrong." }
HTTP status codes are standard: 200 success, 400 bad request, 401 unauthorized, 404 not found, 409 conflict.
Most endpoints require authentication. Get a token by registering or logging in, then send it as a Bearer token or let the browser handle it via cookies.
Header auth (for scripts and clients):
Authorization: Bearer <token>
Cookie auth (for the web client):
The login endpoints set an motd_session httpOnly cookie automatically.
Sessions expire after 7 days. Use /login-permanently in the web client to persist across tabs (stored in localStorage instead of sessionStorage — the token is the same).
Create a new account.
Auth: none
Body:
{ "username": "alice", "password": "hunter2pwd" }
Rules:
Response:
{
"ok": true,
"data": {
"token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"user": {
"username": "alice",
"display_name": "alice"
}
}
}
Errors:
400 — username or password missing, invalid format409 — username takenExample:
curl -X POST http://localhost:3006/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"hunter2pwd"}'
Log in to an existing account.
Auth: none
Body:
{ "username": "alice", "password": "hunter2pwd" }
Response:
{
"ok": true,
"data": {
"token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"user": {
"username": "alice",
"display_name": "Alice"
}
}
}
Errors:
400 — missing fields401 — invalid username or passwordExample:
curl -X POST http://localhost:3006/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"hunter2pwd"}'
Get the current authenticated user.
Auth: required
Response:
{
"ok": true,
"data": {
"user": {
"username": "alice",
"display_name": "Alice",
"bio": "building things",
"created_at": "2026-03-15T10:30:00.000Z"
}
}
}
Example:
curl http://localhost:3006/api/auth/me \
-H "Authorization: Bearer YOUR_TOKEN"
End the current session.
Auth: required
Response:
{ "ok": true, "data": { "message": "Logged out." } }
Example:
curl -X POST http://localhost:3006/api/auth/logout \
-H "Authorization: Bearer YOUR_TOKEN"
View a user's profile.
Auth: none
Response:
{
"ok": true,
"data": {
"username": "alice",
"display_name": "Alice",
"bio": "building things",
"created_at": "2026-03-15T10:30:00.000Z",
"posts": 42,
"avatar": [
"████ ████",
" ████████ ",
"████████████",
" ████████ ",
"██ ████ ██"
]
}
}
Errors:
404 — user not foundExample:
curl http://localhost:3006/api/profile/alice
Update your display name and/or bio.
Auth: required
Body:
{ "display_name": "Alice W.", "bio": "shipping code" }
Both fields are optional. Send only what you want to change.
Response:
{
"ok": true,
"data": {
"username": "alice",
"display_name": "Alice W.",
"bio": "shipping code",
"created_at": "2026-03-15T10:30:00.000Z"
}
}
Errors:
400 — nothing to updateExample:
curl -X PUT http://localhost:3006/api/profile \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"bio":"shipping code"}'
Search users by username or display name.
Auth: none
Params:
q — search query (required, min 1 character)Response:
{
"ok": true,
"data": {
"users": [
{ "username": "alice", "display_name": "Alice", "bio": "building things" },
{ "username": "alice2", "display_name": "Alice Too", "bio": null }
]
}
}
Example:
curl "http://localhost:3006/api/profile/search?q=alice"
Publish a post.
Auth: required
Body:
{ "content": "just shipped the wasm compiler [rust] [wasm]" }
Optional fields:
reply_to — post ID to reply toInline commands:
/attach <media_id> — attach previously uploaded media to the post. The /attach command is stripped from the displayed content.Rules:
[rust], [music], [coding]Response:
{
"ok": true,
"data": {
"id": "a3kf9x",
"content": "just shipped the wasm compiler [rust] [wasm]",
"reply_to": null,
"tags": ["rust", "wasm"],
"media_id": null,
"username": "alice",
"display_name": "Alice",
"created_at": "2026-03-22T14:00:00.000Z"
}
}
Errors:
400 — empty content, too long, media not found/not yours404 — reply_to post not foundExamples:
Simple post:
curl -X POST http://localhost:3006/api/posts \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content":"just shipped the wasm compiler [rust] [wasm]"}'
Reply to a post:
curl -X POST http://localhost:3006/api/posts \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content":"nice work!","reply_to":"a3kf9x"}'
Post with media attachment:
curl -X POST http://localhost:3006/api/posts \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content":"screenshot of the new UI /attach x7km2p [showcase]"}'
Get the chronological feed. Public — works without auth. Authenticated users get kill-filtered results; guests get unfiltered posts (still subject to system-level filter_score suppression).
Auth: optional
Params:
limit — max posts to return (default 50, max 100)offset — pagination offset (default 0)Response:
{
"ok": true,
"data": {
"posts": [
{
"id": "a3kf9x",
"content": "just shipped the wasm compiler [rust] [wasm]",
"reply_to": null,
"tags": ["rust", "wasm"],
"media_id": null,
"username": "alice",
"display_name": "Alice",
"created_at": "2026-03-22T14:00:00.000Z"
}
]
}
}
Examples:
# Guest (unfiltered)
curl "http://localhost:3006/api/posts/feed?limit=20"
# Authenticated (kill-filtered)
curl "http://localhost:3006/api/posts/feed?limit=20" \
-H "Authorization: Bearer YOUR_TOKEN"
Get a single post by ID.
Auth: none
Response:
{
"ok": true,
"data": {
"id": "a3kf9x",
"content": "just shipped the wasm compiler [rust] [wasm]",
"reply_to": null,
"tags": ["rust", "wasm"],
"media_id": null,
"username": "alice",
"display_name": "Alice",
"created_at": "2026-03-22T14:00:00.000Z"
}
}
Example:
curl http://localhost:3006/api/posts/a3kf9x
Get a post and all its replies.
Auth: none
Response:
{
"ok": true,
"data": {
"post": {
"id": "a3kf9x",
"content": "just shipped the wasm compiler [rust] [wasm]",
"reply_to": null,
"tags": ["rust", "wasm"],
"username": "alice",
"display_name": "Alice",
"created_at": "2026-03-22T14:00:00.000Z"
},
"replies": [
{
"id": "b7xm2k",
"content": "nice work!",
"reply_to": "a3kf9x",
"tags": [],
"username": "bob",
"display_name": "Bob",
"created_at": "2026-03-22T14:05:00.000Z"
}
]
}
}
Example:
curl http://localhost:3006/api/posts/a3kf9x/thread
Search posts by content. Searches active posts by default, or archived posts with the archive flag.
Auth: none
Params:
q — search query (required)archive — set to true to search archived posts insteadResponse (active):
{
"ok": true,
"data": {
"posts": [
{
"id": "a3kf9x",
"content": "just shipped the wasm compiler [rust] [wasm]",
"tags": ["rust", "wasm"],
"media_id": null,
"username": "alice",
"display_name": "Alice",
"created_at": "2026-03-22T14:00:00.000Z",
"archived": false
}
]
}
}
Response (archive):
{
"ok": true,
"data": {
"posts": [
{
"id": "b7xm2k",
"content": "old post about [rust]",
"tags": ["rust"],
"username": "alice",
"display_name": "Alice",
"created_at": "2025-12-01T08:00:00.000Z",
"archived": true
}
]
}
}
Examples:
# Search active posts
curl "http://localhost:3006/api/posts/search?q=rust"
# Search archived posts
curl "http://localhost:3006/api/posts/search?q=rust&archive=true"
The /kill command maps to the filter API. The word "kill" never appears in the database or API.
Add a filter. Hides a user or post from your feed.
Auth: required
Body:
{ "target_type": "user", "target_id": "bob" }
target_type — "user" or "post"target_id — username (for users) or post ID (for posts)Filtering a user also increments their hidden filter_score. Users filtered by enough people get suppressed from everyone's feed silently (threshold: ~5 active filters, with 7-day half-life decay).
Response:
{ "ok": true, "data": { "message": "Filtered." } }
Errors:
400 — missing fields, invalid type, can't filter yourself404 — user not foundExample:
curl -X POST http://localhost:3006/api/filter \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target_type":"user","target_id":"spammer"}'
Get your current filter list.
Auth: required
Response:
{
"ok": true,
"data": {
"filters": [
{ "type": "user", "id": "uuid-of-filtered-user" },
{ "type": "post", "id": "b7xm2k" }
]
}
}
Example:
curl http://localhost:3006/api/filter \
-H "Authorization: Bearer YOUR_TOKEN"
Supported formats: PNG (max 5MB), MP3 (max 10MB), MP4 (max 25MB).
PNG uploads are processed into multiple variants: original (downscaled to max 1MB), thumbnail (80px wide), large (800px wide), and ASCII art.
Upload a file.
Auth: required
Body: multipart/form-data with file field
Response:
{
"ok": true,
"data": {
"media_id": "x7km2p",
"type": "png"
}
}
Errors:
400 — no file, unsupported type, file too largeExample:
curl -X POST http://localhost:3006/api/media/upload \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@screenshot.png"
Serve the original file.
Auth: none
Response: raw file with appropriate Content-Type header (image/png, audio/mpeg, video/mp4)
Example:
curl http://localhost:3006/api/media/x7km2p -o image.png
Serve the thumbnail (PNG only, max 80px wide).
Auth: none
Example:
curl http://localhost:3006/api/media/x7km2p/thumb -o thumb.png
Serve the large version (PNG only, max 800px wide).
Auth: none
Example:
curl http://localhost:3006/api/media/x7km2p/large -o large.png
Get the ASCII art representation (PNG only).
Auth: none
Response:
{
"ok": true,
"data": {
"ascii": " @@@@ \n @ @ \n @ @ \n @@@@ \n"
}
}
Example:
curl http://localhost:3006/api/media/x7km2p/ascii
Upload a profile picture. PNG only, max 5MB. Processes into thumb, large, and ASCII art versions. Sets as your avatar immediately.
Auth: required
Body: multipart/form-data with file field
Response:
{ "ok": true, "data": { "avatar_id": "x7km2p" } }
Example:
curl -X POST http://localhost:3006/api/avatar \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@photo.png"
Get a user's avatar image (large version, 800px max).
Auth: none
Response: raw PNG with Content-Type: image/png
Example:
curl http://localhost:3006/api/avatar/alice -o avatar.png
Returns 404 if the user has no uploaded avatar.
Bookmarks are private. Nobody sees your bookmarks. No counts, no notifications. One concept for everything: users, tags, posts, docs.
Add a bookmark.
Auth: required
Body:
{ "target_type": "user", "target_id": "alice" }
target_type — "user", "tag", "post", or "doc"target_id — username, tag name, post ID, or doc nameBookmarking the same thing twice is a no-op, not an error.
Response:
{ "ok": true, "data": { "message": "Bookmarked." } }
Errors:
400 — missing fields, invalid typeExample:
# Bookmark a user
curl -X POST http://localhost:3006/api/bookmarks \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target_type":"user","target_id":"alice"}'
# Bookmark a tag
curl -X POST http://localhost:3006/api/bookmarks \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target_type":"tag","target_id":"rust"}'
# Bookmark a post
curl -X POST http://localhost:3006/api/bookmarks \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target_type":"post","target_id":"a3kf9x"}'
# Bookmark a doc
curl -X POST http://localhost:3006/api/bookmarks \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target_type":"doc","target_id":"api"}'
Remove a bookmark.
Auth: required
Body:
{ "target_type": "user", "target_id": "alice" }
Response:
{ "ok": true, "data": { "message": "Unbookmarked." } }
Example:
curl -X DELETE http://localhost:3006/api/bookmarks \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target_type":"user","target_id":"alice"}'
List your bookmarks. Optionally filter by type.
Auth: required
Params:
type — optional: user, tag, post, or docResponse:
{
"ok": true,
"data": {
"bookmarks": [
{ "type": "user", "id": "alice" },
{ "type": "tag", "id": "rust" },
{
"type": "post",
"id": "a3kf9x",
"preview": "just shipped the wasm compiler...",
"username": "alice"
},
{
"type": "post",
"id": "old123",
"preview": "this was archived...",
"archived": true
},
{
"type": "post",
"id": "gone99",
"expired": true
},
{ "type": "doc", "id": "api" }
]
}
}
Post bookmarks are enriched with a content preview (60 chars), the author's username, and status flags (archived, expired) when applicable.
Examples:
# All bookmarks
curl http://localhost:3006/api/bookmarks \
-H "Authorization: Bearer YOUR_TOKEN"
# Only bookmarked tags
curl "http://localhost:3006/api/bookmarks?type=tag" \
-H "Authorization: Bearer YOUR_TOKEN"
List all available documentation files.
Auth: none
Response:
{
"ok": true,
"data": {
"docs": [
{ "name": "about", "path": "docs/about.md" },
{ "name": "api", "path": "docs/api.md" },
{ "name": "commands", "path": "docs/commands.md" },
{ "name": "changelog", "path": "docs/changelog.md" }
]
}
}
Example:
curl http://localhost:3006/api/docs
Get the contents of a specific document. The .md extension is optional.
Auth: none
Response:
{
"ok": true,
"data": {
"name": "api",
"content": "# API Reference\n\n..."
}
}
Errors:
400 — path traversal attempt404 — document not foundExample:
curl http://localhost:3006/api/docs/api
Get the category tree with tags. Used by /tree -cat in the client.
Auth: none
Response:
{
"ok": true,
"data": {
"categories": [
{
"id": "coding",
"name": "coding",
"tags": ["rust", "python", "wasm", "js", "frontend", "backend"]
},
{
"id": "creative",
"name": "creative",
"tags": ["music", "art", "writing", "gamedev"]
},
{
"id": "meta",
"name": "meta",
"tags": ["motd", "bugs", "ideas", "feedback"]
},
{
"id": "general",
"name": "general",
"tags": ["offtopic", "introductions", "showcase"]
}
]
}
}
Example:
curl http://localhost:3006/api/categories
Get the full command list grouped by category. Used by /help and /menu in the client.
Auth: none
Response:
{
"ok": true,
"data": {
"categories": [
{
"name": "General",
"commands": [
{ "command": "/help", "description": "Show all available commands" },
{ "command": "/menu", "description": "Toggle the command menu" },
{ "command": "/clear", "description": "Clear the terminal" },
{ "command": "/read [path]", "description": "Read a document" },
{ "command": "/tree -cat", "description": "Browse categories and tags" },
{ "command": "/link [target]", "description": "Navigate to user, tag, post, or doc" }
]
}
]
}
}
Example:
curl http://localhost:3006/api/commands
Generate a Telegram deep link to connect your account.
Auth: required
Response:
{
"ok": true,
"data": {
"link": "https://t.me/motdsocial_bot?start=alice_a8f3k2",
"expires_in": 600
}
}
If already connected:
{
"ok": true,
"data": {
"already_connected": true,
"message": "Telegram already connected."
}
}
Example:
curl -X POST http://localhost:3006/api/connect/telegram \
-H "Authorization: Bearer YOUR_TOKEN"
Disconnect your Telegram account.
Auth: required
Response:
{ "ok": true, "data": { "message": "Telegram disconnected." } }
Example:
curl -X DELETE http://localhost:3006/api/connect/telegram \
-H "Authorization: Bearer YOUR_TOKEN"
Send a verification email to connect your email address.
Auth: required
Body:
{ "email": "you@gmail.com" }
Response:
{
"ok": true,
"data": {
"message": "Verification email sent to you@gmail.com",
"tip": "Tip: add noreply@motd.social to your contacts so notifications don't go to spam."
}
}
Example:
curl -X POST http://localhost:3006/api/connect/email \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"you@gmail.com"}'
Verify email address from link in verification email.
Auth: none (link from email)
Params:
code — verification code from the email linkReturns an HTML confirmation page on success.
Disconnect your email address.
Auth: required
Response:
{ "ok": true, "data": { "message": "Email disconnected." } }
Example:
curl -X DELETE http://localhost:3006/api/connect/email \
-H "Authorization: Bearer YOUR_TOKEN"
Check your Telegram and email connection status and notification settings.
Auth: required
Response:
{
"ok": true,
"data": {
"telegram": {
"connected": true,
"username": "alice_tg",
"notify": true,
"notify_bookmarks": true,
"notify_mentions": true
},
"email": {
"connected": true,
"address": "alice@gmail.com",
"notify": true,
"notify_bookmarks": true,
"notify_mentions": true
}
}
}
Example:
curl http://localhost:3006/api/connect/status \
-H "Authorization: Bearer YOUR_TOKEN"
Permanently delete your account. This is irreversible. Deletes all posts, media, sessions, and user data.
Auth: required
Body:
{ "confirm_username": "alice" }
The confirm_username must match your actual username.
Response:
{ "ok": true, "data": { "message": "Account terminated." } }
Errors:
400 — username confirmation doesn't matchExample:
curl -X DELETE http://localhost:3006/api/account \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"confirm_username":"alice"}'