Kraty

Chat

In-game chat as an add-on: direct messages, player groups, guild chat and public channels, with a profanity filter each player can choose, anti-spam limits and a live stream. Kraty runs the rules; your client draws the screens.

Chat lets your players talk inside your game: one-to-one, in groups they make, in their guild and in public channels you define. Kraty stores the messages, runs every rule (who may write to whom, the profanity filter, spam limits, mutes) and pushes new messages live. Your game draws the chat screens and calls the SDK.

Reporting, blocking and the review queue are part of Player safety & moderation, which works in every game, with or without chat.

The model

  • An add-on. Chat is billed separately from your plan. Kraty switches it on for your studio: contact us. Until then the Chat page explains this and every chat call answers 403 chat_disabled. Usage is billed on messages and monthly active chatters (see Usage).

  • Per game. Each game sets chat up on its own, and turns each conversation kind on or off.

  • Four kinds of conversation:

    • direct: one-to-one messages.
    • group: groups players make themselves.
    • guild: one chat per guild, automatic.
    • channel: public channels you define (global, one per language, trading, announcements).

    kind is a plain string. Treat it as open-ended: more kinds may come.

  • Environment gate. Like guilds and events, chat is disabled, staging (test keys only) or live. A new setup starts in staging.

  • Test and live are separate. Test-key players only ever see test conversations.

Setting it up

Once your studio has the add-on, open Live ops → Chat and choose Set up chat. The settings page has these sections:

SectionWhat you set
AvailabilityThe state: disabled, staging or live.
Conversation kindsEach kind on or off, its profanity filter mode, and its own rules (below).
ChannelsYour public channels (up to 50).
MessagesMost characters per message (500 by default, 2,000 at most), whether links are allowed, and your custom card types.
Profanity filterWhich built-in word lists to use (by language), your blocked and allowed words, and what happens on a hit.
Rate limitsMessages per minute, the duplicate window and the new-player delay.
Auto-muteMute a player automatically after reports from several players (see moderation).
Retention and extrasHow long messages are kept, and gift cards in direct messages.

Save, try it with a test key, then switch the state to live.

Your client reads the rules instead of hardcoding them:

const config = await kraty.chat.config();
// config.kinds.direct: { enabled, profanityFilter, allowFrom }
// config.kinds.group: { enabled, profanityFilter, maxMembers, maxGroupsPerPlayer, invitePolicy }
// config.channels: [{ key, name, description, postPolicy, slowModeSeconds, requirement, profanityFilter, metadata }]
// config.messages: { maxLength, allowLinks, customTypes }
// config.rateLimit.messagesPerMinute
// config.preferences: the player's own choices; config.status: mutes and bans in force

Direct messages

// Open (or find) the conversation with another player, by id or friend code.
const dm = await kraty.chat.openDirect({ externalPlayerId: 'player_88' });
await kraty.chat.send(dm.conversationId, 'gg, rematch?');
  • Friends only by default. The game rule allowFrom is friends (only accepted friends can write to each other) or anyone in the game.
  • Each player can narrow it. A player's own allowDirectFrom preference is everyone, friends or nobody. It can only make the game rule stricter.
  • Blocking wins. If either player blocked the other, sends are refused both ways (player_blocked), and in every conversation each player stops seeing the other's messages.
  • Every conversation carries canPost and postDenied (a code and a message), so you can grey out the input box and say why before the player types.
  • leave on a direct conversation hides it from the inbox until a new message arrives. So does setConversationSettings(id, { hidden: true }).

Groups

Players make their own groups and invite others:

const group = await kraty.chat.createGroup({
  title: 'Raid team',
  externalPlayerIds: ['player_88', 'player_90'],
});
await kraty.chat.addMembers(group.conversationId, ['player_91']);
await kraty.chat.setRole(group.conversationId, 'player_88', 'admin');
await kraty.chat.removeMember(group.conversationId, 'player_91');
await kraty.chat.updateGroup(group.conversationId, { title: 'Raid team EU' });
await kraty.chat.leave(group.conversationId);
  • Three roles: owner, admin, member. The owner and admins edit the group, add members and remove members below their own role. Only the owner changes roles; making someone owner hands the group over and the old owner becomes an admin.
  • Owner leaving: the group goes to the longest-serving admin, else the longest-serving member. When the last member leaves, the group closes.
  • Who can be added (invitePolicy): the adder's friends (default) or anyone in the game. Blocked and banned players can't be added.
  • Caps: maxMembers per group (50 by default, 100 at most) and maxGroupsPerPlayer (20 by default).

