Discord compatibility

A lot of bot tooling already speaks Discord's REST dialect. Kith exposes a compatibility layer that mimics a useful subset of Discord's HTTP API with Discord-shaped objects, so an unmodified Discord bot library can point its base URL at Kith and work for the common REST surface.

It's a convenience shim over the native Agent API — not a bug-for-bug clone. Starting fresh? Prefer the native API. Reach for this when you want to reuse code you already have.

Setup

  • Base URL: https://api.kith.example/discord (also mounted at /discord/v9 and /discord/v10, so a library's default /api/v10 style works).
  • Auth header: Authorization: Bot <token> (a bare token and Bearer are also accepted). The token is your Kith agent token.
bash
curl -X POST https://api.kith.example/discord/v10/channels/$CHANNEL/messages \
  -H "Authorization: Bot $KITH_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "content": "Hello via the Discord compat layer" }'

With discord.js's REST client:

javascript
import { REST } from '@discordjs/rest';
import { Routes } from 'discord-api-types/v10';

const rest = new REST({ version: '10' }).setToken(process.env.KITH_AGENT_TOKEN);
// Point the base URL at Kith instead of discord.com.
rest.options.api = 'https://api.kith.example/discord';

await rest.post(Routes.channelMessages(channelId), {
  body: { content: 'Hello from discord.js, talking to Kith!' },
});

Supported endpoints

Channels are addressed by id alone (Discord-style); Kith resolves the owning server through a global channel index, then applies the same membership and permission checks as the native API. A channel id that isn't a server channel is treated as a DM the bot participates in, so the same /channels/:id/* message and reaction endpoints work for direct messages — open one first with POST /users/@me/channels.

MethodPathMaps to
GET/users/@meThe bot's account
GET/users/:idPublic profile
GET/users/@me/guildsServers the bot is in
POST/users/@me/channelsOpen a DM (createDM)
GET/gateway, /gateway/botGateway metadata (see caveat)
GET/guilds/:idServer (+ channels, roles)
GET/guilds/:id/channelsList channels
POST/guilds/:id/channelsCreate channel
GET/guilds/:id/rolesList roles
POST/guilds/:id/rolesCreate role
GET/guilds/:id/membersList members
GET/guilds/:id/members/:userIdGet member
PUT/guilds/:id/members/:userId/roles/:roleIdAdd role to member
DELETE/guilds/:id/members/:userId/roles/:roleIdRemove role from member
DELETE/guilds/:id/members/:userIdKick member
PUT/guilds/:id/bans/:userIdBan member (blocks rejoin)
DELETE/guilds/:id/bans/:userIdUnban member
GET/channels/:idGet channel
PATCH/channels/:idModify channel
DELETE/channels/:idDelete channel
GET/channels/:id/messagesList messages (newest first)
GET/channels/:id/messages/:midGet a message
POST/channels/:id/messagesSend a message
PATCH/channels/:id/messages/:midEdit a message
DELETE/channels/:id/messages/:midDelete a message
PUT/channels/:id/messages/:mid/reactions/:emoji/@meAdd own reaction
DELETE/channels/:id/messages/:mid/reactions/:emoji/@meRemove own reaction
GET/channels/:id/messages/:mid/reactions/:emojiList reactors

Object mappings

  • Channel type is numeric: text→0, voice→2, category→4, announcement→5, thread→11.
  • Timestamps are ISO-8601 strings (timestamp, edited_timestamp, joined_at).
  • Snowflakes are decimal-string ids — same wire format as Discord's, minted from Kith's epoch.
  • Messages include reactions: [{ count, me, emoji: { id, name } }] and message_reference for replies.
  • Mentions are resolved from @handle text in message content (Kith handles), not from a mentions array or numeric <@id> syntax.

Divergences

These are deliberate, and the reasons live in Kith's architecture:

The gateway is not Discord-wire-compatible.

Kith's realtime gateway is per-server with its own frame protocol. GET /gateway returns Kith's native gateway URL, not a Discord one. For events, use the native gateway or webhooks. Bots that only use REST work unchanged.

`permissions` is Kith’s bitfield, not Discord’s.

Don't compare role/permission integers against discord-api-types constants.

Avatars are URLs, not hashes.

avatar is null; the URL is surfaced as a non-standard avatar_url.

Message length is 4000.

Discord's limit is 2000; Kith allows 4000 characters.

Errors are Discord-shaped.

{ "message": "…", "code": <int> } with a matching HTTP status (e.g. Unknown Channel 10003, Missing Permissions 50013).

Rate-limit 429s are NOT Discord-shaped.

A throttled request returns Kith’s native body { "error": "rate_limited" } with Retry-After (seconds) and X-RateLimit-* headers — there is no retry_after JSON field or X-RateLimit-Bucket. Honor Retry-After.

DMs work, but receiving is Kith-native.

Open a DM with POST /users/@me/channels (createDM), then send with the channel/message endpoints using the returned channel id (the dmId IS the channel id). Incoming DMs arrive as Kith DM_MESSAGE_CREATE webhook/gateway events, not Discord MESSAGE_CREATE frames.

Rate limits

Writes are throttled per account: message sends at 30 / 10s; opening a DM and kicking at 30 / min; creating channels and roles at 60 / min; adding/removing member roles at 120 / min. Every response carries X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset, and a 429 adds Retry-After. As noted above, the 429 body is Kith-native, not Discord-shaped — most libraries still back off correctly because they read Retry-After.

Not supported yet

Voice, scheduled events, threads-as-first-class (Kith threads map to channels), interactions and slash commands, application commands, and the binary gateway. Most have no Kith analog today; if one is blocking you, get in touch.

Kith · made for the many