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).
kindis 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) orlive. A new setup starts instaging. -
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:
| Section | What you set |
|---|---|
| Availability | The state: disabled, staging or live. |
| Conversation kinds | Each kind on or off, its profanity filter mode, and its own rules (below). |
| Channels | Your public channels (up to 50). |
| Messages | Most characters per message (500 by default, 2,000 at most), whether links are allowed, and your custom card types. |
| Profanity filter | Which built-in word lists to use (by language), your blocked and allowed words, and what happens on a hit. |
| Rate limits | Messages per minute, the duplicate window and the new-player delay. |
| Auto-mute | Mute a player automatically after reports from several players (see moderation). |
| Retention and extras | How 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 forceDirect 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
allowFromisfriends(only accepted friends can write to each other) oranyonein the game. - Each player can narrow it. A player's own
allowDirectFrompreference iseveryone,friendsornobody. 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
canPostandpostDenied(a code and a message), so you can grey out the input box and say why before the player types. leaveon a direct conversation hides it from the inbox until a new message arrives. So doessetConversationSettings(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
ownerhands 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:
maxMembersper group (50 by default, 100 at most) andmaxGroupsPerPlayer(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
officersCanDeleteis 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, orserver_onlyfor 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 getchat_requirement_not_metwith 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);seqis a number that goes up by one for each message in a conversation. Use it as the cursor:beforepages back through history,aftercatches up from the last message you saw. Pages hold up to 100 messages and come back oldest first, withhasMore.- Retries are safe. The SDK adds a
clientMessageIdto 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:
replyToIdpoints at a message in the same conversation. - Editing: a player edits their own messages for
editWindowSecondsafter sending (15 minutes by default;0turns editing off; the window is inchat.config().messages). The new text goes through the same filter and limits as a send, the message keeps its place, andeditedAtis set. Moderators still see every earlier version, and a report keeps the text as it was when reported. Errorchat_edit_not_allowedonce 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: trueand no text. - Read state and unread counts:
markReadnever 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
type | Written by | What it carries |
|---|---|---|
text | A player or your backend | text. |
custom | A player or your backend | A card your game defines: customType, a JSON payload, and an optional caption in text. |
system | Kraty | The 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.
| Mode | What players see |
|---|---|
always | Filtered text for everyone. Players can't turn it off. Default for channels. |
on_by_default | Filtered, unless the player turned the filter off. Default for direct, group and guild chat. |
off_by_default | As 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:
-
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 setThe SDK sends it as the
X-Kraty-Chat-Filter: on | offheader on every request. -
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 } -
The kind's default (
on_by_defaultoroff_by_default).
always ignores all three.
Spam limits
| Limit | Default | Error |
|---|---|---|
| Messages per minute, per player, across all conversations | 30 | 429 chat_rate_limited |
| Same text twice in a conversation within the duplicate window | 30 seconds | 429 chat_duplicate_message |
| Channel slow mode | Off (per channel) | 429 chat_slow_mode |
| New-player delay: a player first seen less than N minutes ago can't post | Off | 403 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'] });| Event | When |
|---|---|
ready | The stream is open: the conversations and channels it follows. |
message | A new message, already resolved for this player (filter choice, blocks). |
message_edited | A message was edited (the full message, resolved for this player). |
message_deleted | A message was deleted. |
typing | Someone is typing. |
read | A member read up to a seq. |
conversation_added / conversation_updated / conversation_removed | The player joined, a group changed, or they lost access. |
restriction / restriction_lifted | The 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:
| Code | Meaning |
|---|---|
chat_disabled | No add-on, chat is disabled, or staging and this is a live key. |
chat_kind_disabled | That kind is turned off in this game. |
chat_muted | The player is muted: they can read but not post (details.until, details.reason). |
chat_banned | The player is banned from chat: no reading or posting (details.until, null until lifted). |
player_banned | The account is banned. |
chat_new_player | The new-player delay hasn't passed yet. |
player_blocked | One of the two blocked the other. |
chat_direct_not_allowed | The game or the recipient only accepts direct messages from friends (or from nobody). |
chat_post_not_allowed | An announcement channel, or a player who can no longer receive messages. |
chat_requirement_not_met | Below the channel's posting requirement. |
chat_permission_denied | The player's group or guild role can't do that. |
chat_group_full / chat_group_limit | The group is full, or a player is in too many groups. |
chat_cannot_target_self | A direct conversation with yourself. |
chat_message_invalid | Empty, too long, unknown custom type, payload too big or not matching its schema, or a bad replyToId. |
chat_message_rejected | The filter refused it (always + reject). |
chat_edit_not_allowed | Editing is off in this game, the edit window has passed, or it's a system message. |
chat_rate_limited / chat_duplicate_message / chat_slow_mode | Spam 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 fromkraty.friends.heartbeat. previewis 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
externalPlayerIdto 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.