API Reference

motd is API-first. Every feature is an endpoint. Build clients, bots, integrations — whatever you want.

Base URL

https://motd.social/api

Local dev: http://localhost:3006/api

Response Format

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.

Authentication

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).


Auth

POST /api/auth/register

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:

Example:

curl -X POST http://localhost:3006/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"hunter2pwd"}'

POST /api/auth/login

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:

Example:

curl -X POST http://localhost:3006/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"hunter2pwd"}'

GET /api/auth/me

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"

POST /api/auth/logout

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"

Profile

GET /api/profile/:username

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:

Example:

curl http://localhost:3006/api/profile/alice

PUT /api/profile

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:

Example:

curl -X PUT http://localhost:3006/api/profile \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"bio":"shipping code"}'

GET /api/profile/search?q=

Search users by username or display name.

Auth: none

Params:

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"

Posts

POST /api/posts

Publish a post.

Auth: required

Body:

{ "content": "just shipped the wasm compiler [rust] [wasm]" }

Optional fields:

Inline commands:

Rules:

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:

Examples:

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 /api/posts/feed

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:

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 /api/posts/:id

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 /api/posts/:id/thread

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

GET /api/posts/search

Search posts by content. Searches active posts by default, or archived posts with the archive flag.

Auth: none

Params:

Response (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"

Filter (Kill)

The /kill command maps to the filter API. The word "kill" never appears in the database or API.

POST /api/filter

Add a filter. Hides a user or post from your feed.

Auth: required

Body:

{ "target_type": "user", "target_id": "bob" }

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:

Example:

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 /api/filter

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"

Media

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.

POST /api/media/upload

Upload a file.

Auth: required

Body: multipart/form-data with file field

Response:

{
  "ok": true,
  "data": {
    "media_id": "x7km2p",
    "type": "png"
  }
}

Errors:

Example:

curl -X POST http://localhost:3006/api/media/upload \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "file=@screenshot.png"

GET /api/media/:id

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

GET /api/media/:id/thumb

Serve the thumbnail (PNG only, max 80px wide).

Auth: none

Example:

curl http://localhost:3006/api/media/x7km2p/thumb -o thumb.png

GET /api/media/:id/large

Serve the large version (PNG only, max 800px wide).

Auth: none

Example:

curl http://localhost:3006/api/media/x7km2p/large -o large.png

GET /api/media/:id/ascii

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

Avatar

POST /api/avatar

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 /api/avatar/:username

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

Bookmarks are private. Nobody sees your bookmarks. No counts, no notifications. One concept for everything: users, tags, posts, docs.

POST /api/bookmarks

Add a bookmark.

Auth: required

Body:

{ "target_type": "user", "target_id": "alice" }

Bookmarking the same thing twice is a no-op, not an error.

Response:

{ "ok": true, "data": { "message": "Bookmarked." } }

Errors:

Example:

# 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"}'

DELETE /api/bookmarks

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"}'

GET /api/bookmarks

List your bookmarks. Optionally filter by type.

Auth: required

Params:

Response:

{
  "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"

Docs

GET /api/docs

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 /api/docs/:name

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:

Example:

curl http://localhost:3006/api/docs/api

Categories

GET /api/categories

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

Commands

GET /api/commands

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

Telegram Connect

POST /api/connect/telegram

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"

DELETE /api/connect/telegram

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"

POST /api/connect/email

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"}'

GET /api/connect/email/verify

Verify email address from link in verification email.

Auth: none (link from email)

Params:

Returns an HTML confirmation page on success.

DELETE /api/connect/email

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"

GET /api/connect/status

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"

Account

DELETE /api/account

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:

Example:

curl -X DELETE http://localhost:3006/api/account \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"confirm_username":"alice"}'

← Open motd.social terminal