Docs

Build on qux.chat

Keys, conversations, the realtime feed, webhooks, calls and media, the SDK, and the move to QuxCloud.

Keys

Sign in with your QuxCloud account and open the console: your first workspace is made for you, on the Free plan. Create a key there. It looks like qcx_live_…, is shown once, and authenticates as your whole workspace, so it lives on your server, in an environment variable.

Every request carries it: Authorization: Bearer qcx_live_…. The base URL is https://qux.chat/api/chat.

Your first conversation

curl https://qux.chat/api/chat/conversations \
  -H "Authorization: Bearer $QUXCHAT_API_KEY" \
  -H "content-type: application/json" \
  -d '{"kind":"support","createdBy":"agent:sam",
       "participants":[{"actorId":"agent:sam","role":"admin"},{"actorId":"customer:ada"}]}'

Actors are your own ids (customer:ada, agent:sam): qux.chat keeps no user table of yours. Your server says who is acting; the key says which workspace.

The SDK

@quxkit/chat is a typed TypeScript client for this API, with no dependencies, for Node 18+ and edge runtimes. It installs from our registry, npm.quxkit.ai, with a registry token from the console’s Keys page (it starts qcr_live_, and can only install packages):

# .npmrc
registry=https://npm.quxkit.ai/
//npm.quxkit.ai/:_authToken=${QUXKIT_NPM_TOKEN}

pnpm add @quxkit/chat

Set its baseUrl to qux.chat:

import { ChatClient } from '@quxkit/chat';

const chat = new ChatClient({
  apiKey: process.env.QUXCHAT_API_KEY!,   // qcx_live_…, from the console
  baseUrl: 'https://qux.chat/api/chat',
});

const msg = await chat.messages.send(conversationId, {
  body: 'Hello',
  senderActorId: 'customer:ada',
  idempotencyKey: 'order-1042-hello',
});

An idempotency key is unique across the workspace: the same key, conversation and body returns the original message instead of a second one.

Realtime

Every conversation has an event feed. Read it with a cursor and nothing is ever missed between two reads; ask it to wait and it answers the moment something happens.

// Long-poll: every event after a cursor, oldest first, gap-free.
// An empty answer after the wait means nothing happened; ask again with the same cursor.
let since = '';
for (;;) {
  const q = new URLSearchParams({ wait: '25000', ...(since ? { since } : {}) });
  const r = await fetch(`https://qux.chat/api/chat/conversations/${id}/events?${q}`, {
    headers: { authorization: `Bearer ${key}` },
  });
  const { events, nextCursor } = await r.json();
  for (const e of events) handle(e);
  if (nextCursor) since = nextCursor;
}

Webhooks

Subscribe an endpoint of yours with POST /webhooks. Every delivery is signed (Standard Webhooks), retried with backoff, and verifiable in one call with the SDK’s verifyWebhook.

Calls and media

Voice and video calls ring the people you name, connect browser to browser for small calls and through an SFU for groups, and have a lobby: a guest knocks, someone in the call admits them. Media uploads go straight to storage on a presigned link and attach to a message by id.

Browsers

The API sends no CORS headers, on purpose: a key is your workspace and must never reach a browser. Your browser talks to your server, which takes the user from your session, adds the key and forwards. The SDK’s turnProxy is that route for calls.

Endpoints

RouteWhat it does
GET /conversations?actorId=…One actor’s conversations, newest first, with unread counts
POST /conversationsCreate a conversation with its participants
GET /conversations/:id/messagesHistory, newest first; page with `before`
POST /conversations/:id/messagesSend, with an idempotency key
POST /conversations/:id/readMark read up to a message
GET /conversations/:id/participantsWho is in it
GET /conversations/:id/events?since=&wait=The realtime feed (long-poll)
GET /conversations/:id/streamThe same feed as server-sent events (beta)
GET · POST /webhooksList and add webhook subscriptions
POST /callsStart a call: rings the invitees
POST /calls/:id/join · leave · knock · admitAnswer, hang up, and the guest lobby
POST /calls/:id/sfu/…A group call’s media path through the SFU
POST /mediaA presigned upload; then POST /media/:id/complete

Moving to QuxCloud

qux.chat runs QuxCloud’s chat API: the same routes, the same qcx keys and the same data model. On QuxCloud the base URL is https://cloud.quxkit.ai/api/chat (the SDK’s default). Nothing else in your code changes.