🎉 Beta: Welcome to the beta! Join our Discord to report bugs or request features.

Developers & API

Carrion exposes a REST API for building bots, integrations, and third-party tools. Bots are first-class citizens — they use the same endpoints as regular characters, authenticated with bearer tokens instead of session cookies.

Quickstart

Get a bot connected and listening for events in under 5 minutes.

1. Create a character for your bot

Create a regular character via the site. This will become your bot's identity.

2. Designate it as a bot

POST /api/v1/bots/designate/ Content-Type: application/json Cookie: sessionid=your_session_cookie { "character_id": 42, "character_name": "MyBotName", "bot_description": "Dice roller and game master" }

You'll receive a token like bottoken_a8f3... in the response. Copy it immediately — it's shown once and never again.

Irreversible. Designating a character as a bot permanently sacrifices that character slot. The character will display a bot badge and cannot be converted back. The character_name field must match the character's actual name — this prevents accidentally botting the wrong character.

3. Connect to the event stream

GET /api/v1/events/subscribe/YourBotName/ Authorization: Bearer bottoken_a8f3... # Response: Server-Sent Events stream event: initial_state data: {"online_characters": [...], "server_version": "..."} event: channel_message data: {"room": "lobby@conference.carrion", "from": "Alice", "body": "Hello!", ...} # Keepalive every 30 seconds : keepalive

4. Send a message

POST /api/v1/channels/lobby@conference.carrion/send/ Authorization: Bearer bottoken_a8f3... X-Character-ID: 42 Content-Type: application/json { "body": "Hello from a bot!" }

That's it. Your bot is online, receiving events, and can send messages.

Authentication

Two authentication methods are supported:

Bot Token (recommended for bots)

Include the token in the Authorization header on every request:

Authorization: Bearer bottoken_a8f3...

The middleware authenticates the token, sets the request user to the bot's owner account, and sets the active character to the bot character. No session cookie needed.

Session Cookie (browsers)

Standard Django session authentication via the sessionid cookie. Used by the web UI. Requires X-Character-ID header to identify which character is acting.

Character Header

Most endpoints require the X-Character-ID header to identify which character is making the request. Bot token auth sets this automatically, but you can also include it explicitly:

X-Character-ID: 42

Bot Setup

MethodEndpointDescription
POST /api/v1/bots/designate/ Designate character as bot, get token Auth
POST /api/v1/bots/regenerate-token/ Regenerate token (invalidates old) Auth
GET /api/v1/bots/list/ List your bots Auth
GET /api/v1/bots/channel/{room_jid}/ List bots permitted in a channel Auth

Token security. Tokens are hashed with SHA-256 before storage — we never store the plaintext. If you lose your token, use regenerate-token to get a new one (the old token stops working immediately).

API Overview

The API is organized into routers by domain. All endpoints are under /api/v1/.

RouterPathPurpose
Characters/charactersSearch, profiles, kinks, compatibility
Presence/presenceHeartbeat, room subscribe/join/leave, online status
Channels/channelsSend messages, topics, moderation, ownership
DM/dmSend DMs, cradle pull/clear, room generation
Events/eventsSSE event stream (Drakensberg)
Classifieds/classifiedsPost, browse, respond to classifieds No Bots
Love Letters/love-lettersAsync messages (inbox, send, quota) No Bots
Moderation/moderationReports, restrictions, bans
Social/socialBlock/unblock characters
Bots/botsBot designation, tokens, system hooks
Matchmaker/matchmakerProfile-based matchmaking No Bots
Blind Date/blind-dateAnonymous matching with profiles No Bots
Blind Chat/blind-chatAnonymous matching (no profile needed) No Bots
Vault/vaultEncrypted chat vault (cross-device sync)
Sync/syncPeer-to-peer message sync relay
Keys/keysECDH public key publish/fetch
Roll/rollVerifiable dice rolls
Broadcast/broadcastSite-wide announcements Admin
Notifications/notificationsNotification center
Content Filters/content-filtersKink blacklist per account No Bots
Ads/adsSubmit and display ads No Bots
Support/supportSubscriptions No Bots
Health/healthLiveness, readiness probes
Kink Wizard/kink-wizardKink suggestion data

For full endpoint details with request/response schemas, see the interactive API docs (Swagger UI, requires login).

SSE Event Stream

Carrion uses a single Server-Sent Events connection per character for all real-time events. This is the primary way bots receive data.

Connecting

