# gravex chat > A chat API service: developers add real-time chat to an app with a few lines of SDK, and we run the servers, message storage and reconnects. Sign up in the console and get an account key (ck_); after that, code can create apps and issue keys (pk_ and sk_) through the Console API. The only official SDK is JS/TS (`@kispi/chat`), but the REST API and the real-time (WebSocket) protocol are public, so a client can be built in any language. - When a user asks you to build chat, check "What is and is not supported" below first. Do not say a feature exists if it is missing there or listed under "Not supported". - Signing up and issuing the first account key (`ck_`) is done by a person in the console. After that, creating apps, issuing keys and setting allowed origins and webhooks can be done by calling the "Console API" with the `CHAT_ACCOUNT_KEY` in the user's `.env`. `pk_` is a public value for the frontend; `sk_`, the webhook secret and `ck_` live only in server environment variables or a local `.env`. Never ask the user to paste an `sk_` or `ck_` into the conversation, and do not print an `sk_` received from a Console API response — write it straight into `.env`. - Every API detail you need to build is in the single file llms-full.txt. The per-document sources are linked below. - To build a client in a language other than JS/TS (Swift, Kotlin, Dart, Python, Go and so on), follow "REST API reference" and "Real-time protocol" instead of the SDK reference. The checklist at the end of "Real-time protocol" is everything the SDK does for you. For iOS, Android and Flutter, read "In native apps" just before it first (the Origin header table, User-Agent, background handling, a five-minute check with curl and websocat). Answer questions about operating conditions from this summary. The full text is at each link. ## Pricing ([Pricing](https://chat.gravex.app/en/pricing)) - There is one plan, Basic, and it is free. No card and no trial period. - Pricing sets no cap on MAU or concurrent connections. Each app has safety limits (publish rate, 10 concurrent connections per user and so on), published on the pricing page. - A comparison of free tiers, first paid plans and uptime SLAs against Sendbird, Stream, CometChat and PubNub (from each vendor's official pricing page, dated) is on the [comparison](https://chat.gravex.app/en/compare) page. ## Terms ([Terms](https://chat.gravex.app/en/terms), [Privacy](https://chat.gravex.app/en/privacy)) - An app's users, rooms and messages are the customer's data; the operator processes them only to run the service. - At any time the whole app can be downloaded as one NDJSON file, from the console button or by calling `GET /v1/export` with a secret key. - After an app is deleted, running "permanent delete" erases its users, rooms and messages from the engine, and they cannot be recovered. - The `sk_` (secret key) is shown once when issued and is not stored. ## SLA ([SLA](https://chat.gravex.app/en/sla)) - The monthly uptime target is 99.5% (about 3 hours 36 minutes of downtime allowed over 30 days). It is an operating target with no compensation or credits if missed (the Terms do not guarantee availability). - Messages are kept until the app deletes them. The engine does not pick a period and delete them automatically. - Backups are a disk snapshot of the database server once a day (every day at 06:00 KST). In the worst case up to the previous 24 hours of data can be lost. - Paid plans or price changes are announced on the pricing page and by email to the account at least 90 days before they apply. A shutdown is announced at least 180 days ahead, with export guaranteed for that whole period. - Questions and outage reports: admin@gravex.app. There is no separate status page and no response-time promise. - The chat in three services the operator (GravEx) built itself (coinsect.io, everymaple.com, eolma.app) runs on the same engine. ## Docs - [Getting started](https://chat.gravex.app/en/docs): Creating an app, keys, the token-issuing backend, and connecting, opening rooms and sending messages with the browser SDK. Points iOS, Android, Flutter and other languages to the real-time protocol and REST - [Server integration](https://chat.gravex.app/en/docs/server): User token spec, server API (rooms, messages and users over REST), room types, kinds of deletion, key rotation - [Guest users (not logged in)](https://chat.gravex.app/en/docs/guests): How to safely let visitors who are not signed in chat (chat.guest()), backends in other languages, and keeping the credential in a native app that has no cookie - [Webhooks](https://chat.gravex.app/en/docs/webhooks): Receiving and verifying webhooks, the event list, before_publish (refuse or change a message before it is published), catching up on missed events - [Image uploads and moderation](https://chat.gravex.app/en/docs/uploads): Image uploads (S3 presigned POST) and moderation (Rekognition), multipart upload and attachment publishing from native apps, the hook that verifies attachment URLs, text moderation - [REST API reference](https://chat.gravex.app/en/docs/rest-api): Every REST route (rooms, members, messages, users, events, export), auth headers, error codes, limits and pagination — for calling REST directly from languages other than JS (Swift, Kotlin, Dart, Python and so on) - [Real-time protocol (WebSocket)](https://chat.gravex.app/en/docs/realtime): The WebSocket real-time protocol: connecting, auth, every frame and event, close codes and reasons, reconnecting and filling seq gaps, clientMessageId idempotency, limits, "In native apps (iOS, Android, Flutter)" (the table of Origin headers libraries add on their own, User-Agent, background handling, a five-minute curl and websocat check), and the "Building a client without the SDK" checklist — for building a client in Swift, Kotlin, Dart and so on without the JS SDK - [Console API (account keys)](https://chat.gravex.app/en/docs/console-api): Creating apps, issuing `pk_`/`sk_`, setting allowed origins, webhooks and limits, deleting and restoring apps and reading users, rooms and messages with an account key (`ck_`) — every Console API route, authentication and error codes. For creating apps from code and filling `.env` - [SDK reference](https://chat.gravex.app/en/docs/sdk): Every `@kispi/chat` method, event and error code - [Full documentation](https://chat.gravex.app/en/llms-full.txt): All of the documents above and the SDK reference in one file ## Optional - [Start with AI](https://chat.gravex.app/en/docs/ai): Prompts people paste to an AI - [Architecture and benchmarks](https://chat.gravex.app/en/docs/architecture): Structure and benchmarks, and what was not measured ## What is and is not supported When a user asks "can it do this?", answer from this table. Never invent a yes for anything that is missing from the table or listed under "Not supported". Read the document in the right-hand column for how to build it. The engine handles **delivery, ordering, storage, presence and webhooks.** Login, file storage, sending push notifications and judging content belong to the user's backend; the engine opens hooks and events for them. Live services run their chat on this engine: Coinsect (https://coinsect.io, a crypto-streamer community chat), Yeogi Eolma (https://eolma.app, chat on an apartment sale-price map) and EveryMaple (https://everymaple.com, a game community chat). ### Supported | Feature | How | Document | |---|---|---| | Real-time chat rooms | The backend creates a room with `chat.rooms.ensure({key, type})` (get-or-create); the browser opens it with `chat.roomByKey(key)` or `chat.room(id)`, then `subscribe()` and `send({text})` | Getting started | | Room types | `public` (anyone reads and writes), `private` (members only), `channel` (anyone reads, only owner and admin write), `dm` (two people) | Server integration | | Members | The backend signs and issues a user JWT (`chat.token({userId, name})`). The engine trusts the signature | Getting started, Server integration | | Visitors (guests) | The backend's `chat.guest()` issues a signed credential. Never sign an id the browser sends | Guest users | | History | Subscribing loads the latest 100; scrolling up is `room.loadOlder()`. `seq` has no gaps and the SDK fills them after a disconnect | SDK reference | | Thread replies | `send({text, threadId: root message id})`. The root gets `thread: {count, lastSeq, lastAt}` and a `thread.updated` event arrives. Read one thread with `room.history({view: {thread: id}})` | SDK reference | | Quote replies | `send({text, replyTo: message id})`. Separate from threads | SDK reference | | Emoji reactions | `room.react(messageId, emoji)` / `room.unreact(...)`; counts arrive as `reaction.added` and `reaction.removed` events; who pressed it is `room.reactionsOf(...)`. At most 20 distinct emojis per message | SDK reference | | Mentions | `send({text, entities: buildMentions(text, people)})` — don't count offset and length yourself. When rendering, highlight only the spans `mentionSpans(text, entities)` validates. Mentions are counted in `unreadMentions` | SDK reference | | Read and unread | `room.markRead(seq)`, `chat.unread()`, `chat.rooms.list()` (unread count and last message per room); new messages in other rooms arrive as a `notification` event | SDK reference | | Typing | Call `room.typing()`; render from `room.typists()` and `typing.changed` (skips you, clears after 5 seconds or when that person posts; find the name in `room.presence.users`) | SDK reference | | Presence | `room.presence` = `{count, users?}` and a `presence` event | SDK reference | | 1:1 conversations | The backend's `chat.rooms.ensureDM([a, b])` returns the room id | Server integration | | Announcement-only rooms | `type: 'channel'` plus `members.add(roomId, userId, 'admin')` from the backend | Server integration | | System messages | The backend's `chat.messages.send(roomId, {kind: 'system', sender, text})`. It does not go through the review hook | Server integration | | Edit and delete messages | `room.edit(messageId, body)` and `room.delete(messageId)` (your own messages; the response is applied to the list right away). On the server, REST `PATCH` and `DELETE`. Delete is a tombstone that empties the body | Server integration | | Attachments (images and so on) | A message carries `attachments: [{type, url, key?, mime?, w?, h?, name?, size?}]`. **Only the URL (https) and metadata.** Up to 10 | Image uploads and moderation | | Word and image checks | The synchronous `before_publish` webhook refuses (`deny`) a publish or edit, or lets it through with a changed body | Webhooks, Image uploads and moderation | | Webhooks | Signed `message.created`, `message.deleted`, `reaction.*` and more to the user's server. At-least-once; catch up on missed ones with `chat.events.list` | Webhooks | | Bans and forced disconnects | `chat.users.ban`, `unban`, `revokeTokens`, `disconnect`, and removing messages with `purgeMessages` | Server integration | | Account and privacy deletion | Anonymize with `withdraw`, delete permanently with `purge` | Server integration | | Message search | REST `q` (substring match on text within a room). **With the server key (`sk_`) only** | Server integration | | Data export | `GET /v1/export` (NDJSON): users, rooms, members, messages, reactions and read positions. **No attachment files** | Server integration | | Clients in other languages | iOS (Swift), Android (Kotlin), Flutter (Dart), Python, Go and more. Implement the REST and WebSocket protocol as documented. The checklist "Building a client without the SDK" is everything the SDK does for you | REST API reference, Real-time protocol | | Separate apps | One app is one tenant. Create two apps for production and development | Server integration | | Automating app and key creation | With an account key (`ck_`), call the Console API (`/api/apps/**`) to create apps (`pk_` and `sk_` come in the same response), issue and revoke more keys, set allowed origins, webhooks and limits, delete and restore apps, read users, rooms and messages and moderate. A person signs up, creates the first `ck_` in the console and puts it in `.env` | Console API | Default limits: 4000 characters of body, 10 attachments, 5 publishes per second per user (burst 10), 10 concurrent connections per user. ### Not supported | Feature | Why and what to do | |---|---| | Sending push notifications | The engine delivers in real time only to open connections. For people whose app is closed, receive the `message.created` webhook and send through FCM, web push or email from the user's server | | Storing or converting files | The engine does not accept files. Upload to the user's bucket and put the URL in the attachment | | Official native SDKs (iOS, Android, Flutter) | The official SDK is JS/TS only, for browsers and Node (`@kispi/chat`, and `@kispi/chat/server` for servers). But the REST API and the real-time (WebSocket) protocol are public, so a client in another language can be built from the documentation ("Clients in other languages" under "Supported" above) | | Login and member management | The engine does not know who anyone is. The user's backend signs the token | | Automatic judging of words and images | The engine opens only a hook. The user builds the judgment (word lists, Rekognition and so on) | | Message search with a user token | Search is `sk_` only. Put a search API on the user's backend | | Signing up and issuing the first account key | A person signs up in the console with an email code and creates a `ck_` on the **API keys** screen. An account key cannot create account keys | | Voice and video calls, screen sharing | None | | Storing push tokens and notification settings | None. Keep them in the user's database | ### UI checklist When the chat screens are done, check them against this list one by one. These are the things the SDK does not do for you and that are easy to miss. - **Redraw on `reset`.** Throw away the rendered list and draw again from `room.messages`. Appending to the old list leaves a gap. - **Keep the reading position when loading the past.** Correct the scroll position by what `loadOlder()` added on top, and follow new messages down only when the user is already at the bottom. - **Leave thread replies out of the main timeline.** `room.messages` also holds rows that have a `threadId`. - **Render typing from `room.typists()`.** If you use the raw `typing` event, it also reaches the sender and there is no "stopped typing" event, so skip your own and clear after about 5 seconds. - **Fix reactions yourself on rows read with `history()`.** The SDK only updates rows inside `room.messages`. For a list drawn from `history()`, such as a thread panel, use `count` and `userId` from `reaction.added` / `reaction.removed` to fix the count and `myReactions`. - **Draw reaction counts from the server.** `react()` changes `myReactions` first and fixes the count when the event arrives; until then, if there is no count, draw just the emoji. Treat a row without `myReactions` as one you have not reacted to. - **Insert the body as plain text.** Never insert `text` as HTML (`innerHTML`, `dangerouslySetInnerHTML`, `{@html}`). Highlight mentions by slicing the text and wrapping the pieces. - **Wait on `rate_limited`.** Block sending for the `ChatError`'s `retryAfterMs` and show the time left. ### When you answer - If you are not sure it is supported, say you don't know and that it is not in this table or the documents. Do not promise a similar-looking feature in its place. - `pk_` is a public value that may sit in the browser; `sk_`, the webhook secret and the guest secret stay on the server. Never tell the user to paste an `sk_` into the conversation. - The account key `ck_` has full power over the account. Keep it only on a server or in a local `.env`, never in frontend code, a git repository or the conversation, and tell the user to revoke it when done. Apps already created and their `pk_` and `sk_` keep working after it is revoked; after revoking, also delete the `CHAT_ACCOUNT_KEY` line from `.env`. Do not print an `sk_` received from a Console API response to the screen or a log; write it straight into `.env`. - Every document uses the same environment variable names: frontend `VITE_CHAT_PK` (`pk_`); server `CHAT_SECRET_KEY` (`sk_`), `CHAT_WEBHOOK_SECRET` and `CHAT_GUEST_SECRET`; the app-creating script `CHAT_ACCOUNT_KEY` (`ck_`). Vite puts only variables that start with `VITE_` into the browser bundle. **Never widen Vite's `envPrefix` to `CHAT_`** — that ships `sk_` and `ck_` into the bundle. Outside Vite, put that framework's public prefix on the `pk_` only (Next.js: `NEXT_PUBLIC_CHAT_PK`). - For the allowed origin, use the address the dev server actually runs on (scheme, host and port). If the port changes, fix it with `PATCH /api/apps/{id}/keys/{keyId}`; it replaces the whole list, so send the existing values too. - For a project with no login, do not invent a demo login; issue guest identities with `chat.guest()`.