Guild chat

Every guild gets its own conversation. There is nothing to create:

const guildChat = await kraty.chat.guild(); // 409 conflict when the player has no guild
  • Members follow the roster. Joining the guild gives access; leaving or being kicked ends it.
  • Officers can delete any message in their guild's chat when officersCanDelete is on (the default).
  • System messages (systemMessages, on by default): Kraty posts a line when a member joins, leaves or is removed, when a role changes, and for help requests and donations.

Public channels

Channels are defined by you on the settings page. Anyone in the game can read them. Per channel you set:

  • key (global, es, trade) and a name and description.
  • postPolicy: players, or server_only for announcement channels where only your backend posts.
  • slowModeSeconds: the wait between two messages from the same player.
  • requirement: a minimum of a progression item (level, trophies) to post. Reading stays open. Players below it get chat_requirement_not_met with the item, threshold and their balance.
  • profanityFilter: overrides the channel kind's filter mode for this channel only.
  • metadata: anything your client needs (language, icon, region).
const channels = await kraty.chat.channels();
const global = await kraty.chat.channel('global');

Channels never appear in the inbox. Your client picks which ones to show and follows them on the live stream.

Messages

const page = await kraty.chat.messages(conversationId, { limit: 50 }); // oldest → newest
const older = await kraty.chat.messages(conversationId, { before: page.messages[0].seq });

const msg = await kraty.chat.send(conversationId, 'see you at 8', { replyToId: someMessageId });
await kraty.chat.markRead(conversationId, msg.seq);
await kraty.chat.typing(conversationId);          // "is typing…", never stored
await kraty.chat.editMessage(conversationId, msg.messageId, 'see you at 9');
await kraty.chat.deleteMessage(conversationId, msg.messageId);
  • seq is a number that goes up by one for each message in a conversation. Use it as the cursor: before pages back through history, after catches up from the last message you saw. Pages hold up to 100 messages and come back oldest first, with hasMore.
  • Retries are safe. The SDK adds a clientMessageId to each send. Sending again with the same id returns the same message instead of a copy. Pass your own if you keep an outbox.
  • Replies: replyToId points at a message in the same conversation.
  • Editing: a player edits their own messages for editWindowSeconds after sending (15 minutes by default; 0 turns editing off; the window is in chat.config().messages). The new text goes through the same filter and limits as a send, the message keeps its place, and editedAt is set. Moderators still see every earlier version, and a report keeps the text as it was when reported. Error chat_edit_not_allowed once the window has passed.
  • Deleting: a player deletes their own messages. Group owners and admins, and guild officers (when allowed), can delete others' messages. A deleted message keeps its place with deleted: true and no text.
  • Read state and unread counts: markRead never moves backwards. kraty.chat.unread() returns totals for a badge (total, per conversation and per kind). Muted conversations don't count.
  • Mute a conversation for a while with setConversationSettings(id, { mutedUntil }).

Message types

typeWritten byWhat it carries
textA player or your backendtext.
customA player or your backendA card your game defines: customType, a JSON payload, and an optional caption in text.
systemKratyThe event in customType (for example member_joined), its data in payload, and an English line in text as a fallback.

sender.kind is player, server or system. For players, sender.player has their externalPlayerId and display identity.

Custom cards (match invites, replays, shared items) are declared on the settings page: a key, a description, a size cap for the payload (4 KB at most) and, optionally, a JSON Schema the payload must match. How a card looks is up to your client.

await kraty.chat.sendCustom(conversationId, 'match_invite', { lobbyId, mode: 'duo' }, { text: 'Join me!' });

System events are member_joined, member_left, member_removed, role_changed, conversation_renamed, gift_sent, guild_help_requested and guild_help_donated. New events may be added. Render the ones you know and fall back to text for the rest.

