# RTLayer documentation > RTLayer delivers realtime messages from your servers to your users. Publish with one HTTPS request; every connected web or mobile client receives it over a websocket within milliseconds. No socket servers to build, scale or babysit. Web version: https://rtlayer.com/docs ## How RTLayer works RTLayer sits between your backend and your users’ apps. Your server sends a message to the **HTTP API**; RTLayer fans it out to every client connected to the same **service** over a **websocket**. Clients never publish: anything they need to send goes through your own backend, which keeps you in control of what is broadcast. _flow_ ``` Your backend ──POST /message──▶ api.rtlayer.com ──▶ RTLayer ──websocket──▶ web, iOS, Android clients (API key, secret) (org ID + service ID, public) ``` - **HTTP API**: `https://api.rtlayer.com`. Publish messages and create services. Authenticated with an API key. - **Websocket**: `wss://ws.rtlayer.com///`. Clients connect, join channels and receive messages. - **Dashboard**: [rtlayer.com/dashboard](https://rtlayer.com/dashboard). Create services, manage API keys and teammates, try messages in the Playground and watch live analytics. > **Note:** Servers run in Mumbai, India (asia-south1). From India a message typically reaches clients about 50 ms after you publish it. ## Quickstart 1. [Sign in](https://rtlayer.com/auth). The first visit to [Home](https://rtlayer.com/) creates an organization, a service and an API key for you. 2. Open your service on the [Dashboard](https://rtlayer.com/dashboard) and go to **API Keys**. Copy the **Organization ID**, the **Service ID** and an **API key**. 3. Connect a client and listen on a channel (step 3 below). 4. Publish to that channel from your server (step 4 below). 5. Watch it arrive. The service’s **Playground** tab can also send and receive test messages, with timings. ### 3. Receive in your app _terminal_ ```bash npm i rtlayer-client ``` _app.js_ ```js import RTLayer from "rtlayer-client"; // Org ID and service ID are public: they are safe in frontend code const rtlayer = RTLayer("YOUR_ORG_ID", "YOUR_SERVICE_ID"); // Joins the "orders" channel and receives every message published to it rtlayer.on("orders", (message) => { console.log("Live:", JSON.parse(message)); }); ``` ### 4. Publish from your server **cURL** _terminal_ ```bash curl -X POST https://api.rtlayer.com/message \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"channel": "orders", "message": {"id": 1042, "status": "shipped"}}' ``` **Node.js** _server.js_ ```js // Node 18+, no packages needed const res = await fetch("https://api.rtlayer.com/message", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ channel: "orders", message: { id: 1042, status: "shipped" } }), }); console.log(await res.json()); // { status: "ok" } ``` **Python** _server.py_ ```python import requests res = requests.post( "https://api.rtlayer.com/message", headers={"x-api-key": "YOUR_API_KEY"}, json={"channel": "orders", "message": {"id": 1042, "status": "shipped"}}, ) print(res.json()) # {'status': 'ok'} ``` > **Keep API keys on your server:** An API key can publish to your service. Never ship it in frontend or mobile code, and prefer the `x-api-key` header over the `apiKey` query parameter so it stays out of request logs. ## Core concepts | Concept | What it is | | --- | --- | | **Organization** | Your team’s workspace. It holds services, teammates (owner, admins, members) and org-level API keys. | | **Service** | One realtime app or environment, such as "Production chat" or "Staging". Each has its own ID, API keys, settings and analytics. Messages never cross services. | | **Org ID and service ID** | Public identifiers. Clients need both to connect, so they are safe in frontend code. | | **API key** | Secret credential for the HTTP API. A **service key** publishes to one service; an **org key** can publish to any service in the org and create services. See [API keys](https://rtlayer.com/docs#api-keys). | | **Channel** | A named stream inside a service, such as `orders` or `room-42`. Clients join the channels they care about; a message published to a channel reaches only those clients. | | **Broadcast** | A message published **without** a channel. It reaches every client connected to the service. | | **Personal channel** | Every connection is also subscribed to a channel named after its user ID (`uid`). Publishing to that name reaches one user. Useful with [authentication](https://rtlayer.com/docs#authentication), which sets the uid. | | **Message** | Any string or JSON value. Strings are delivered as-is; other values are sent as their JSON text, so clients `JSON.parse` them. | | **TTL** | Optional seconds to keep a message for a channel nobody is listening to yet. See [Delivery and TTL](https://rtlayer.com/docs#delivery). | ## Publish messages _endpoint_ ``` POST https://api.rtlayer.com/message ``` | Header | Value | | --- | --- | | `x-api-key` | Your service API key (or an org key; see below). The `apiKey` query parameter also works, but the header wins and keeps the key out of logs. | | `Content-Type` | `application/json` (form-encoded bodies work too) | | Body field | Type | Description | | --- | --- | --- | | `message` | any, **required** | The payload. A string is delivered as-is; objects, arrays, numbers and booleans are delivered as their JSON text. Up to 8 MB, though small messages deliver fastest. | | `channel` | string, optional | Channel to publish to. Omit it (or send `""`) to broadcast to every client of the service. Any characters are allowed; colons are fine. | | `ttl` | integer seconds, optional | Keep the message for up to this long if **no client is subscribed** to the channel right now. Default 0 (deliver now or drop). See [Delivery and TTL](https://rtlayer.com/docs#delivery). | | `serviceId` | string | Only with an **org** API key: the service to publish to. Ignored for service keys. | A successful call returns `200` with `{"status":"ok"}`. The response is the same whether the message reached clients, was stored for later, or found nobody listening; it is not a delivery receipt. > **Tip:** Every request is authenticated and rate limited per service (see [Limits](https://rtlayer.com/docs#limits)). For bursts, batch several updates into one message rather than sending one request per update. ### Examples **Broadcast** _terminal_ ```bash # No channel: every connected client of the service receives it curl -X POST https://api.rtlayer.com/message \ -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"message": "Deploy finished"}' ``` **Channel** _terminal_ ```bash curl -X POST https://api.rtlayer.com/message \ -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"channel": "room-42", "message": {"from": "Priya", "text": "Hi!"}}' ``` **One user** _terminal_ ```bash # The personal channel is the user's uid (from your auth token) curl -X POST https://api.rtlayer.com/message \ -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"channel": "user-123", "message": {"type": "notification", "unread": 3}}' ``` **With TTL** _terminal_ ```bash # Kept for up to 60 s if nobody has joined "order-1042" yet curl -X POST https://api.rtlayer.com/message \ -H "x-api-key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"channel": "order-1042", "ttl": 60, "message": {"status": "shipped"}}' ``` ### Node.js SDK (optional) The `rtlayer-node` package wraps the same request. A plain `fetch` (shown above) is just as short and also supports org keys, which the SDK can’t send `serviceId` for. _server.js_ ```js // npm i rtlayer-node const RTLayer = require("rtlayer-node").default; const rtlayer = new RTLayer("YOUR_API_KEY"); const result = await rtlayer.message("Order 1042 shipped", { channel: "orders", ttl: 60 }); // The SDK returns errors instead of throwing them: check the result if (result?.status !== "ok") console.error("RTLayer publish failed", result?.response?.data ?? result); ``` ### GET /message For quick tests, `GET /message?apiKey=…&message=…&channel=…` publishes too (so does `HEAD`). Avoid it in production: the key and message end up in URLs and logs, and anything that prefetches the URL sends a message. ## Receive messages (JavaScript) _terminal_ ```bash npm i rtlayer-client ``` `RTLayer(orgId, serviceId, token?, retryInterval?)` returns a client that connects at once and reconnects every `retryInterval` ms (default 5000) when the connection drops, re-joining your channels. It needs a global `WebSocket`: browsers, React Native and Node 22+ have one. _app.js_ ```js import RTLayer from "rtlayer-client"; const rtlayer = RTLayer("YOUR_ORG_ID", "YOUR_SERVICE_ID"); // on(channel) joins the channel; the handler gets each message as a string const orders = rtlayer.on("orders", (message) => { const order = JSON.parse(message); // published objects arrive as JSON text console.log(order.status); }); // Stop listening (leaves the channel when its last handler is removed) orders.remove(); ``` ### Client API | Call | What it does | | --- | --- | | `RTLayer(orgId, serviceId, token?, retryInterval = 5000)` | Returns the client for that service. Calling it again with the same IDs returns the same client. | | `on(channel, handler)` | Joins `channel` and calls `handler(message)` for each message on it. Returns `{ remove() }`. | | `on("*", handler)` | Every frame the connection receives, without joining anything. See the table below for what it gets. | | `removeListener(channel, handler)` | Removes a handler; leaves the channel once no handlers remain. | | `subscribe(channel)` / `unsubscribe(channel)` | Join or leave a channel without a handler (re-joined after reconnects). | | `close()` | Closes the socket. Note: the client then reconnects after `retryInterval`; reload or navigate away to stop it for good. | | Handler | Channel message | Broadcast | System frames | | --- | --- | --- | --- | | `on("orders")` | the payload string | — | — | | `on("*")` | the payload string (no channel name) | the raw frame: `{"channel":"","message":"…"}` | raw: `{"status":"connected"}` etc. | > **Warning:** Avoid `on("message")`, `on("open")` and `on("close")`: besides the event, they also join channels literally named "message", "open" and "close". Use `on("*")` for everything, and named channels for your data. ### Broadcasts Broadcasts (messages published without a channel) reach `*` handlers as the raw frame. Unwrap them like this: _app.js_ ```js rtlayer.on("*", (data) => { let frame; try { frame = JSON.parse(data); } catch { return; } // Broadcast frame: {"channel": "", "message": ""} if (frame && frame.channel === "" && typeof frame.message === "string") { console.log("Broadcast:", frame.message); } }); ``` ### React _useOrders.js_ ```js "use client"; // Next.js App Router: websockets only run in the browser import { useEffect, useState } from "react"; import RTLayer from "rtlayer-client"; export function useOrders() { const [orders, setOrders] = useState([]); useEffect(() => { const listener = RTLayer("YOUR_ORG_ID", "YOUR_SERVICE_ID").on("orders", (message) => { setOrders((list) => [JSON.parse(message), ...list]); }); return () => listener.remove(); // leave the channel on unmount }, []); return orders; } ``` ## Websocket protocol (any language) The SDK is a thin layer over a small JSON protocol, so any language with a websocket client works. | Step | Details | | --- | --- | | Connect | `wss://ws.rtlayer.com///`, plus `?token=` when the service requires [authentication](https://rtlayer.com/docs#authentication). Servers may send `Authorization: Bearer ` instead. | | On connect | The server sends `{"status":"connected"}`. You are already subscribed to broadcasts and to your personal channel (your uid). | | Join a channel | Send `{"action":"join","id":"orders"}`. | | Leave a channel | Send `{"action":"leave","id":"orders"}`. | | Receive | Each message arrives as `{"channel":"orders","message":""}`. `channel` is `""` for broadcasts; `message` is always a string (JSON text for published objects). | | Errors | `{"status":"error","message":"Invalid JSON"}` for frames the server can’t read; `{"status":"error","message":"You are not allowed to join "}` when your token doesn’t allow the channel. The connection stays open. | | Keep-alive | The server pings every connection and closes it after 30 s without a pong. Browsers and standard libraries answer pings automatically. | | Reconnect | If the socket closes, reconnect and send your joins again; channel membership doesn’t survive a reconnect. | ### Examples **Browser** _app.js_ ```js const ws = new WebSocket("wss://ws.rtlayer.com/YOUR_ORG_ID/YOUR_SERVICE_ID/"); ws.onopen = () => ws.send(JSON.stringify({ action: "join", id: "orders" })); ws.onmessage = (event) => { const frame = JSON.parse(event.data); if (frame.status) return; // {"status":"connected"} or an error frame console.log(frame.channel || "(broadcast)", frame.message); }; ``` **Python** _listen.py_ ```python # pip install websockets import asyncio, json, websockets async def main(): url = "wss://ws.rtlayer.com/YOUR_ORG_ID/YOUR_SERVICE_ID/" async with websockets.connect(url) as ws: await ws.send(json.dumps({"action": "join", "id": "orders"})) async for raw in ws: frame = json.loads(raw) if "status" in frame: # connected / error frames continue print(frame["channel"] or "(broadcast)", frame["message"]) asyncio.run(main()) ``` **wscat** _terminal_ ```bash npx wscat -c "wss://ws.rtlayer.com/YOUR_ORG_ID/YOUR_SERVICE_ID/" # then type: {"action":"join","id":"orders"} ``` ## Authenticate clients By default anyone who knows your org and service IDs can connect and join any channel. That’s fine for public data. For anything private, turn on authentication: clients must then present a JWT that your server signs with the service’s secret. 1. In the dashboard, open the service’s **Settings** tab, switch on **Require authentication** and set a long random secret (owners and admins only). 2. On your server, after you have authenticated the user, sign a token with that secret (HS256) carrying the claims below. 3. Give the token to the client, which connects with it. | Claim | Required | Meaning | | --- | --- | --- | | `orgId` | yes | Must equal the org ID in the connection URL. | | `serviceId` | yes | Must equal the service ID in the connection URL. | | `uid` | yes | Your user’s ID. It becomes the connection’s personal channel. | | `room.allowed` | yes, in practice | Channel patterns this user may join, e.g. `["orders", "room-*"]`. `*` matches anything. **If it’s missing, every join is refused.** | | `room.denied` | no | Patterns to refuse even if allowed; deny wins. | | `exp` | recommended | Expiry (standard JWT claim). Expired tokens are rejected. | **Node.js** _token.js_ ```js // npm i jsonwebtoken const jwt = require("jsonwebtoken"); function rtlayerToken(user) { return jwt.sign( { orgId: "YOUR_ORG_ID", serviceId: "YOUR_SERVICE_ID", uid: user.id, room: { allowed: ["orders", `team-${user.teamId}-*`] }, }, process.env.RTLAYER_SECRET, // the secret from the Settings tab { expiresIn: "12h" } // HS256 by default ); } ``` **Python** _token.py_ ```python # pip install pyjwt import os, time, jwt def rtlayer_token(user): return jwt.encode( { "orgId": "YOUR_ORG_ID", "serviceId": "YOUR_SERVICE_ID", "uid": str(user.id), "room": {"allowed": ["orders", f"team-{user.team_id}-*"]}, "exp": int(time.time()) + 12 * 3600, }, os.environ["RTLAYER_SECRET"], algorithm="HS256", ) ``` **Client** _app.js_ ```js import RTLayer from "rtlayer-client"; const token = await fetch("/api/rtlayer-token").then((res) => res.text()); const rtlayer = RTLayer("YOUR_ORG_ID", "YOUR_SERVICE_ID", token); // Raw websockets: wss://ws.rtlayer.com/YOUR_ORG_ID/YOUR_SERVICE_ID/?token= ``` - A missing, invalid or expired token, or IDs that don’t match the URL, fail the websocket handshake (HTTP 401). Browsers report this only as a failed connection. - A join the token doesn’t allow gets an error frame; the connection stays open. - Patterns are matched against the whole channel name. Stick to letters, digits, `-`, `_` and `:` in channel names: other symbols such as `.` have special meaning in patterns. - Turning authentication on or changing the secret can take up to 10 minutes to apply to new connections. - The JavaScript client is reused per org and service, so a fresh token takes effect after a page reload. ## Delivery and TTL - **Live delivery**: a message goes to every client subscribed to its channel at that moment (all clients of the service for a broadcast). Messages you publish one after another arrive in that order; requests sent in parallel can arrive in any order. - **At most once**: there are no acknowledgements or retries. Clients that are offline, reconnecting or too slow to keep up miss messages. Treat RTLayer as the fast path, and let clients re-sync from your API after a reconnect when every message matters. - **No history**: messages are not stored, except as described for TTL below. With `ttl`, a message published to a channel **nobody is currently subscribed to** is kept for up to `ttl` seconds. The first client to join that channel within that time receives it. Details worth knowing: - If anyone is subscribed when you publish, the message is delivered live and `ttl` has no effect. - A stored message is handed out once: to the client(s) that join first, not to everyone who joins later. - Identical messages stored for the same channel are kept once. Add an ID or timestamp if repeats matter. - Stored messages are delivered in expiry order and are kept for at most about 24 hours, whatever the `ttl`. ## Org API keys and creating services An **org key** belongs to an organization rather than a single service. Use it on servers that manage many services. Owners and admins create and disable org keys on the organization page; every member can see them. ### Publish to any service Send the org key in `x-api-key` and name the target with `serviceId`. The service must belong to the key’s organization. _terminal_ ```bash curl -X POST https://api.rtlayer.com/message \ -H "x-api-key: YOUR_ORG_API_KEY" -H "Content-Type: application/json" \ -d '{"serviceId": "YOUR_SERVICE_ID", "channel": "orders", "message": "Hello"}' ``` ### Create a service _terminal_ ```bash curl -X POST https://api.rtlayer.com/service \ -H "x-api-key: YOUR_ORG_API_KEY" -H "Content-Type: application/json" \ -d '{"name": "Production chat", "description": "Chat for the web app"}' ``` Both `name` and `description` are required. The service is created in the key’s organization and comes with a new service API key: _response_ ```json { "id": "", "name": "Production chat", "description": "Chat for the web app", "orgId": "", "status": "active", "createdAt": 1760000000000, "createdBy": "", "key": { "key": "", "sid": "", "scope": "service", "status": "active" } } ``` > **Note:** Save `key.key` from this response: API keys can’t list a service’s keys later (the dashboard can). Service keys get `403 This operation needs an org API key` on this endpoint. ## API keys | | Service key | Org key | | --- | --- | --- | | Publishes to | its own service | any service in the org (with `serviceId`) | | Creates services | no | yes (`POST /service`) | | Where to manage | service → **API Keys** tab | organization page → **API access** | | Who can create | any member | owners and admins | | Who can disable | owners and admins | owners and admins | - Every new service comes with one service key. Create more to rotate keys without downtime: add the new key to your servers, then disable the old one. - Disabling is permanent; a disabled key can’t be re-enabled. It stops working within about a minute. - A disabled key and a key that never existed both get `401 Invalid API Key`. ## Limits | Limit | Value | | --- | --- | | Publish rate | 3,000 requests per 3 minutes per service, shared by all its keys (including org keys targeting it). Over the limit: `429 Too many requests`, no `Retry-After` header. Requests that are rejected still count, so back off before retrying. | | Request body | 8 MB for JSON, 100 KB for form-encoded bodies. Keep messages small for the fastest delivery. | | Concurrent connections | 10,000 per service. Further connection attempts get HTTP 429 during the handshake. | | Idle connections | Closed after 30 s without answering the server’s ping (handled automatically by clients). | | Client frames | Up to 16 MB; clients only ever send small join/leave frames. | | TTL storage | About 24 hours at most. | ## Errors Errors come back as JSON: `{"status":"error","message":"","code":}`. Validation errors are `400` with a list instead: `{"status":"error","message":[{"field":"message","message":"Required"}]}`. | Status | Message | Meaning | | --- | --- | --- | | 400 | `[{"field":"message","message":"Required"}]` | The body has no `message`. | | 400 | `[{"field":"ttl","message":"Expected number, received nan"}]` | `ttl` isn’t a number. | | 400 | `serviceId is required when using an org API key` | Org keys must say which service to publish to. | | 401 | `API Key not found` | No `x-api-key` header or `apiKey` parameter. | | 401 | `Invalid API Key` | The key doesn’t exist or was disabled. | | 401 | `Token not found` | `POST /service` without credentials. | | 403 | `This operation needs an org API key` | `POST /service` with a service key. | | 404 | `Service not found` | An org key’s `serviceId` doesn’t exist or belongs to another org. | | 429 | `Too many requests` | Publish rate limit; wait and retry with backoff. | | 500 | `Internal Server Error` | Also returned for malformed JSON and bodies over the size limit. Check your JSON first. | | Websocket | Meaning | | --- | --- | | Handshake fails with HTTP 401 | Authentication is on and the token is missing, invalid, expired or for other IDs. | | Handshake fails with HTTP 429 | The service already has 10,000 connections. | | `{"status":"error","message":"You are not allowed to join "}` | The token’s `room.allowed` / `room.denied` don’t permit the channel. | | `{"status":"error","message":"Invalid JSON"}` | The client sent a frame the server couldn’t parse. | ## Using the dashboard | Where | What you can do | | --- | --- | | [Dashboard](https://rtlayer.com/dashboard) | Every service with its live connection count. Press **Ctrl K** (⌘K on Mac) to jump to any service or org. | | Service → **Overview** | Connections right now, messages in the last 24 hours, message chart, active sessions. | | Service → **Playground** | Send test messages and watch them arrive live, with the round-trip time of each one. | | Service → **API Keys** | Org and service IDs, create and disable service keys. | | Service → **Settings** | Require authentication and set the token secret. | | Organization page | Org API keys, team summary, org-wide message analytics, rename. | | [Analytics](https://rtlayer.com/analytics) | Sessions by device, platform, browser and origin; message volume over time. | | Account menu → **Team** | Invite teammates by email (they join on their first sign-in) and change roles. | | Role | Can | | --- | --- | | Owner | Everything an admin can do. The creator of the organization; can’t be removed from it. | | Admin | Invite and remove members, change roles, create and disable org keys, disable service keys, change service authentication, rename the org. | | Member | Create services and service keys, rename services, use the Playground and analytics, view keys. | ## For AI agents - [/llms.txt](https://rtlayer.com/llms.txt): a short index of these docs in the llms.txt format. - [/llms-full.txt](https://rtlayer.com/llms-full.txt): this whole page as one Markdown file. The **Copy as Markdown** button at the top copies the same text. 1. Ask the user for their **org ID**, **service ID** and a **service API key** (dashboard → service → API Keys). Never put the API key in client code; read it from an environment variable such as `RTLAYER_API_KEY`. 2. Server side, publish with `POST https://api.rtlayer.com/message`, header `x-api-key`, JSON body `{"channel": "...", "message": ...}`. Check for `{"status":"ok"}`. 3. Client side, use `rtlayer-client`: `RTLayer(orgId, serviceId).on(channel, handler)`, and `JSON.parse` the message when you published an object. 4. If the data is private, enable authentication and sign tokens on the server with `orgId`, `serviceId`, `uid` and `room.allowed`. 5. Verify end to end: subscribe, publish, and confirm the handler fires (or use the service’s Playground tab). ## Troubleshooting | Symptom | Check | | --- | --- | | Publish returns `ok` but nothing arrives | The client uses the same org and service IDs as the key’s service; it joined the channel **before** you published (or use `ttl`); the channel names match exactly. | | Broadcasts show up as `{"channel":"","message":…}` | That’s the raw frame `on("*")` receives for broadcasts; unwrap it as shown in [Broadcasts](https://rtlayer.com/docs#receiving-broadcasts), or publish to a channel. | | Objects arrive as strings | Expected: messages are delivered as text. `JSON.parse` them. | | Client can’t connect after enabling authentication | Pass a token; check `orgId`/`serviceId` match the URL, the secret is current and the token hasn’t expired. Changes can take up to 10 minutes to apply. | | Connected but joins are refused | The token needs `room.allowed` patterns that match the channel. | | `401 Invalid API Key` | The key was disabled or mistyped; copy a current one from the API Keys tab. | | `429 Too many requests` | Over 3,000 publishes in 3 minutes for this service: batch updates and back off. | | `500` on publish | Usually malformed JSON or an oversized body. |