GET /api/v1/events/subscribe/{character_name}/ Authorization: Bearer bottoken_...
  • Long-lived connection (max 2 hours, then reconnect)
  • Server sends keepalive comment every 30 seconds
  • First event is initial_state with full presence snapshot
  • Reconnect with exponential backoff on disconnect

Event Format

event: channel_message data: {"room": "lobby@conference.carrion", "from": "Alice", "from_id": 7, "body": "Hello!", "message_id": "abc123", "timestamp": "2026-02-25T12:00:00Z"} event: user_online data: {"character": "Alice", "avatar_url": "https://...", "name_color": "#ff6b6b"}

Event Types

45 event types across these categories:

Messaging

channel_message — Message in a channel
dm_message — DM notification (pull cradle)
classified_response — Response to your classified
love_letter — Love letter received
cross_character_notification — Event for another character on same account

Presence

user_online — Character came online
user_offline — Character went offline
intent_changed — Character changed intent
presence_changed — Online/away/absent/lurking
in_chat_changed — Entered or left chat
profile_updated — Profile was edited

Rooms

user_joined_room — Character joined a room
user_left_room — Character left a room
topic_changed — Room topic updated

Moderation

you_were_kicked — You were kicked
you_were_banned — You were banned
you_were_muted / you_were_unmuted
user_kicked / user_banned / user_muted
moderation_action — System hook: moderation taken

Ownership

claim_granted — Channel ownership claimed
mod_invite / mod_accepted
mod_granted / mod_revoked
transfer_invite / ownership_granted

Other

broadcast — Site-wide announcement
blind_date_match — Matched in blind date
vault_available — Chat vault ready
evicted — Another session took your slot

Sending Messages

Channel Messages

Your bot must be subscribed to a room before sending. Messages are fan-out delivered via SSE to all room members.

# 1. Subscribe to rooms POST /api/v1/presence/subscribe-rooms/ Authorization: Bearer bottoken_... X-Character-ID: 42 { "rooms": ["lobby@conference.carrion"] } # 2. Send a message POST /api/v1/channels/lobby@conference.carrion/send/ Authorization: Bearer bottoken_... X-Character-ID: 42 { "body": "Hello from a bot!" }

Direct Messages

DMs use a pull model: you send to the cradle, the recipient pulls. DMs are end-to-end encrypted — see DM Encryption for how to set up your bot's keypair and encrypt/decrypt messages.

# 1. Generate the DM room ID (deterministic, server-side) POST /api/v1/dm/generate-room/ { "character_id_1": 42, "character_id_2": 99 } # Response: { "room_jid": "dm-a8f3e2...@conference.carrion" } # 2. Send message (queued in Redis cradle, 7-day TTL) # Body should be ECDH-encrypted — see DM Encryption section POST /api/v1/dm/send-message/ { "room_jid": "dm-a8f3e2...@conference.carrion", "to_character_id": 99, "body": "ENC:AQx8k2f..." }

No server-side storage. Channel messages are relayed through RAM and discarded. DM messages are cradled in Redis with a 7-day TTL, then deleted after delivery. The server never persists message content to disk.

DM Encryption (ECDH)

All DMs on Carrion are end-to-end encrypted using ECDH (Elliptic Curve Diffie-Hellman) with P-256. If your bot sends or receives DMs, it needs an encryption keypair.