Gifts: when a friend sends a gift and giftMessages is on (the default), a gift_sent card appears in their direct conversation, opening it if needed.

The profanity filter

Each conversation kind has a filter mode. A channel can override it.

ModeWhat players see
alwaysFiltered text for everyone. Players can't turn it off. Default for channels.
on_by_defaultFiltered, unless the player turned the filter off. Default for direct, group and guild chat.
off_by_defaultAs written, unless the player turned the filter on.

What the filter matches:

  • Kraty's built-in word lists, one per language: English, Spanish, Portuguese, French, German and Italian (filter.languages, English by default). Matching folds leetspeak, repeated letters, accents and look-alike characters, so "fuuuck", "sh1t" and "coño" are caught. Turn on the languages your players write in; an empty list leaves only your own words.
  • Your blocked words. They match whole words. A trailing * makes a prefix: noob* also catches "noobs".
  • Your allowed words, which are never treated as profanity (words in your game that the list gets wrong).

A match is masked with one * per character, so the message keeps its shape. On an always kind you can choose to refuse the message instead (onViolation: 'reject', error chat_message_rejected).

Links are masked for everyone, whatever the filter choice, unless you allow them (allowLinks).

The player's choice

Kraty keeps both what the player wrote and the masked copy, and picks one per reader. Each message has filtered: true when the text you got is the masked copy. For each reader, the choice comes from, in order:

  1. The SDK option, if your game sets it. Use it to follow your own settings toggle or parental controls:

    const kraty = new Kraty({ apiKey: '...', chat: { profanityFilter: true } });
    kraty.client.setChatFilter(false); // change it at runtime; null = not set

    The SDK sends it as the X-Kraty-Chat-Filter: on | off header on every request.

  2. The player's saved preference, stored by Kraty:

    await kraty.chat.setPreferences({ profanityFilter: false }); // null = back to the default
    const prefs = await kraty.chat.preferences(); // { profanityFilter, allowDirectFrom }
  3. The kind's default (on_by_default or off_by_default).

always ignores all three.

Spam limits

LimitDefaultError
Messages per minute, per player, across all conversations30429 chat_rate_limited
Same text twice in a conversation within the duplicate window30 seconds429 chat_duplicate_message
Channel slow modeOff (per channel)429 chat_slow_mode
New-player delay: a player first seen less than N minutes ago can't postOff403 chat_new_player

Every 429 has details.retryAfterSeconds, so you can show a countdown.

Live updates

One stream per player carries everything: every conversation in their inbox (direct, groups, guild) plus up to 10 public channels you name.

const stream = await kraty.chat.live({ channels: ['global'] });
for await (const ev of stream.events) {
  switch (ev.kind) {
    case 'message': addMessage(ev); break;           // already filtered for this player
    case 'message_edited': replaceMessage(ev); break;
    case 'message_deleted': removeMessage(ev.messageId); break;
    case 'typing': showTyping(ev.conversationId, ev.player); break;
    case 'conversation_added': refreshInbox(); break;
    case 'restriction': showMuted(ev); break;
  }
}
await stream.close();

// Or with a callback; returns a stop function.
const stop = await kraty.chat.watch((ev) => handle(ev), { channels: ['global'] });
EventWhen
readyThe stream is open: the conversations and channels it follows.
messageA new message, already resolved for this player (filter choice, blocks).
message_editedA message was edited (the full message, resolved for this player).
message_deletedA message was deleted.
typingSomeone is typing.
readA member read up to a seq.
conversation_added / conversation_updated / conversation_removedThe player joined, a group changed, or they lost access.
restriction / restriction_liftedThe player was muted or chat-banned, or that ended.

The stream does not reconnect on its own. When it drops, wait a moment, open it again, and catch up each open conversation with kraty.chat.messages(id, { after: lastSeqSeen }).

Errors

All on KratyApiError.code:

