API Reference
TheLoopChat's server exposes a chat HTTP API plus a WebSocket gateway, all on one port (:8080 by default):
| Group | Base path | Auth | Documented in |
|---|---|---|---|
| Chat API (v1) | / (root paths) |
API key + user JWT or API secret | API — Chat REST |
| WebSocket gateway | /connect |
API key + user JWT (query params) | API — WebSocket |
Outbound integrations (FCM push, webhooks) are documented in API — Push and Webhooks.
Chat API authentication
Every chat request must identify the app with the x-api-key header. There are two auth modes:
Server auth — for customer backends (trusted, full admin over the app's data):
x-api-key: lck_...
x-api-secret: lcs_...
Client auth — for end-user devices (scoped to the authenticated user):
x-api-key: lck_...
authorization: Bearer <user JWT>
The user JWT is an HS256 token with a {"user_id": "<id>"} claim, signed with the app's api_secret — minted by your backend (e.g. @loopchat/node's createToken). Optional iat/exp claims are honored.
Dev tokens: a JWT-shaped string whose signature segment is literally devtoken (header {"alg":"HS256","typ":"JWT"}, payload {"user_id":"..."}, base64url, no padding). Accepted only when the app has dev mode enabled — for local development, never production.
App resolution also enforces suspension: requests for suspended apps fail with code 99, apps of suspended projects with code 98.
Plan limits
Effective limits = defaults ← project plan limits ← per-app permission_overrides (overrides win). Defaults: max_apps: 1, max_mau: 100, max_messages_month: 10000.
Enforcement is tolerant (Redis-backed counters; slight overrun is accepted): creating a new user checks max_mau (monthly-active HyperLogLog), sending a message checks max_messages_month, creating an app via the dashboard checks max_apps. Violations return HTTP 403 with code 97.
Error format
All errors — across every API group — share one body shape (see Protocol for the full code table):
{ "code": 17, "message": "user ada is not a member of messaging:general", "status": 403 }
| HTTP | Codes | Typical causes |
|---|---|---|
| 400 | 4 | Validation failure (zod), malformed JSON |
| 401 | 5 | Missing/invalid API key, user token, or session |
| 403 | 17, 97, 98, 99 | Not a member / not allowed; plan limit; project or app suspended |
| 404 | 16, 18, 19 | Channel / user / message not found |
| 500 | 1 | Internal error |
The most important one for chat clients: 403 + code 17 on watch means "not a member of an existing channel" — the SDKs expose this (LoopChatNetworkError.isNotMember) and the intended fix is a server-side idempotent addMembers call.
Conventions
- JSON everywhere. Send
content-type: application/json; empty bodies are tolerated (so DELETE with a JSON content-type header is fine). - Channel addressing: channels are identified by
type+id; thecidis"type:id"(e.g.messaging:general). Both appear throughout payloads and events. - Message IDs are ULIDs — sortable by creation time; history pagination is
?before=<message id>. - Timestamps are ISO-8601 UTC strings.
- Custom data: users, channels, and messages all carry a free-form
customobject; SDKs surface it ascustom(JS) orextraData(Dart). - Booleans in query strings are the literal strings
true/false. - CORS is permissive with credentials, so the chat API can be called cross-origin from browser-based clients.
Health
GET /health → { "status": "ok" } — unauthenticated liveness probe.