This guide will walk you through integrating the Ethora SDK into your existing Node.js backend application.
- Prerequisites
- Installation
- Environment Configuration
- Basic Integration
- Integration Patterns
- Common Use Cases
- Error Handling
- Best Practices
- Troubleshooting
- Node.js 18+ or higher
- TypeScript 5.0+ (for TypeScript projects)
- An existing Node.js backend application (Express, Fastify, NestJS, etc.)
- Ethora API credentials:
ETHORA_CHAT_API_URLETHORA_CHAT_APP_IDETHORA_CHAT_APP_SECRETETHORA_CHAT_BOT_JID(optional, for chatbot features)
Ethora exposes Swagger UI from every running backend instance at:
- Hosted (Ethora main):
https://api.ethoradev.com/api-docs/ - Enterprise/self-hosted:
https://api.<your-domain>/api-docs/
If you are running a separate staging instance, the same pattern applies (e.g. https://api.asterotoken.com/api-docs/).
npm install @ethora/sdk-backend
# or
yarn add @ethora/sdk-backend
# or
pnpm add @ethora/sdk-backendThe package includes TypeScript definitions, so no additional @types package is needed.
Add the following environment variables to your .env file or your environment configuration:
# Required
ETHORA_CHAT_API_URL=https://api.ethoradev.com
ETHORA_CHAT_APP_ID=your_app_id_here
ETHORA_CHAT_APP_SECRET=your_app_secret_here
# Optional (for chatbot features)
ETHORA_CHAT_BOT_JID=your_bot_jid@domain.comIf you're using a .env file, ensure you have dotenv installed and configured:
npm install dotenvIn your main application file (e.g., app.js, server.js, or index.ts):
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();import { getEthoraSDKService } from '@ethora/sdk-backend';You can initialize the service in several ways:
// services/chatService.ts
import { getEthoraSDKService } from '@ethora/sdk-backend';
// Get the singleton instance
const chatService = getEthoraSDKService();
export default chatService;// In your route handler or service
import { getEthoraSDKService } from '@ethora/sdk-backend';
const chatService = getEthoraSDKService();// chat.service.ts
import { Injectable } from '@nestjs/common';
import { getEthoraSDKService } from '@ethora/sdk-backend';
@Injectable()
export class ChatService {
private readonly ethoraService = getEthoraSDKService();
// Your methods here
}// routes/chat.ts
import express, { Request, Response } from 'express';
import { getEthoraSDKService } from '@ethora/sdk-backend';
import axios from 'axios';
const router = express.Router();
const chatService = getEthoraSDKService();
// Create a chat room for a workspace
router.post(
'/workspaces/:workspaceId/chat',
async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
const roomData = req.body;
const response = await chatService.createChatRoom(workspaceId, {
title: roomData.title || `Chat Room ${workspaceId}`,
uuid: workspaceId,
type: roomData.type || 'group',
...roomData,
});
res.json({ success: true, data: response });
} catch (error) {
if (axios.isAxiosError(error)) {
res.status(error.response?.status || 500).json({
error: 'Failed to create chat room',
details: error.response?.data,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
},
);
// Create a user
router.post('/users/:userId', async (req: Request, res: Response) => {
try {
const { userId } = req.params;
const userData = req.body;
const response = await chatService.createUser(userId, userData);
res.json({ success: true, data: response });
} catch (error) {
if (axios.isAxiosError(error)) {
res.status(error.response?.status || 500).json({
error: 'Failed to create user',
details: error.response?.data,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
// Grant user access to chat room
router.post(
'/workspaces/:workspaceId/chat/users/:userId',
async (req: Request, res: Response) => {
try {
const { workspaceId, userId } = req.params;
await chatService.grantUserAccessToChatRoom(workspaceId, userId);
res.json({ success: true, message: 'Access granted' });
} catch (error) {
if (axios.isAxiosError(error)) {
res.status(error.response?.status || 500).json({
error: 'Failed to grant access',
details: error.response?.data,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
},
);
// Remove user access from chat room
router.delete(
'/workspaces/:workspaceId/chat/users/:userId',
async (req: Request, res: Response) => {
try {
const { workspaceId, userId } = req.params;
await chatService.removeUserAccessFromChatRoom(workspaceId, userId);
res.json({ success: true, message: 'Access removed' });
} catch (error) {
if (axios.isAxiosError(error)) {
res.status(error.response?.status || 500).json({
error: 'Failed to remove access',
details: error.response?.data,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
},
);
// Generate client JWT token
router.get('/users/:userId/chat-token', (req: Request, res: Response) => {
try {
const { userId } = req.params;
const token = chatService.createChatUserJwtToken(userId);
res.json({ token });
} catch (error) {
res.status(500).json({ error: 'Failed to generate token' });
}
});
// Get users
router.get('/users', async (req: Request, res: Response) => {
try {
const { chatName, xmppUsername } = req.query;
const params: any = {};
if (chatName) params.chatName = String(chatName);
if (xmppUsername) params.xmppUsername = String(xmppUsername);
const response = await chatService.getUsers(
Object.keys(params).length > 0 ? params : undefined,
);
res.json({ success: true, data: response });
} catch (error) {
if (axios.isAxiosError(error)) {
res.status(error.response?.status || 500).json({
error: 'Failed to get users',
details: error.response?.data,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
// Update users (batch)
router.patch('/users', async (req: Request, res: Response) => {
try {
const { users } = req.body;
if (!Array.isArray(users) || users.length === 0) {
return res.status(400).json({ error: 'users must be a non-empty array' });
}
if (users.length > 100) {
return res
.status(400)
.json({ error: 'Maximum 100 users allowed per request' });
}
const response = await chatService.updateUsers(users);
res.json({ success: true, data: response });
} catch (error) {
if (axios.isAxiosError(error)) {
res.status(error.response?.status || 500).json({
error: 'Failed to update users',
details: error.response?.data,
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
export default router;// chat/chat.service.ts
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { getEthoraSDKService } from '@ethora/sdk-backend';
import axios from 'axios';
@Injectable()
export class ChatService {
private readonly ethoraService = getEthoraSDKService();
async createChatRoom(workspaceId: string, roomData?: any) {
try {
return await this.ethoraService.createChatRoom(workspaceId, roomData);
} catch (error) {
if (axios.isAxiosError(error)) {
throw new HttpException(
{
message: 'Failed to create chat room',
details: error.response?.data,
},
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
throw error;
}
}
async createUser(userId: string, userData?: any) {
try {
return await this.ethoraService.createUser(userId, userData);
} catch (error) {
if (axios.isAxiosError(error)) {
throw new HttpException(
{
message: 'Failed to create user',
details: error.response?.data,
},
error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
throw error;
}
}
generateClientToken(userId: string): string {
return this.ethoraService.createChatUserJwtToken(userId);
}
}
// chat/chat.controller.ts
import { Controller, Post, Get, Param, Body } from '@nestjs/common';
import { ChatService } from './chat.service';
@Controller('chat')
export class ChatController {
constructor(private readonly chatService: ChatService) {}
@Post('workspaces/:workspaceId/rooms')
async createChatRoom(
@Param('workspaceId') workspaceId: string,
@Body() roomData: any,
) {
return this.chatService.createChatRoom(workspaceId, roomData);
}
@Post('users/:userId')
async createUser(@Param('userId') userId: string, @Body() userData: any) {
return this.chatService.createUser(userId, userData);
}
@Get('users/:userId/token')
getClientToken(@Param('userId') userId: string) {
return { token: this.chatService.generateClientToken(userId) };
}
}// routes/chat.ts
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { getEthoraSDKService } from '@ethora/sdk-backend';
const chatService = getEthoraSDKService();
export async function chatRoutes(fastify: FastifyInstance) {
// Create chat room
fastify.post(
'/workspaces/:workspaceId/chat',
async (request: FastifyRequest, reply: FastifyReply) => {
const { workspaceId } = request.params as { workspaceId: string };
const roomData = request.body as any;
try {
const response = await chatService.createChatRoom(
workspaceId,
roomData,
);
return { success: true, data: response };
} catch (error) {
reply.code(500).send({ error: 'Failed to create chat room' });
}
},
);
// Generate client token
fastify.get(
'/users/:userId/chat-token',
async (request: FastifyRequest, reply: FastifyReply) => {
const { userId } = request.params as { userId: string };
const token = chatService.createChatUserJwtToken(userId);
return { token };
},
);
}When creating a new workspace, set up the chat room and initial users:
async function setupWorkspaceChat(
workspaceId: string,
userIds: string[],
adminUserId: string,
) {
const chatService = getEthoraSDKService();
try {
// 1. Create chat room
await chatService.createChatRoom(workspaceId, {
title: `Workspace ${workspaceId}`,
uuid: workspaceId,
type: 'group',
});
// 2. Create users (if they don't exist)
for (const userId of userIds) {
try {
await chatService.createUser(userId, {
firstName: 'User',
lastName: 'Name',
});
} catch (error) {
// User might already exist, continue
console.warn(`User ${userId} might already exist`);
}
}
// 3. Grant access to all users
await chatService.grantUserAccessToChatRoom(workspaceId, userIds);
// 4. Grant chatbot access (if configured)
try {
await chatService.grantChatbotAccessToChatRoom(workspaceId);
} catch (error) {
console.warn('Chatbot access not configured or failed');
}
return { success: true };
} catch (error) {
console.error('Failed to setup workspace chat:', error);
throw error;
}
}When a new user joins your platform:
async function onboardNewUser(
userId: string,
userData: { firstName: string; lastName: string; email: string },
) {
const chatService = getEthoraSDKService();
try {
// Create user in chat service
await chatService.createUser(userId, {
firstName: userData.firstName,
lastName: userData.lastName,
email: userData.email,
displayName: `${userData.firstName} ${userData.lastName}`,
});
// Generate client token for frontend
const clientToken = chatService.createChatUserJwtToken(userId);
return {
success: true,
chatToken: clientToken,
};
} catch (error) {
console.error('Failed to onboard user:', error);
throw error;
}
}When adding a user to an existing workspace:
async function addUserToWorkspace(workspaceId: string, userId: string) {
const chatService = getEthoraSDKService();
try {
// Ensure user exists
try {
await chatService.createUser(userId);
} catch (error) {
// User might already exist, continue
}
// Grant access to workspace chat room
await chatService.grantUserAccessToChatRoom(workspaceId, userId);
return { success: true };
} catch (error) {
console.error('Failed to add user to workspace:', error);
throw error;
}
}When removing a user from a workspace:
async function removeUserFromWorkspace(workspaceId: string, userId: string) {
const chatService = getEthoraSDKService();
try {
// Remove access from workspace chat room
await chatService.removeUserAccessFromChatRoom(workspaceId, userId);
return { success: true };
} catch (error) {
console.error('Failed to remove user from workspace:', error);
throw error;
}
}
// Remove multiple users at once
async function removeMultipleUsersFromWorkspace(
workspaceId: string,
userIds: string[],
) {
const chatService = getEthoraSDKService();
try {
await chatService.removeUserAccessFromChatRoom(workspaceId, userIds);
return { success: true };
} catch (error) {
console.error('Failed to remove users from workspace:', error);
throw error;
}
}When deleting a workspace:
async function cleanupWorkspaceChat(workspaceId: string, userIds: string[]) {
const chatService = getEthoraSDKService();
try {
// Delete chat room (handles non-existent gracefully)
await chatService.deleteChatRoom(workspaceId);
// Optionally delete users (if they're no longer needed)
if (userIds.length > 0) {
try {
await chatService.deleteUsers(userIds);
} catch (error) {
console.warn('Some users might not exist:', error);
}
}
return { success: true };
} catch (error) {
console.error('Failed to cleanup workspace chat:', error);
throw error;
}
}Retrieve users from the chat service:
async function getUsersExample() {
const chatService = getEthoraSDKService();
try {
// Get all users
const allUsers = await chatService.getUsers();
console.log(`Total users: ${allUsers.results?.length || 0}`);
// Get users by chat name (group chat)
const groupChatUsers = await chatService.getUsers({
chatName: 'appId_workspaceId',
});
// Get users by chat name (1-on-1 chat)
const oneOnOneUsers = await chatService.getUsers({
chatName: 'userA-userB',
});
return { allUsers, groupChatUsers, oneOnOneUsers };
} catch (error) {
console.error('Failed to get users:', error);
throw error;
}
}Update multiple users at once:
async function updateUsersExample() {
const chatService = getEthoraSDKService();
try {
// Update multiple users (1-100 users per request)
const response = await chatService.updateUsers([
{
xmppUsername: 'appId_user1',
firstName: 'John',
lastName: 'Doe',
username: 'johndoe',
profileImage: 'https://example.com/avatar1.jpg',
},
{
xmppUsername: 'appId_user2',
firstName: 'Jane',
lastName: 'Smith',
username: 'janesmith',
},
]);
// Check results
response.results?.forEach((result: any) => {
if (result.status === 'updated') {
console.log(`User ${result.xmppUsername} updated successfully`);
} else if (result.status === 'not-found') {
console.warn(`User ${result.xmppUsername} not found`);
} else if (result.status === 'skipped') {
console.log(`User ${result.xmppUsername} update skipped`);
}
});
return response;
} catch (error) {
console.error('Failed to update users:', error);
throw error;
}
}Creates a user in the chat service using the /v2/users/batch endpoint.
Parameters:
userId(UUID): The unique identifier of the useruserData(optional): Additional user datafirstName(string): User's first namelastName(string): User's last name (minimum 2 characters)email(string): User's email addresspassword(string): User's passworddisplayName(string): Display name (will be split into firstName/lastName if needed)
Returns: Promise resolving to the API response
Note: The API requires lastName to be at least 2 characters. If not provided or too short, defaults to "User".
Creates a chat room using the /v2/chats endpoint.
Parameters:
chatId(UUID): The unique identifier of the chat/workspaceroomData(optional): Room configurationtitle(string): Chat room titleuuid(string): Room UUID (defaults to chatId)type(string): Room type (defaults to "group")
Returns: Promise resolving to the API response
Grants user(s) access to a chat room using the /v2/chats/users-access endpoint.
Parameters:
chatId(UUID): The unique identifier of the chat/workspaceuserId(UUID | UUID[]): Single user ID or array of user IDs
Returns: Promise resolving to the API response
Note: User IDs are automatically prefixed with {appId}_ if they don't already have the prefix.
Removes user(s) access from a chat room using the /v2/chats/users-access DELETE endpoint.
Parameters:
chatId(UUID): The unique identifier of the chat/workspaceuserId(UUID | UUID[]): Single user ID or array of user IDs to remove
Returns: Promise resolving to the API response
Note: User IDs are automatically prefixed with {appId}_ if they don't already have the prefix.
Grants chatbot access to a chat room.
Parameters:
chatId(UUID): The unique identifier of the chat/workspace
Returns: Promise resolving to the API response
Requires: ETHORA_CHAT_BOT_JID environment variable to be set
Retrieves users from the chat service using the /v2/chats/users endpoint.
Parameters:
params(GetUsersQueryParams, optional): Query parameterschatName(string): Filter by chat name- Group chats:
appId_chatIdformat - 1-on-1 chats:
xmppUsernameA-xmppUsernameBformat
- Group chats:
xmppUsername(string): Filter by specific XMPP username
Query Modes:
- No parameters: Returns all users of the app
- With
chatName: Returns all users of the specified chat - With
xmppUsername: Returns a specific user
Returns: Promise resolving to the API response with users array
Updates multiple users at once using the /v2/chats/users PATCH endpoint.
Parameters:
users(UpdateUserData[]): Array of user data to update (1-100 users)xmppUsername(string, required): XMPP username to identify the userfirstName(string, optional): First namelastName(string, optional): Last nameusername(string, optional): UsernameprofileImage(string, optional): Profile image URL
Returns: Promise resolving to the API response with results array
Response Status Values:
updated: User was successfully updated (includes updated user data)not-found: User was not foundskipped: User update was skipped
Limits: 1-100 users per request
Deletes users from the chat service using the /v1/users/batch endpoint.
Parameters:
userIds(UUID[]): Array of user IDs to delete
Returns: Promise resolving to the API response
Note: Gracefully handles non-existent users (422 status with "not found").
Deletes a chat room using the /v1/chats endpoint.
Parameters:
chatId(UUID): The unique identifier of the chat/workspace
Returns: Promise resolving to the API response
Note: Gracefully handles non-existent rooms (422 status with "not found").
Generates a chat room JID from a chat ID.
Parameters:
chatId(UUID): The unique identifier of the chatfull(boolean, optional): Whether to include the full JID domain (default: true)
Returns: The JID string
- Full:
{appId}_{chatId}@conference.xmpp.ethoradev.com - Short:
{appId}_{chatId}
Creates a client-side JWT token for user authentication.
Parameters:
userId(UUID): The unique identifier of the user
Returns: The encoded JWT token for client-side authentication
The SDK uses Axios for HTTP requests, so errors are AxiosError instances:
import axios from 'axios';
import { getEthoraSDKService } from '@ethora/sdk-backend';
const chatService = getEthoraSDKService();
async function createChatRoomSafely(workspaceId: string) {
try {
return await chatService.createChatRoom(workspaceId);
} catch (error) {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const errorData = error.response?.data;
// Handle specific error cases
if (status === 422) {
// Validation error
console.error('Validation error:', errorData);
} else if (status === 401) {
// Authentication error
console.error('Authentication failed - check your credentials');
} else if (status === 404) {
// Resource not found
console.error('Resource not found');
} else {
// Other HTTP errors
console.error(`HTTP error ${status}:`, errorData);
}
} else {
// Non-HTTP errors
console.error('Unexpected error:', error);
}
throw error;
}
}Some operations are idempotent and can be safely retried:
async function ensureChatRoomExists(workspaceId: string) {
const chatService = getEthoraSDKService();
try {
await chatService.createChatRoom(workspaceId);
} catch (error) {
if (axios.isAxiosError(error)) {
const errorData = error.response?.data;
const errorMessage =
typeof errorData === 'object' && errorData !== null
? (errorData as { error?: string }).error || ''
: String(errorData || '');
// If room already exists, that's okay
if (
error.response?.status === 422 &&
(errorMessage.includes('already exist') ||
errorMessage.includes('already exists'))
) {
console.log('Chat room already exists, continuing...');
return; // Success - room exists
}
}
// Re-throw if it's a different error
throw error;
}
}The SDK provides a singleton instance. Reuse it rather than creating multiple instances:
// Good
const chatService = getEthoraSDKService();
// Avoid
const chatService1 = getEthoraSDKService();
const chatService2 = getEthoraSDKService(); // UnnecessaryCreate a service wrapper in your application:
// services/chatService.ts
import { getEthoraSDKService } from '@ethora/sdk-backend';
import type { ChatRepository } from '@ethora/sdk-backend';
class ChatServiceWrapper {
private service: ChatRepository;
constructor() {
this.service = getEthoraSDKService();
}
async setupWorkspace(workspaceId: string, userIds: string[]) {
// Your custom logic here
await this.service.createChatRoom(workspaceId);
// ... more setup logic
}
// Expose other methods as needed
getService() {
return this.service;
}
}
export default new ChatServiceWrapper();Validate environment variables on application startup:
// config/validateEnv.ts
function validateEthoraConfig() {
const required = [
'ETHORA_CHAT_API_URL',
'ETHORA_CHAT_APP_ID',
'ETHORA_CHAT_APP_SECRET',
];
const missing = required.filter((key) => !process.env[key]);
if (missing.length > 0) {
throw new Error(
`Missing required Ethora environment variables: ${missing.join(', ')}`,
);
}
}
// Call on startup
validateEthoraConfig();Integrate with your existing logging system:
import { getEthoraSDKService } from '@ethora/sdk-backend';
import { logger } from './utils/logger'; // Your logger
const chatService = getEthoraSDKService();
async function createChatRoomWithLogging(workspaceId: string) {
logger.info(`Creating chat room for workspace: ${workspaceId}`);
try {
const result = await chatService.createChatRoom(workspaceId);
logger.info(`Chat room created successfully: ${workspaceId}`);
return result;
} catch (error) {
logger.error(`Failed to create chat room: ${workspaceId}`, error);
throw error;
}
}Use TypeScript types from the SDK:
import type { UUID, ApiResponse } from '@ethora/sdk-backend';
async function createUserTyped(
userId: UUID,
userData: {
firstName: string;
lastName: string;
email: string;
},
): Promise<ApiResponse> {
const chatService = getEthoraSDKService();
return await chatService.createUser(userId, userData);
}Solution: Ensure all required environment variables are set:
ETHORA_CHAT_API_URL=https://api.ethoradev.com
ETHORA_CHAT_APP_ID=your_app_id
ETHORA_CHAT_APP_SECRET=your_app_secretSolution: Verify your ETHORA_CHAT_APP_SECRET is correct and matches your app ID.
Solution: Handle idempotent operations gracefully:
try {
await chatService.createUser(userId);
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 422) {
// User already exists, continue
console.log('User already exists');
} else {
throw error;
}
}Solution: The SDK handles this gracefully. The deleteChatRoom method returns { ok: false, reason: "Chat room not found" } if the room doesn't exist, which is safe to ignore.
Solution: Ensure you're using TypeScript 5.0+ and have proper type definitions:
npm install --save-dev typescript@^5.0.0- Review the API Reference for detailed method documentation
- Check out the Examples directory for complete integration examples
- See the Healthcare Insurance Demo for a real-world use case
For issues, questions, or contributions, please refer to the main README.md file.
The project uses strict TypeScript settings. See tsconfig.json for details.
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
Apache 2.0
For issues and questions, please open an issue on the GitHub repository.
To run tests with logs run from root: TEST_LOG_FILE=logs/chat-repo.log npm test