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.
Contents
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
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
4. Send a message
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:
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:
Bot Setup
| Method | Endpoint | Description |
|---|---|---|
| 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/.
| Router | Path | Purpose |
|---|---|---|
| Characters | /characters | Search, profiles, kinks, compatibility |
| Presence | /presence | Heartbeat, room subscribe/join/leave, online status |
| Channels | /channels | Send messages, topics, moderation, ownership |
| DM | /dm | Send DMs, cradle pull/clear, room generation |
| Events | /events | SSE event stream (Drakensberg) |
| Classifieds | /classifieds | Post, browse, respond to classifieds No Bots |
| Love Letters | /love-letters | Async messages (inbox, send, quota) No Bots |
| Moderation | /moderation | Reports, restrictions, bans |
| Social | /social | Block/unblock characters |
| Bots | /bots | Bot designation, tokens, system hooks |
| Matchmaker | /matchmaker | Profile-based matchmaking No Bots |
| Blind Date | /blind-date | Anonymous matching with profiles No Bots |
| Blind Chat | /blind-chat | Anonymous matching (no profile needed) No Bots |
| Vault | /vault | Encrypted chat vault (cross-device sync) |
| Sync | /sync | Peer-to-peer message sync relay |
| Keys | /keys | ECDH public key publish/fetch |
| Roll | /roll | Verifiable dice rolls |
| Broadcast | /broadcast | Site-wide announcements Admin |
| Notifications | /notifications | Notification center |
| Content Filters | /content-filters | Kink blacklist per account No Bots |
| Ads | /ads | Submit and display ads No Bots |
| Support | /support | Subscriptions No Bots |
| Health | /health | Liveness, readiness probes |
| Kink Wizard | /kink-wizard | Kink 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
- Long-lived connection (max 2 hours, then reconnect)
- Server sends keepalive comment every 30 seconds
- First event is
initial_statewith full presence snapshot - Reconnect with exponential backoff on disconnect
Event Format
Event Types
45 event types across these categories:
Messaging
channel_message — Message in a channeldm_message — DM notification (pull cradle)classified_response — Response to your classifiedlove_letter — Love letter receivedcross_character_notification — Event for another character on same accountPresence
user_online — Character came onlineuser_offline — Character went offlineintent_changed — Character changed intentpresence_changed — Online/away/absent/lurkingin_chat_changed — Entered or left chatprofile_updated — Profile was editedRooms
user_joined_room — Character joined a roomuser_left_room — Character left a roomtopic_changed — Room topic updatedModeration
you_were_kicked — You were kickedyou_were_banned — You were bannedyou_were_muted / you_were_unmuteduser_kicked / user_banned / user_mutedmoderation_action — System hook: moderation takenOwnership
claim_granted — Channel ownership claimedmod_invite / mod_acceptedmod_granted / mod_revokedtransfer_invite / ownership_grantedOther
broadcast — Site-wide announcementblind_date_match — Matched in blind datevault_available — Chat vault readyevicted — Another session took your slotSending Messages
Channel Messages
Your bot must be subscribed to a room before sending. Messages are fan-out delivered via SSE to all room members.
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.
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.
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:
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:
| Offset | Length | Field |
|---|---|---|
| 0 | 1 byte | Version (0x01) |
| 1 | 12 bytes | IV (random nonce) |
| 13 | variable | AES-GCM ciphertext (includes 16-byte auth tag) |
Key Derivation (must match exactly)
Encrypting (sending a DM)
Decrypting (receiving a DM)
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:
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
| Parameter | Value |
|---|---|
| Curve | P-256 (secp256r1) |
| Encryption | AES-256-GCM |
| Key Derivation | HKDF-SHA256, info="carrion-dm-encryption", no salt |
| IV | 12 bytes (random) |
| Key Length | 32 bytes |
| Wire Prefix | ENC: |
| Encoding | Standard base64 (RFC 4648) |
| Version Byte | 0x01 |
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:
Room Lifecycle
| Method | Endpoint | Description |
|---|---|---|
| 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 Type | Event | Description |
|---|---|---|
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
| Level | What the Bot Sees | User Notification | Use 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.
Listing Permitted Bots
Any authenticated user can see which bots have access to a channel:
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.
| Action | Limit |
|---|---|
| Channel messages | Reasonable use (no hard cap, but flooding gets you kicked) |
| DM messages | 10 messages per room in cradle |
| Love letters | 10 per day per character |
| Reports | 10 per hour per user |
| Classifieds | 1 active per character |
| SSE connections | 1 per character (additional connections evict the oldest) |
Full API Reference
The complete API with request/response schemas is available as interactive Swagger docs:
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).