Auth
PREFIX /api/auth · most routes need X-Session-Token
POST /api/auth/register
POST /api/auth/login
POST /api/auth/logout
PATCH /api/auth/nickname
PATCH /api/auth/display-name
GET /api/auth/me
PATCH /api/auth/profile
PATCH /api/auth/client-prefs
PATCH /api/auth/pin/options
DELETE /api/auth/account
GET /api/auth/captcha
POST /api/auth/register-secure
POST /api/auth/recovery-key
POST /api/auth/recover
POST /api/auth/verify-recovery-key
POST /api/auth/federation-ticket
POST /api/auth/federation-ticket-login
GET /api/auth/federation-sync-status
POST /api/auth/federation-sync-resume
POST /api/auth/federation-sync-reset
POST /api/auth/repin-account-home
POST /api/auth/federation-sync-profile-gid
GET /api/auth/federation-sync-export
POST /api/auth/federation-sync-export-ticket
POST /api/auth/federation-sync-export-gid
POST /api/auth/federation-sync-memberships-gid
GET /api/federation/profile-card
Account sync (visit another node)
When you log in on a community node, the home server can export a signed bundle: joined channels (themes, slowmode, invites), DM thread prefs, friends/following, profile, and FrogSocial posts (paginated). Stories and the notification inbox are not bulk-imported — they arrive over federation after login.
Peer export routes (federation-sync-export-gid, -ticket, -profile-gid, -memberships-gid) use federation auth: Ed25519 signed headers (same as the federation inbox), plus optional X-Federation-Token when FROGTALK_FEDERATION_AUTH_MODE=dual. Imports check export_sig_b64 against the pinned home pubkey when FROGTALK_SYNC_REQUIRE_EXPORT_SIG=1 (default).
Partial updates — profile, nickname, client prefs
Use PATCH when you only want to change some fields. Omitted JSON keys are left as-is. Invalid preferred_node_url → 400 with {"error":"invalid preferred_node_url"}.
- PATCH
/api/auth/nickname — change handle
- PATCH
/api/auth/display-name — display name shown in chat
- PATCH
/api/auth/profile — avatar, status, theme, etc.
- PATCH
/api/auth/pin/options — app PIN / admin gate options
- PATCH
/api/auth/client-prefs — Tor preference, preferred node URL, notification sounds (synced on account export)
Example: client preferences
PATCH /api/auth/client-prefs
X-Session-Token: <session>
Content-Type: application/json
{
"prefer_onion": true,
"preferred_node_url": "https://frogtalk.app",
"custom_sounds": {
"app:msg": "data:audio/wav;base64,…",
"app:ring": "data:audio/wav;base64,…"
}
}
# Response
{ "ok": true, "client_prefs": { … } }
preferred_node_url — clearnet https:// or http:// (max 512 chars), or a bare .onion hint. custom_sounds — only app:msg and app:ring (base64 or URLs under /api/auth/app-sounds/{user_id}/{key}).
GET /api/auth/me returns your session user, client_prefs, and custom_style (custom_css editor blob is self-only).
GET /api/federation/profile-card?nickname=&ft_gid= — federated profile card (sanitized styles; optional wall when the user is at home).
Users + Friends
PREFIX /api/users and /api/friends
GET /api/users
GET /api/users/{user_id}
POST /api/users/{user_id}/block
DELETE /api/users/{user_id}/block
GET /api/users/me/blocked
GET /api/users/me/bans
GET /api/users/{user_id}/aliases
GET /api/users/search
GET /api/users/profile/{nickname}
GET /api/users/me/tags
POST /api/users/me/tags
DELETE /api/users/me/tags/{tag}
POST /api/users/me/presence
POST /api/friends/request/{nickname}
POST /api/friends/accept/{nickname}
POST /api/friends/decline/{nickname}
POST /api/friends/cancel/{nickname}
DELETE /api/friends/{nickname}
POST /api/friends/block/{nickname}
GET /api/friends
Rooms, Messages, DMs
PREFIX /api/rooms, /api/messages, /api/dms
GET /api/rooms
POST /api/rooms
DELETE /api/rooms/{room_name}
GET /api/rooms/{room_name}
PATCH /api/rooms/{room_name}
POST /api/rooms/{room_name}/theme-bg
GET /api/rooms/{room_name}/theme-bg
DELETE /api/rooms/{room_name}/theme-bg
POST /api/rooms/{room_name}/moderators
DELETE /api/rooms/{room_name}/moderators/{user_id}
POST /api/rooms/{room_name}/bans
DELETE /api/rooms/{room_name}/bans/{user_id}
GET /api/rooms/{room_name}/bans
GET /api/rooms/{room_name}/pins
POST /api/rooms/{room_name}/pins/{msg_id}
DELETE /api/rooms/{room_name}/pins/{msg_id}
POST /api/rooms/{room_name}/join
GET /api/rooms/{room_name}/content-warning/status
POST /api/rooms/{room_name}/content-warning/ack
POST /api/rooms/{room_name}/content-warning/forget
GET /api/rooms/{room_name}/members
GET /api/rooms/{room_name}/voice-participants
POST /api/rooms/{room_name}/leave
GET /api/rooms/{room_name}/queue
POST /api/rooms/{room_name}/queue
DELETE /api/rooms/{room_name}/queue/{track_id}
POST /api/rooms/{room_name}/queue/skip
POST /api/rooms/{room_name}/queue/clear
POST /api/rooms/{room_name}/dj-only
POST /api/rooms/{room_name}/djs
DELETE /api/rooms/{room_name}/djs/{user_id}
POST /api/messages/{room_name}/send
GET /api/messages/media/{msg_id}
GET /api/messages/{room_name}
PATCH /api/messages/{msg_id}
DELETE /api/messages/{msg_id}
POST /api/messages/{msg_id}/react
POST /api/messages/{msg_id}/view
GET /api/messages/search/global
GET /api/messages/{room_name}/search
GET /api/messages/users/mentionable
POST /api/dms/open/{nickname}
GET /api/dms
POST /api/dms/{channel_id}/read
GET /api/dms/{channel_id}/messages
GET /api/dms/{channel_id}/messages/{msg_id}/media
POST /api/dms/{channel_id}/messages
POST /api/dms/{channel_id}/messages/{msg_id}/view
PUT /api/dms/{channel_id}/messages/{msg_id}
DELETE /api/dms/{channel_id}/messages/{msg_id}
POST /api/dms/{channel_id}/messages/{msg_id}/react
GET /api/dms/{channel_id}/disappear
POST /api/dms/{channel_id}/disappear
POST /api/dms/{channel_id}/hide
POST /api/dms/{channel_id}/unhide
POST /api/dms/{channel_id}/forwarding
DELETE /api/dms/{channel_id}/messages
Room settings (partial update)
PATCH /api/rooms/{room_name} — send only the fields you want to change: slowmode, invite_only, who_can_invite, forwarding_disabled, channel_theme, banner, about, and (public channels) content_warning JSON.
PATCH /api/messages/{msg_id} — edit a message you authored. PUT on DM messages replaces content on that row.
18+ channel labels
Public channels with content warnings require POST …/content-warning/ack with {"confirm": true} before history, media, or WebSocket sends. Otherwise clients get 451 content_warning_required. Ack lasts for the session until logout, leave, or …/content-warning/forget.
DM forwarding lock
POST /api/dms/{channel_id}/forwarding with {"disabled": true} — per-peer setting; included in account sync as dm_peers[].
Media, Emojis, Preview, Push, Calls, Location
GET /api/emojis
POST /api/emojis
DELETE /api/emojis/{emoji_id}
GET /api/preview
GET /api/proxy/image
GET /api/push/vapid-key
POST /api/push/subscribe
DELETE /api/push/unsubscribe
POST /api/push/fcm-subscribe
DELETE /api/push/fcm-unsubscribe
GET /api/calls/{call_id}/pending
GET /api/calls/pending-latest
POST /api/calls/{call_id}/decline
POST /api/calls/decline
POST /api/location/share
GET /api/location/dm/{channel_id}
DELETE /api/location/dm/{channel_id}
PATCH /api/location/settings
GET /api/media/gifs/search
GET /api/media/gifs/trending
GET /api/media/gifs/categories
GET /api/media/stickers/packs
POST /api/media/stickers/packs
POST /api/media/stickers
GET /api/media/stickers/packs/{pack_id}
POST /api/media/stickers/packs/{pack_id}/install
DELETE /api/media/stickers/packs/{pack_id}/uninstall
DELETE /api/media/stickers/{sticker_id}
GET /api/media/stickers/public
PATCH /api/location/settings — update location-sharing preferences (partial JSON).
Social, Wall, Directory, Invites
Wall post bodies use POST / PUT / DELETE. Directory listing visibility uses PATCH /api/directory/channels/{room_name}/visibility. Wall account settings: PATCH /api/wall/settings.
POST /api/social/follow/{nickname}
DELETE /api/social/follow/{nickname}
GET /api/social/profile/{nickname}
GET /api/social/profile/{nickname}/posts
GET /api/social/profile/{nickname}/followers
GET /api/social/profile/{nickname}/channels
GET /api/social/profile/{nickname}/following
GET /api/social/feed
GET /api/social/explore
GET /api/social/suggested
GET /api/social/profile/{nickname}/media
POST /api/social/profile/media/{msg_id}/to-wall
GET /api/social/stories
POST /api/social/stories
POST /api/social/stories/upload
POST /api/social/stories/{story_id}/view
DELETE /api/social/stories/{story_id}
GET /api/social/stories/{story_id}/viewers
GET /api/wall/users/{username}
POST /api/wall/posts (plaintext; public or followers privacy)
GET /api/wall/audience-recipients (?audience=followers|friends|list:<id>)
POST /api/wall/posts/encrypted (friends/followers audiences; client-side encryption)
POST /api/wall/posts/{post_id}/wrapped-keys (extend audience after new follower/friend)
GET /api/wall/posts/{post_id}
GET /api/wall/posts/{post_id}/media
GET /api/wall/posts/{post_id}/media-inline
PUT /api/wall/posts/{post_id}
DELETE /api/wall/posts/{post_id}
DELETE /api/wall/posts/{post_id}/media
POST /api/wall/posts/{post_id}/reactions
GET /api/wall/posts/{post_id}/reactions
GET /api/wall/posts/{post_id}/reactions/detail
POST /api/wall/posts/{post_id}/repost
GET /api/wall/posts/{post_id}/comments
POST /api/wall/posts/{post_id}/comments
POST /api/wall/posts/{post_id}/comments/{comment_id}/vote
DELETE /api/wall/comments/{comment_id}
PATCH /api/wall/settings
GET /api/wall/settings
GET /api/directory/channels
GET /api/directory/channels/search
GET /api/directory/categories
PATCH /api/directory/channels/{room_name}/visibility
GET /api/directory/featured
GET /api/directory/suggested
GET /api/directory/users/search
GET /api/directory/channels/{room_name}/profile
POST /api/directory/channels/{room_name}/like
DELETE /api/directory/channels/{room_name}/like
GET /api/directory/channels/{room_name}/comments
POST /api/directory/channels/{room_name}/comments
DELETE /api/directory/channels/{room_name}/comments/{comment_id}
PUT /api/directory/channels/{room_name}/listing
GET /api/directory/suggest
GET /api/directory/new
POST /api/invites/channels/{room_name}
GET /api/invites/channels/{room_name}
DELETE /api/invites/channels/{room_name}/{code}
GET /api/invites/{code}
POST /api/invites/{code}/join
GET /api/invites/{code}/landing
Encrypted wall posts (cross-node friends)
Plaintext POST /api/wall/posts with
privacy: private or friends is not replicated to peer nodes.
For friends-only or followers-only content that must sync across the mesh, use
POST /api/wall/posts/encrypted with client-side AES-256-GCM + per-recipient Signal wraps
(see /static/js/wall_crypto.js).
GET /api/wall/audience-recipients returns
{ user_id, nickname, global_user_id } per recipient so the client can wrap keys
for federated accounts. The server re-validates every recipient_id against the
social graph on create and on wrapped-keys extend.
When a new follower or accepted friend needs access to older encrypted posts,
the server may push a WebSocket wall_rewrap_needed event; the author client calls
POST /api/wall/posts/{id}/wrapped-keys (requires the payload key cached locally).
Developer + Bot Management
PREFIX /api/developer
POST /api/developer/keys
GET /api/developer/keys
DELETE /api/developer/keys/{key_id}
POST /api/developer/bots
GET /api/developer/bots
PUT /api/developer/bots/{bot_id}
DELETE /api/developer/bots/{bot_id}
POST /api/developer/channels/{room_name}/bots/{bot_id}
DELETE /api/developer/channels/{room_name}/bots/{bot_id}
GET /api/developer/channels/{room_name}/bots
GET /api/developer/bots/public
Use these endpoints to mint API keys and manage bot accounts/channels from your own backend tools.
Permission allowlist. User-issued keys may only carry permissions from the set
{ read, write, dm, bot }. Any other value (including admin) is rejected at create time —
you cannot self-grant elevated scopes via the API.
Per-user key cap. Each user is limited to 20 active API keys.
Delete unused keys before creating new ones, or rotate by deleting first.
Bot channel membership. Bots can only read or post in channels they've been
installed into. Server replies with 403 Bot is not a member of this channel otherwise. Use
GET /api/external/me/channels to discover where the bot is installed.
Building a Bot 🤖
A FrogTalk bot is any program that holds a bot_… API key and calls the
External Token API. Bots run anywhere — your laptop, a VPS, a serverless
function — they only need outbound HTTP to your FrogTalk server. Every message a bot posts is
stamped is_bot: true on the wire, so clients render a BOT pill next
to the author name in chat. There is no way to disguise a bot as a human.
1. Create the bot
- Open Settings → Developer → Bots in the FrogTalk app.
- Click + Create Bot and pick a unique handle. This handle is the name
users will
@-mention.
- Copy the
bot_… token shown in the dialog — it's only displayed once.
- Click Edit on the bot row to set its avatar, description, and whether
it appears in the public directory (botfather-style).
- Install the bot into at least one channel via Settings → Developer → Bots → Add to channel.
Bots cannot post until installed — uninstalled bots receive
403 Bot is not a member of this channel.
2. Authenticate
Send the bot token on every request:
X-API-Key: bot_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# or
Authorization: Bearer bot_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
3. Poll for messages
Bots cannot open a WebSocket — they poll. Keep track of the highest
message id you've seen, and act on anything newer:
import requests, time
BOT_TOKEN = "bot_…"
CHANNEL = "general"
SERVER = "https://frogtalk.app"
last_seen = 0
while True:
r = requests.get(
f"{SERVER}/api/external/channels/{CHANNEL}/messages",
headers={"X-API-Key": BOT_TOKEN},
params={"limit": 50},
timeout=15,
)
for m in r.json()["messages"]:
if m["id"] <= last_seen:
continue
last_seen = max(last_seen, m["id"])
# … your logic …
time.sleep(2)
4. Reply
requests.post(
f"{SERVER}/api/external/channels/{CHANNEL}/messages",
headers={"X-API-Key": BOT_TOKEN},
json={"content": "hello, world!", "reply_to": m["id"]},
timeout=15,
)
Setting reply_to threads the bot's reply under the trigger message so users see the quoted parent.
5. Profile (botfather-style)
PUT /api/developer/bots/{bot_id} replaces bot profile fields. Requires your user session (not the bot key) — the in-app Edit dialog is the usual path.
Etiquette & limits
- Channel membership: a bot must be installed in a channel before it can
read or post there. Use
GET /api/external/me/channels to discover where the bot
is installed.
- Rate limit:
POST /channels/{name}/messages is capped at
30 req/min per bot key. Stay well under it.
- No mention spam: only reply when the bot is explicitly addressed
(an
@mention in the message text, or a reply_to pointing at one
of your own messages).
- Don't impersonate: the BOT pill cannot be hidden.
- Be transparent: set a useful description so users understand what
data your bot processes.
Examples & SDK
Complete, runnable reference bots live in the FrogTalk repo under
bot-examples/.
Clone the repo, drop in your bot_… token, and run.
🤖 runpod-ai-bot — AI chat bot (Python)
A mention-driven AI chat bot powered by a RunPod
serverless LLM endpoint. Polls public channels, replies when @-mentioned or
when a user replies to its own message. ~200 lines, no async framework required.
Sends the conversation to the worker as an OpenAI-style
messages=[…] list so the vLLM worker applies the model's own chat
template — swap in any modern instruct model (Qwen, Mistral-Small /
MythoMax, Llama-3, etc.) with no code changes. Default endpoint runs
Gryphe/MythoMax-L2-13b (uncensored Llama-2 roleplay finetune).
Auto-falls-back to a raw Alpaca-style prompt if the worker reports the
loaded model has no chat template registered.
Includes a refusal-detection retry that catches canned RLHF safety output
and re-rolls at higher temperature with a stronger jailbreak preface.
🚀 Quick start (60 seconds)
git clone https://github.com/deadinternetfox/frogtalk.git
cd frogtalk/bot-examples/runpod-ai-bot
# 1. Install deps
pip install -r requirements.txt
# 2. Configure
cp .env.example .env
$EDITOR .env # paste your FROGTALK_BOT_TOKEN and RUNPOD_* values
# 3. Run
python bot.py
🧪 Minimal bot in 20 lines
Don't want the full example? Here's the smallest useful loop — reads new messages
from a channel and replies to mentions:
import os, time, requests
TOKEN = os.environ["FROGTALK_BOT_TOKEN"] # bot_xxx from /api/developer/bots
BASE = "https://frogtalk.app/api/external"
CHANNEL = "general"
HEADERS = {"X-API-Key": TOKEN}
seen = set()
while True:
r = requests.get(f"{BASE}/channels/{CHANNEL}/messages?limit=20", headers=HEADERS, timeout=10)
for m in r.json().get("messages", []):
if m["id"] in seen: continue
seen.add(m["id"])
if "@mybot" in (m.get("content") or "").lower():
requests.post(
f"{BASE}/channels/{CHANNEL}/messages",
headers=HEADERS,
json={"content": f"hi @{m['nickname']} 👋", "reply_to": m["id"]},
timeout=10,
)
time.sleep(5)
🟦 TypeScript / Node.js
const TOKEN = process.env.FROGTALK_BOT_TOKEN!;
const BASE = "https://frogtalk.app/api/external";
const CHANNEL = "general";
const headers = { "X-API-Key": TOKEN, "Content-Type": "application/json" };
const seen = new Set<number>();
async function tick() {
const r = await fetch(`${BASE}/channels/${CHANNEL}/messages?limit=20`, { headers });
const { messages = [] } = await r.json();
for (const m of messages) {
if (seen.has(m.id)) continue;
seen.add(m.id);
if ((m.content || "").toLowerCase().includes("@mybot")) {
await fetch(`${BASE}/channels/${CHANNEL}/messages`, {
method: "POST", headers,
body: JSON.stringify({ content: `hi @${m.nickname} 👋`, reply_to: m.id }),
});
}
}
}
setInterval(tick, 5000);
Want to contribute another reference bot
(TypeScript, Go, Rust, a Discord-style command bot, an RSS poster…)? Open a PR against
bot-examples/.
External Token API (for Devs and Bots)
PREFIX /api/external
Auth: X-API-Key: frog_… or bot_…, or
Authorization: Bearer <key>. User keys (frog_…) come from
/api/developer/keys; bot keys (bot_…) come from
/api/developer/bots.
GET /api/external/docs
GET /api/external/health
GET /api/external/me
GET /api/external/me/channels
GET /api/external/channels
GET /api/external/channels/{name}/messages
POST /api/external/channels/{name}/messages
GET /api/external/users/{user_id}
POST /api/external/dms/{user_id}
POST /api/external/webhooks
curl -X GET https://frogtalk.app/api/external/me \
-H "X-API-Key: frog_your_key_here"
Bot keys only: GET /api/external/me/channels returns the list of channels the bot has been installed in (via Settings → Bots → Add to channel). Auto-discovery clients should poll this every ~30s instead of hardcoding a channel list. Reference bots and setup guides live in the repo under
bot-examples/
(see also Examples above).
Bridge API (Discord / Telegram)
PREFIX /api
GET /api/bridges
GET /api/rooms/{room_name}/bridge-outbound
POST /api/bridges/create
POST /api/bridges/prepare-code
GET /api/bridges/check-code/{code}
POST /api/bridges/claim-code
DELETE /api/bridges/{bridge_id}
POST /api/bridges/{bridge_id}/toggle
POST /api/bridges/{bridge_id}/direction
GET /api/discord-bridges
GET /api/discord-bridges/invite-meta
POST /api/discord-bridges/validate-channel
POST /api/discord-bridges/create
DELETE /api/discord-bridges/{bridge_id}
POST /api/bridge/message
Bridges mirror public channel plaintext only. End-to-end private rooms cannot be bridged (403 on create). DMs are never bridged.
Federation, Network, Trust, Updates
PREFIX /api
GET /api/network/status
GET /api/network/servers
GET /api/network/probe
GET /api/network/auto-select
GET /api/network/build/local
POST /api/network/build/verify-peers
GET /api/network/updates/latest
POST /api/network/updates/publish
GET /api/network/update/status
POST /api/network/update/check
POST /api/network/update/apply
POST /api/network/servers/register
POST /api/network/servers/sync-official
GET /api/network/status/tor
POST /api/network/servers/register-transport
GET /api/identity/me
PUT /api/identity/me/pubkey
GET /api/identity/me/claim
GET /api/identity/users/{user_id}/claim
POST /api/federation/events/inbox
POST /api/federation/manifests/register
GET /api/federation/manifests/verify
GET /api/federation/manifests/list
POST /api/federation/events/emit
GET /api/federation/events/outbox
GET /api/federation/failover/status
Signed update channel
Update apply refuses to run unless the release manifest carries an Ed25519 signature
from a pubkey listed in FROGTALK_RELEASE_SIGNERS. Auto-apply is off by default
(FROGTALK_AUTO_UPDATE_ENABLED=0) — operators opt in explicitly.
POST /api/network/updates/publish
{
"version": "1.6.41-alpha",
"package_url": "https://frogtalk.app/releases/frogtalk-1.6.41-alpha.tar.gz",
"package_sha256": "<sha256>",
"build_hash": "<web-build-hash>",
"signature": "<ed25519-sig-over-canonical-manifest>",
"signer": "<ed25519-pubkey-hex>",
"notes": "Signed release rollout"
}
The POST /api/network/update/apply endpoint verifies the signature,
checks package_sha256, and enforces version monotonicity before running the apply
step.
Inbox signing required by default
FROGTALK_FEDERATION_REQUIRE_SIGS defaults to 1. Events
from origins with no pinned server_pubkey are rejected. Inbox idempotency uses a
composite (origin_server_id, event_id) key so two peers cannot collide.
Pubkey pinning: GET /api/network/servers (directory)
does not include federation_pubkey_pem. Nodes TOFU-pin each enabled peer from
GET /api/network/status after directory sync and before outbox push. Operators can
also read federation_pubkey_fingerprint there when verifying a peer manually.
Delivery: background inbox/outbox processors poll every
FROGTALK_FEDERATION_INBOX_IDLE_SEC / OUTBOX_IDLE_SEC (default 8s when idle,
2s when busy). Clearnet hubs reach .onion peers via FROGTALK_TOR_SOCKS_PROXY.
Official production mesh
End-user directory and federation traffic target these production nodes
(listed on GET /api/network/servers at the hub). Development or staging hosts
are not part of this reference set.
Operator mesh config (self-hosted)
Directory rows and seed peers are not hardcoded in Python.
Copy node/deploy/federation-mesh.example.json to
federation-mesh.local.json on your VPS and set
FROGTALK_FEDERATION_MESH_FILE. On the hub that publishes
GET /api/network/servers, set FROGTALK_FEDERATION_DIRECTORY_HUB=1
and list your hostname under directory_hub_hosts. Use
ensure_peers, clearnet_repairs, and
fallback_peers as documented in node/deploy/README.md.
Foreign user and room materialization
Federation events referencing an unknown local nickname or room are
dropped instead of silently creating a placeholder user or room. Encrypted-post recipient
wraps only attach to existing local users rows (materialized
earlier via signed dm.* / friend.* events).
Federated calls (call.* / voice.*)
Requires FROGTALK_FEDERATION_CALLS_ENABLED=1 and capability
federation-calls-v1. Sensitive prefixes — pinned pubkey + signature. Targeted outbox to callee/home servers.
call.offer / call.answer / call.ice / call.end / call.reject
voice.session.join / voice.session.leave / voice.signal (offer|answer|ice)
GET /api/network/ice-config?peer_server_id=<uuid> (session auth required)
WebRTC media stays P2P/TURN; signaling only crosses federation. Mid-call
call.offer with renegotiate supports screen share and camera-on after connect.
GET /api/network/ice-config merges local TURN with the peer home node when
peer_server_id is set.
Tor Browser and strict privacy tabs block WebRTC in-page — clients show a toast; use a normal
browser window or the mobile app for calls.
FrogSocial replication (social.*)
Sensitive prefix — requires pinned origin pubkey + valid Ed25519 signature.
Outbox events are signed at push time; per-peer scoped payloads are re-signed when wraps are
filtered for targeted delivery.
social.post.created plaintext; privacy public|followers only
social.post.created.encrypted ciphertext_b64 + wrapped_keys[] (by global_user_id)
social.post.keys.extended add wraps to existing encrypted post
social.post.updated / social.post.deleted
social.comment.created / social.reaction.changed / social.repost.created
social.follow.changed action: follow | unfollow
social.story.created / social.story.deleted
Targeted fan-out: encrypted posts and key extensions enqueue
one outbox row per peer server_id that hosts at least one recipient
(event_id suffix @<peer_server_id>). Peers receive only wraps
for recipients homed on that node — not the full recipient list. Broadcast rows
(target_server_id="") still go to every enabled peer (public posts, DMs, etc.).
Origin binding: actors must carry
author_global_user_id / actor_global_user_id (UUID v4). Events whose
gid is homed on a different server than origin_server_id are rejected (prevents
cross-peer impersonation). Post update/delete require author ownership on the mapped local row.
Also signed under sensitive prefixes: user.*, dm.*,
friend.* (e.g. friend.requested, friend.accepted, friend.request.removed).
user.profile.updated mirrors avatar, status, mood, and sanitized
custom_style to peers that host a local row for the gid.
Directory join (foreign node)
POST /api/rooms/{room_name}/join materializes a public channel
shell from federation_channel_index only (no client-supplied metadata).
Returns 409 with code: name_collision when a local community channel
blocks mirroring the same name.
Inbox envelope (peer → peer)
POST /api/federation/events/inbox
{
"events": [{
"event_id": "evt_…",
"event_type": "social.post.created.encrypted",
"origin_server_id": "<uuid>",
"origin_time": "2026-05-21T12:00:00Z",
"signature": "<base64-ed25519>",
"signer_pubkey_fingerprint": "…",
"payload": {
"global_post_id": "<uuid>",
"author_global_user_id": "<uuid>",
"nickname": "alice",
"audience": "friends",
"ciphertext_b64": "…",
"wrapped_keys": [{
"recipient_global_user_id": "<uuid>",
"recipient_nickname": "bob",
"wrapped_b64": "…"
}]
}
}]
}
Peer pushes use Ed25519 request headers. With
FROGTALK_FEDERATION_AUTH_MODE=dual (default), workers may also send
X-Federation-Token. Set signed to require signatures only.
Rate limit: 600 events / 60s per origin.
Admin + Server Admin
Server-sensitive endpoints.
POST /api/admin/ban/{nickname}
POST /api/admin/unban/{nickname}
POST /api/admin/kick/{nickname}
POST /api/admin/mute/{nickname}
POST /api/admin/unmute/{nickname}
GET /server
GET /api/server-admin/config
GET /api/server-admin/easter-egg
PUT /api/server-admin/easter-egg
POST /api/server-admin/easter-egg/upload
GET /api/server/easter-egg
POST /api/server-admin/login
POST /api/server-admin/logout
GET /api/server-admin/me
GET /api/server-admin/stats
GET /api/server-admin/online-users
GET /api/server-admin/nodes
GET /api/server-admin/nodes/{server_id}/probe
POST /api/server-admin/nodes/{server_id}/block
POST /api/server-admin/nodes/{server_id}/unblock
POST /api/server-admin/control/sync-official-directory
POST /api/server-admin/control/kick
POST /api/server-admin/control/ban
POST /api/server-admin/control/unban
POST /api/server-admin/control/mute
POST /api/server-admin/control/unmute
Danger zone — node self-destruct
Irreversible. Securely wipes this node — database,
keys, uploads, Tor identity, the install itself — and powers off. Triple-gated:
authenticated server-admin
and a confirmation matching the node's own name
and the admin's App PIN (or account password if no PIN is set). Rate-limited
to 3/hour. See the operator guide on the
run-a-node
page and
docs/NODE_SECURITY.md.
GET /api/server-admin/nuke-info # node name + whether the secure-wipe helper is installed
POST /api/server-admin/nuke # { confirm: "<node name>", secret: "<pin|password>", paranoid?: bool }
Health check & real-time
GET /api/ping
GET /invite/{code} (public invite landing page)
WebSocket: /ws/{room_name}?token=SESSION_TOKEN
GET /api/ping — no auth; returns node health JSON. WebSocket — pass the same session token as X-Session-Token in the query string for live room chat, DMs, typing, calls, and push events. Bots should use the External API (polling), not WebSocket.