Required for DMs. Without a keypair, your bot can't decrypt incoming DMs and shouldn't send DMs (they'd arrive as plaintext, which recipients' clients don't expect). If your bot only uses channels, you can skip this section.

Step 1: Generate a Keypair

Call the bot keypair endpoint. This generates a P-256 keypair, publishes the public key to your bot's profile, and returns the private key once.

POST /api/v1/keys/bot/generate-keypair/ Authorization: Bearer bottoken_... # Response: { "success": true, "private_key_hex": "a1b2c3d4e5f6...64 hex characters...", "public_key_jwk": { "kty": "EC", "crv": "P-256", "x": "base64url...", "y": "base64url..." }, "message": "Store the private_key_hex securely..." }

Save the private key immediately. It's shown once and never again. Store it in your .env file or secret manager. If you lose it, you can call the endpoint again to generate a new keypair, but old encrypted DMs will become unreadable.

Step 2: Fetch Peer Public Keys

To encrypt a message to someone (or decrypt one from them), you need their public key:

GET /api/v1/keys/{character_id}/ Authorization: Bearer bottoken_... # Response: { "character_id": 99, "public_key": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"...\",\"y\":\"...\"}" }

The public_key field is a JSON string containing a JWK. Parse it to get the x and y coordinates.

Step 3: Encrypt & Decrypt

The protocol is standard ECDH with AES-256-GCM. Both sides derive the same shared secret, which is used to encrypt/decrypt.

Wire Format

Encrypted messages on the wire look like: ENC: + base64 of:

OffsetLengthField
01 byteVersion (0x01)
112 bytesIV (random nonce)
13variableAES-GCM ciphertext (includes 16-byte auth tag)

Key Derivation (must match exactly)

# 1. ECDH shared secret shared_secret = ECDH(your_private_key, peer_public_key) # 2. HKDF to derive AES key derived_key = HKDF( algorithm = SHA-256, length = 32 bytes, salt = None, info = b"carrion-dm-encryption" ).derive(shared_secret)

Encrypting (sending a DM)

# Generate random 12-byte IV iv = os.urandom(12) # Encrypt with AES-256-GCM aesgcm = AESGCM(derived_key) ciphertext = aesgcm.encrypt(iv, plaintext.encode('utf-8'), None) # Build wire format message_bytes = b'\x01' + iv + ciphertext body = "ENC:" + base64.b64encode(message_bytes).decode() # Send via DM API POST /api/v1/dm/send-message/ { "room_jid": "dm-...", "to_character_id": 99, "body": body }

Decrypting (receiving a DM)

# Parse wire format raw = base64.b64decode(body[4:]) # Strip "ENC:" prefix version = raw[0] # Should be 0x01 iv = raw[1:13] ciphertext = raw[13:] # Derive shared secret from SENDER's public key sender_public_key = fetch_key(message["from_id"]) shared_secret = ECDH(your_private_key, sender_public_key) derived_key = HKDF(SHA-256, 32, salt=None, info=b"carrion-dm-encryption").derive(shared_secret) # Decrypt aesgcm = AESGCM(derived_key) plaintext = aesgcm.decrypt(iv, ciphertext, None).decode('utf-8')

Receiving DMs (Pull Model)

When your bot receives a dm_message SSE event, it's just a notification. You need to pull the actual messages from the cradle:

# 1. Receive SSE notification event: dm_message data: {"room": "dm-a8f3e2...@conference.carrion"} # 2. Pull messages from cradle GET /api/v1/dm/cradle/dm-a8f3e2...@conference.carrion/ Authorization: Bearer bottoken_... # Response: list of message objects [ { "messageId": "abc123", "from": "Alice", "from_id": 7, "body": "ENC:AQ...", <-- decrypt this "timestamp": 1740500000 } ] # 3. After storing/processing, clear the cradle DELETE /api/v1/dm/cradle/dm-a8f3e2...@conference.carrion/ Authorization: Bearer bottoken_...

Per-character keys. Bot keys are stored per-character (not per-account like browser users). This means a bot on a shared account won't clobber the human user's encryption key. Key lookup at GET /api/v1/keys/{id}/ transparently returns the right key for both bots and regular characters — callers don't need to know the difference.

Crypto Summary

ParameterValue
CurveP-256 (secp256r1)
EncryptionAES-256-GCM
Key DerivationHKDF-SHA256, info="carrion-dm-encryption", no salt
IV12 bytes (random)
Key Length32 bytes
Wire PrefixENC:
EncodingStandard base64 (RFC 4648)
Version Byte0x01

Presence & Rooms

Bots maintain presence the same way browsers do: heartbeats extend your TTL, and room membership is managed via REST.

Heartbeat

The SSE keepalive doubles as a heartbeat, so connected bots stay online automatically. If you need explicit heartbeat control:

POST /api/v1/presence/heartbeat/ Authorization: Bearer bottoken_... X-Character-ID: 42

Room Lifecycle

MethodEndpointDescription
POST /presence/subscribe-rooms/ Subscribe to multiple rooms at once
POST /presence/join-room/ Join a single room
POST /presence/leave-room/ Leave a room
POST /presence/disconnect/ Go offline and clean up all rooms
GET /presence/default-rooms/ Get the public room list
GET /presence/room/{room_jid}/ Get room occupants

System Hooks

System hooks allow bots to receive internal platform events that aren't part of the normal SSE stream. These are admin-granted and invisible to regular users.

Hook TypeEventDescription
reports moderation_report Fires when a user submits a report. Payload includes report details, target context, and chat evidence.
moderation_actions moderation_action Fires when a moderation action is taken (warning, mute, ban, etc.).

Admin only. System hooks are granted by site administrators via POST /api/v1/bots/system-hook/. They expose sensitive platform data and should only be given to trusted bots.

Channel Permissions

By default, bots can only join user-created channels (chan-* and priv-*) where they've been explicitly permitted. Channel owners control bot access via owner commands.

Permission Levels

LevelWhat the Bot SeesUser NotificationUse Case
ping Only messages that mention it via ?[BotName]. Everything else is filtered out server-side — the bot never receives other messages. None. Users opt in per-message by choosing to ping. Utility bots (dice, 8-ball, lookup tools)
read All messages in the channel. Full visibility into the conversation. Permanent banner: "Bots with read access: BotName". Cannot be hidden by channel themes. Moderation bots, AI assistants

Why Two Levels?

Ping-only is the recommended default. It mirrors modern bot invocation (similar to Discord's slash commands): your bot only has to process messages explicitly directed at it, instead of every message in the channel. This is better for bot developers (no firehose of irrelevant messages to filter through) and better for users (no surveillance concern).

Read access is for bots that genuinely need full context — moderation bots, AI assistants that track conversation flow, etc. Because read access means full visibility, channels with read-access bots display a persistent notification banner that cannot be hidden or restyled by channel themes. Users always know when a bot can see everything.

Server-side enforcement. Permission filtering happens on the server during message fan-out. A ping-only bot literally never receives messages it wasn't mentioned in — there's no client-side filtering to bypass. Default-deny: a bot in a room with no permissions sees nothing.

Granting Permissions (Channel Owner)

Channel owners grant bot permissions via the owner-command API. This is typically done through the chat UI's /permit-bot slash command.

# Grant ping access (bot only sees messages directed at it) POST /api/v1/channels/owner-command/ { "room_jid": "chan-abc123@conference.carrion", "command": "permit-bot", "target_nick": "MyBot", "bot_permission": "ping", "proof": "ownership_proof_hash..." } # Grant read access (bot sees ALL messages in channel) POST /api/v1/channels/owner-command/ { "room_jid": "chan-abc123@conference.carrion", "command": "permit-bot", "target_nick": "MyBot", "bot_permission": "read", "proof": "ownership_proof_hash..." } # Revoke a specific permission POST /api/v1/channels/owner-command/ { "room_jid": "chan-abc123@conference.carrion", "command": "remove-bot", "target_nick": "MyBot", "bot_permission": "read", "proof": "ownership_proof_hash..." } # Revoke ALL permissions (omit bot_permission) POST /api/v1/channels/owner-command/ { "room_jid": "chan-abc123@conference.carrion", "command": "remove-bot", "target_nick": "MyBot", "proof": "ownership_proof_hash..." }

Listing Permitted Bots

Any authenticated user can see which bots have access to a channel:

GET /api/v1/bots/channel/chan-abc123@conference.carrion/ # Response: { "room_jid": "chan-abc123@conference.carrion", "bots": [ { "character_id": 42, "character_name": "DiceBot", "bot_description": "Dice roller and game master", "permissions": ["ping"] }, { "character_id": 99, "character_name": "Claire", "bot_description": "Friendly utility bot", "permissions": ["ping", "read"] } ] }

Ownership restriction. You can only permit your own bots in your channels. Admins can permit any bot (e.g., site-wide bots like Claire).

Bot Identity

Bots are first-class characters with a few differences from regular characters.

What's Different

  • Bot badge — Automatically added on designation. Cannot be removed (irrevocable).
  • Not selectable — Bot characters don't appear in the character selector dropdown. They can only connect via API token.
  • No love letters — Users can't send love letters to bots.
  • Exempt from character slots — Bot characters don't count against the concurrent character limit.

Avatar & Appearance

Bots use the same profile fields as regular characters. Set your bot's avatar, name color, and description through the normal character edit page or via the character API.

Ping format. Users trigger bots via ?[BotName] command in chat. Your bot receives these as channel_message events where the body starts with ?[YourBotName]. Parse the text after your bot's name as the command.

Rate Limits

Most endpoints have rate limits to prevent abuse. Bots are subject to the same limits as regular users.

ActionLimit
Channel messagesReasonable use (no hard cap, but flooding gets you kicked)
DM messages10 messages per room in cradle
Love letters10 per day per character
Reports10 per hour per user
Classifieds1 active per character
SSE connections1 per character (additional connections evict the oldest)

Full API Reference

The complete API with request/response schemas is available as interactive Swagger docs:

Open API Docs (Swagger UI)

The Swagger UI lets you browse all 24 routers and 170+ endpoints, view request/response schemas, and try endpoints directly from your browser (requires login).

Building something? We'd love to see it. Bots, tools, integrations — reach out on Discord or message Vulture on the site.