CodeMeaning
chat_disabledNo add-on, chat is disabled, or staging and this is a live key.
chat_kind_disabledThat kind is turned off in this game.
chat_mutedThe player is muted: they can read but not post (details.until, details.reason).
chat_bannedThe player is banned from chat: no reading or posting (details.until, null until lifted).
player_bannedThe account is banned.
chat_new_playerThe new-player delay hasn't passed yet.
player_blockedOne of the two blocked the other.
chat_direct_not_allowedThe game or the recipient only accepts direct messages from friends (or from nobody).
chat_post_not_allowedAn announcement channel, or a player who can no longer receive messages.
chat_requirement_not_metBelow the channel's posting requirement.
chat_permission_deniedThe player's group or guild role can't do that.
chat_group_full / chat_group_limitThe group is full, or a player is in too many groups.
chat_cannot_target_selfA direct conversation with yourself.
chat_message_invalidEmpty, too long, unknown custom type, payload too big or not matching its schema, or a bad replyToId.
chat_message_rejectedThe filter refused it (always + reject).
chat_edit_not_allowedEditing is off in this game, the edit window has passed, or it's a system message.
chat_rate_limited / chat_duplicate_message / chat_slow_modeSpam limits; wait details.retryAfterSeconds.

A player's own standing is in config.status and in kraty.moderation.status(): check canChat and canPost to show a "you are muted until…" banner instead of waiting for an error.

From your backend

The server API reads conversations (with both the original and the filtered text, deleted messages included), posts as your game and deletes messages. It works in the API key's environment.

const { conversations } = await kratyServer.chat.conversationsFor('player_42');
const convo = await kratyServer.chat.conversation(conversationId);
const { messages } = await kratyServer.chat.messages(conversationId, { after: 120 });

await kratyServer.chat.announce('news', { text: 'Double XP weekend starts now!' });
await kratyServer.chat.post(conversationId, { customType: 'match_invite', payload: { lobbyId } });
await kratyServer.chat.deleteMessage(messageId);

Messages your backend posts have sender.kind: "server". See the Node and Python server SDKs.

Webhooks: chat.message_sent (every player and server message, with the original and the filtered text, so you can run your own moderation), chat.message_flagged (the filter matched) and chat.message_deleted. chat.message_sent fires for every message, so subscribe to it only if you need it. chat.message_edited carries the new and the previous text. See Webhooks.

Push notifications

Kraty doesn't hold device tokens or talk to FCM / APNs. Instead, turn on Notifications on the chat settings page and subscribe an endpoint to chat.notification: for each message in a notifying kind (direct, group and guild by default), Kraty tells your backend who should get a push.

  • Recipients are the conversation's members, minus the sender, anyone who muted the conversation (setConversationSettings(id, { mutedUntil })), and anyone blocking or blocked by the sender. With only offline on (the default), players with live presence are left out: they see the message in the game. Presence comes from kraty.friends.heartbeat.
  • preview is the first 140 characters of the filtered text (a push lands on a lock screen), or null when previews are off.
  • Your backend maps each externalPlayerId to its device tokens and sends the push. Nothing is sent when no endpoint subscribes.
{
  "conversationId": "cnv_…",
  "conversationKind": "direct",
  "title": null,
  "messageId": "msg_…",
  "seq": 7,
  "type": "text",
  "sender": { "kind": "player", "externalPlayerId": "player_42", "name": "Ana" },
  "preview": "are you joining the raid tonight?",
  "recipients": [{ "externalPlayerId": "player_7", "playerId": "plr_…" }],
  "createdAt": "2026-10-08T10:00:00.000Z"
}

Usage and retention

The Chat page shows this month's usage: messages sent and active chatters (players who sent at least one message), live and test. Live usage is what the add-on is billed on. Its conversation browser finds conversations by kind or player and opens them.

Private messages are personal data. Only admins, debuggers and moderators can read conversations on the dashboard; QA can read test conversations only.

Retention: messages older than retentionDays (90 by default, 7 at least, or keep forever) are deleted. Reports keep their own copy of the reported messages, so deleting or expiring a message never removes evidence. A GDPR erasure blanks the player's messages.

SDK availability

The TypeScript SDK has kraty.chat and kraty.moderation, documented above and in the TypeScript SDK reference. Unity (kraty.Chat) and Flutter (kraty.chat) have the same methods, with the filter choice as a client option (ChatProfanityFilter in Unity, chatProfanityFilter in Flutter). The endpoints are in the SDK API reference.

Chat · Kraty