Server SDK
Documentation
Server SDK
Sign chat tokens and run admin operations from your backend with @emb-chat/server-sdk.
@emb-chat/server-sdk is the backend half of emb.chat. It does two things: ChatAuth signs the tokens your users connect with, and ChatAdmin performs host-side admin operations over REST with your API key.
npm install @emb-chat/server-sdk
import { ChatAuth, ChatAdmin, ChatApiError } from '@emb-chat/server-sdk';
ChatAuth — signing tokens
Tokens are signed with RS256 or EdDSA (asymmetric only — HS* and none are rejected). You hold the private key; emb.chat verifies with the public key you registered under a kid.
Generate a keypair
const pair = await ChatAuth.generateKeyPair('RS256'); // or 'EdDSA'
// pair.privatePem -> keep on your backend, never commit
// pair.publicPem -> register with emb.chat under a kid
Load a private key
import { readFileSync } from 'node:fs';
const privateKey = await ChatAuth.loadPrivateKey(
readFileSync('./emb-private.pem', 'utf8'),
);
Create a token
const token = await ChatAuth.createToken(privateKey, {
sub: 'user_abc123', // required — your user id
aud: 'app_demo', // required — your app id
kid: 'key_01', // required — selects the registered public key
name: 'Jane Doe', // optional — mirrored to display_name
avatar: 'https://…', // optional — mirrored to avatar_url
meta: { role: 'admin' }, // optional — mirrored to metadata
expiresIn: '1h',
});
| Claim | Required | Purpose |
|---|---|---|
sub | ✓ | The host’s user id. |
aud | ✓ | The app id — selects which registered keys verify the token. |
kid | ✓ | Key id in the JWT header; looks up the public key by (aud, kid). |
exp | ✓ | Set via expiresIn. 15m–1h for browsers, 24h for backend/mobile. |
name / avatar / meta | — | Mirrored into the user cache on connect (field-conditional — omitting them never wipes existing values). |
Rotate a key
Register the new key, start signing with its kid, then deactivate the old one. Tokens already signed with the old kid keep verifying until they expire.
await admin.keys.register({ kid: 'key_02', publicKey: pair.publicPem, algorithm: 'RS256' });
// …sign new tokens with kid: 'key_02'…
await admin.keys.delete('key_01');
ChatAdmin — admin operations
ChatAdmin authenticates with your app API key (Authorization: Bearer <api_key>) and exposes the full host-side REST surface. API-key callers bypass the group-mode matrix — they always have full rights for that app.
const admin = new ChatAdmin({
url: 'https://chat.acme.com',
apiKey: process.env.EMB_CHAT_API_KEY!, // never in source control
appId: 'app_demo',
});
Users
await admin.users.upsert({ id: 'user_abc123', display_name: 'Jane Doe' });
const presence = await admin.users.presence('user_abc123'); // { user_id, status: 'online' | 'offline' }
Channels
const channel = await admin.channels.create({
type: 'group',
name: 'Engineering',
members: ['user_abc123', 'user_xyz789'],
});
await admin.channels.update(channel.id, { name: 'Eng' });
await admin.channels.sendMessage(channel.id, { body: 'Welcome!' });
const members = await admin.channels.members(channel.id);
await admin.channels.setMemberRole(channel.id, 'user_xyz789', 'admin');
const channels = await admin.channels.list();
await admin.channels.delete(channel.id);
Apps & group mode
const app = await admin.apps.get();
await admin.apps.update({ config: { group_mode: 'hybrid' } }); // 'external' | 'internal' | 'hybrid'
Webhooks
const wh = await admin.webhooks.register({
url: 'https://acme.com/hooks/chat',
events: ['message.*', 'member.*', 'presence.*'],
});
// wh.secret is returned exactly once — store it now.
await admin.webhooks.list();
await admin.webhooks.update(wh.id, { events: ['message.created'] });
await admin.webhooks.deliveries(wh.id); // per-endpoint delivery log
await admin.webhooks.test(wh.id); // fire a one-shot test event
await admin.webhooks.delete(wh.id);
See the Webhooks reference for the delivery envelope and verification.
Error handling
Every failed REST call throws ChatApiError — one type for both SDKs. Branch on status and the stable code.
import { ChatApiError } from '@emb-chat/server-sdk';
try {
await admin.channels.delete(channelId);
} catch (err) {
if (err instanceof ChatApiError && err.code === 'channel_not_found') {
return; // already gone — ignore
}
throw err;
}
class ChatApiError extends Error {
readonly status: number; // HTTP status (0 for transport-only failures)
readonly code: string; // stable machine code, e.g. 'channel_not_found'
readonly message: string;
readonly raw: unknown; // parsed response body
}
Common code values: validation_error, channel_not_found, forbidden_in_group_mode, role_required, last_owner_required, invalid_token, expired_token, unknown_kid, inactive_key, invalid_api_key.