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)
[email protected]If 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" });
}
}
}
);
// 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 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;
}
}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.