Skip to content

Message Exchange API Specifications

While we offer a drop-in implementation of a live chat widget that works for any website, you might want to build your own implementation, say on mobile apps or any other platforms, and connect to our live chat infrastructure and make full use of our platform. And you can! You should know that NinjaPear is built by a technical founder, and we believe strongly in extensibility.

In this section, we will break down the communication protocol so you can implement your own live chat implementation to be connected to NinjaPear.

NinjaPear Communication Protocol at a high level

The NinjaPear live chat system uses Socket.IO (WebSocket with fallback) for real-time communication.

To begin implementation, you will need two pieces of information:

  1. An api_key which you can fetch and create in your NinjaPear Dashboard Integration page.
  2. A client_id, something like a cookie to link all messages from the same customer across page refreshes and device sessions. You should generate a unique ID per customer session.

Next, you will begin the authentication flow for the widget:

  1. Initial Authentication - Make an HTTP POST request to /api/v1/auth to retrieve a session token.
  2. Socket Authentication - After connecting to Socket.IO, emit client.authenticate with the session token.

To send messages:

  • Customers would emit client.send_message via Socket.IO.

To receive incoming operator messages:

  • The widget would handle the server.new_message event.

Putting it all together:

  Title: NinjaPear Live Chat Communication Protocol

  participant Widget as W
  participant Server as S

  Note over W: Generate client_id\n"client_abc123_1703001234567"\nStore in cookie/localStorage

  Note over W,S: === Authentication Phase ===

  W->S: POST /api/v1/auth\n{"api_key": "...", "client_id": "..."}
  Note over S: Validate API key\nGenerate JWT token
  S-->W: {"token": "jwt_xyz", "client_id": "..."}

  Note over W,S: === WebSocket Connection ===

  W->S: Connect to Socket.IO\nws://server/socket.io/
  S-->W: Event: "connect"

  W->S: Emit: "client.authenticate"\n{"token": "jwt_xyz", "client_id": "..."}
  Note over S: Verify JWT\nJoin room:\n"product:id:client_id"
  S-->W: Event: "server.auth_success"\n{"history": [...messages...]}

  Note over W: === Message Exchange ===

  W->S: Emit: "client.send_message"\n{"message": "Hello", "client_id": "..."}
  Note over S: Store message\nBroadcast
  S-->W: Event: "server.new_message"\n{"sender_type": "customer", ...}

  Note over S: Route to target
  S-->W: Event: "server.new_message"\n{"sender_type": "agent", ...}

  Note over W,S: === Reconnection ===

  W-->S: Event: "disconnect"
  Note over W: Auto-reconnect
  W->S: Connect to Socket.IO
  W->S: Re-authenticate

Authentication

To authenticate, you will need:

  1. API Key: Get this from your https://nubela.co/dashboard/integration
  2. Client ID: Generate a unique identifier for each customer (more on this below)

Step 1: Generate and Store a Client ID

Your widget needs to identify each customer uniquely. Generate a client ID once per customer and reuse it across sessions:

function getOrCreateClientId() {
  // Check if we already have a client_id stored
  let clientId = getCookie('np_client_id');

  if (!clientId) {
    // Generate new client_id: "client_" + random + "_" + timestamp
    const random = Math.random().toString(36).substr(2, 9);
    const timestamp = Date.now();
    clientId = `client_${random}_${timestamp}`;

    // Store it persistently (cookie preferred, localStorage as fallback)
    setCookie('np_client_id', clientId, 365); // Store for 1 year
    localStorage.setItem('np_client_id', clientId);
  }

  return clientId;

}

Step 2: Get a Session Token (HTTP)

Before connecting via WebSocket, you need to get a session token. To do that, you will make an HTTP request to the following widget authentication API endpoint:

Endpoint

POST https://nubela.co/api/v1/auth

Description

Authenticates a widget client and returns a JWT session token that must be used for WebSocket authentication. This endpoint exchanges your API credentials for a temporary session token.

Request Headers

Content-Type: application/json

Body Parameters

Parameter Type Required Description
api_key string Yes Your API key from the NinjaPear Dashboard Integration page
client_id string Yes The unique client identifier you generated for this customer session (e.g., client_abc123_1703001234567)

Example Request Body

{
  "api_key": "pk_live_a1b2c3d4e5f6",
  "client_id": "client_h3x8k9m2p_1703001234567"
}

Response

// Success Response (200 OK)
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "client_id": "client_h3x8k9m2p_1703001234567"
}

Response Fields:

Field Type Description
token string JWT token valid for ~30 minutes. Use this for WebSocket authentication
client_id string Echo back of your client_id (useful for verification)

Error Responses:

Status Code Error Type Response Body Description
400 Bad Request {"error": "Missing api_key or client_id"} Required parameters are missing from the request
401 Unauthorized {"error": "Invalid API key"} The provided API key is invalid or doesn't exist
403 Forbidden {"error": "API key disabled"} The API key exists but has been disabled
429 Too Many Requests {"error": "Rate limit exceeded"} Too many auth requests in a short period
500 Internal Server Error {"error": "Failed to generate token"} Server-side error during token generation
503 Service Unavailable {"error": "Service temporarily unavailable"} Server is temporarily unable to handle the request

Important Notes

  1. Token Expiration: The JWT token expires after 30 minutes by default. You'll need to request a new token if the connection is lost after expiration.
  2. Client ID Consistency: Always use the same client_id for a customer across requests to maintain conversation continuity.
  3. HTTPS Required: Always use HTTPS in production to protect your API key.
  4. CORS: This endpoint supports CORS, so it can be called directly from browser JavaScript.
  5. Rate Limiting: Be mindful of rate limits. Cache the token and reuse it rather than requesting a new one for each connection.

Usage Flow

  1. Call this endpoint once when initializing the chat widget
  2. Store the returned token temporarily (in memory or sessionStorage)
  3. Use the token immediately to authenticate your WebSocket connection
  4. If WebSocket disconnects and token has expired (after 30 min), request a new token before reconnecting

Javascript Example

async function getSessionToken() {
  const response = await fetch('https://nubela.co/api/v1/auth', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      api_key: 'your_api_key_here',  // From your dashboard
      client_id: getOrCreateClientId() // The client ID you generated
    })
  });

  const data = await response.json();
  // Response: { "token": "jwt_token_xyz", "client_id": "client_abc123_..." }
  return data.token;

}

Step 3: WebSocket Authentication - client.authenticate Event

Event Name

client.authenticate

Description

Authenticates a widget client's WebSocket connection using the JWT token obtained from the HTTP /api/v1/auth endpoint. This must be the first event emitted after establishing a Socket.IO connection.

Event Direction

Client → Server (You emit this to the server)

Emit Parameters

Event Data Object:

Parameter Type Required Description
token string Yes The JWT token received from the /api/v1/auth HTTP endpoint
client_id string Yes The same client identifier used in the HTTP auth request

Example Emit

socket.emit('client.authenticate', {
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "client_id": "client_h3x8k9m2p_1703001234567"
});

Server Responses

The server will respond with one of two events:

Success Response: server.auth_success

Event Data:

Field Type Description
history array Array of previous messages in this conversation (if any)
history[].id string Message ID
history[].content string Message content
history[].sender_type string Either "customer" or "agent"
history[].sent_at string ISO 8601 timestamp
history[].sender_name string Name of sender (for agent messages)

Example Success Response:

socket.on('server.auth_success', (data) => {
  // data = {
  //   "history": [
  //     {
  //       "id": "msg_123",
  //       "content": "Hello, I need help",
  //       "sender_type": "customer",
  //       "sent_at": "2024-01-15T10:30:00Z"
  //     },
  //     {
  //       "id": "msg_124",
  //       "content": "Hi! How can I help you today?",
  //       "sender_type": "agent",
  //       "sender_name": "Support Agent",
  //       "sent_at": "2024-01-15T10:30:15Z"
  //     }
  //   ]
  // }
});

Failure Response: server.auth_failed

Event Data:

Field Type Description
reason string Human-readable reason for authentication failure

Failure Reasons:

Reason Description Action to Take
"invalid_token" JWT token is invalid, malformed, or expired Request a new token via HTTP /api/v1/auth
"missing_token" No token provided in authentication request Include the token in your emit
"missing_client_id" No client_id provided in authentication request Include the client_id in your emit
"token_expired" JWT token has expired (>30 minutes old) Request a new token via HTTP /api/v1/auth
"verification_failed" Token signature verification failed Check your API key and request a new token

Example Failure Response:

socket.on('server.auth_failed', (data) => {
  // data = {
  //   "reason": "invalid_token"
  // }
  console.error('Authentication failed:', data.reason);
  // Handle failure - usually reconnect with fresh token
});

Important Notes

  1. Timing: You must emit client.authenticate immediately after the Socket.IO connect event. The server may disconnect you if authentication doesn't occur within a timeout period.
  2. Room Assignment: Upon successful authentication, the server automatically joins your socket to the room product:{product_id}:{client_id}, ensuring you only receive messages for your conversation.
  3. One-Time Event: You only need to authenticate once per connection. If the connection drops and reconnects, you must authenticate again.
  4. Connection State: After server.auth_success, your socket is ready to:
    • Send messages via client.send_message
    • Receive messages via server.new_message
  5. Auto-Disconnect: If authentication fails, the server will typically disconnect your socket. You'll need to reconnect and try again with valid credentials.

Complete Authentication Flow Example

const socket = io('https://nubela.co');

// Step 1: Wait for connection
socket.on('connect', () => {
  console.log('Connected, authenticating...');

  // Step 2: Send authentication
  socket.emit('client.authenticate', {
    token: savedToken,  // From HTTP auth
    client_id: savedClientId  // Your persistent client ID
  });

});

// Step 3: Handle authentication result
socket.on('server.auth_success', (data) => {
  console.log('Authenticated! Ready to chat.');
  // Load conversation history if provided
  if (data.history) {
    displayChatHistory(data.history);
  }
});

socket.on('server.auth_failed', (data) => {
  console.error('Auth failed:', data.reason);
  // Get new token and reconnect
  reconnectWithNewToken();
});

Sending and Receiving Messages

Once authenticated, your widget can exchange messages with operators. Here's how message communication works:

Sending Messages: client.send_message

Event Name

client.send_message

Direction

Client → Server (You emit this to send a message)

Event Parameters

Parameter Type Required Description
message string Yes The message content to send
client_id string Yes Your client identifier (same one used for auth)
profile object Optional Customer profile information (sent with first message)
profile.name string Optional Customer's name
profile.email string Optional Customer's email address
profile.phone string Optional Customer's phone number
profile.company string Optional Customer's company name

Example

socket.emit('client.send_message', {
  message: "Hello, I need help with my order",
  client_id: "client_h3x8k9m2p_1703001234567",
  profile: {
    name: "John Doe",
    email: "[email protected]"
  }
});

Notes - Include profile with the first message to help operators identify the customer - The message will be echoed back to you via server.new_message event - Messages are automatically persisted and linked to your conversation


Receiving Messages: server.new_message

Event Name

server.new_message

Direction

Server → Client (You listen for this event)

Event Data

Field Type Description
id string Unique message identifier
content string The message text
sender_type string Either "customer" or "agent"
sender_name string Name of sender (for agent messages only)
sent_at string ISO 8601 timestamp when message was sent
client_id string For customer messages: the sender's client_id
target_client_id string For agent messages: the recipient client_id (yours)
conversation_id string UUID of the conversation
user_id string For human agents: their user ID (null for AI)

Example Customer Message (Echo)

socket.on('server.new_message', (message) => {
  // When you send a message, it's echoed back:
  // {
  //   "id": "msg_abc123",
  //   "content": "Hello, I need help with my order",
  //   "sender_type": "customer",
  //   "client_id": "client_h3x8k9m2p_1703001234567",
  //   "sent_at": "2024-01-15T10:30:00Z",
  //   "conversation_id": "conv_uuid_here"
  // }
});

Example Agent/Operator Message

socket.on('server.new_message', (message) => {
  // When an operator responds:
  // {
  //   "id": "msg_xyz789",
  //   "content": "Hi John! I'd be happy to help with your order.",
  //   "sender_type": "agent",
  //   "sender_name": "Sarah from Support",
  //   "target_client_id": "client_h3x8k9m2p_1703001234567",
  //   "sent_at": "2024-01-15T10:30:45Z",
  //   "conversation_id": "conv_uuid_here",
  //   "user_id": "user_123"
  // }
});

Example AI Assistant Message

socket.on('server.new_message', (message) => {
  // AI responses have no user_id:
  // {
  //   "id": "msg_ai456",
  //   "content": "I can help you track your order. Please provide your order number.",
  //   "sender_type": "agent",
  //   "sender_name": "AI Assistant",
  //   "target_client_id": "client_h3x8k9m2p_1703001234567",
  //   "sent_at": "2024-01-15T10:30:30Z",
  //   "conversation_id": "conv_uuid_here",
  //   "user_id": null  // null indicates AI response
  // }
});

Message Handling Best Practices

  1. Distinguish Message Types:

    ```javascript socket.on('server.new_message', (message) => {

    if (message.sender_type === 'customer') { // Your own message echoed back if (message.client_id === myClientId) { displayMyMessage(message); } } else if (message.sender_type === 'agent') { // Message from operator or AI const isAI = !message.user_id; displayOperatorMessage(message, isAI); }

    }); ```

  2. Prevent Duplicate Display:

    ```javascript const displayedMessageIds = new Set();

    socket.on('server.new_message', (message) => { // Skip if already displayed (important for echoed messages) if (displayedMessageIds.has(message.id)) { return; } displayedMessageIds.add(message.id);

    // Display the message renderMessage(message);

    }); ```

  3. Optimistic UI Updates:

    ```javascript function sendMessage(text) {

    // Display immediately for better UX (optimistic update) const tempId = temp_${Date.now()}; displayMyMessage({ id: tempId, content: text, sent_at: new Date().toISOString() });

    // Send to server socket.emit('client.send_message', { message: text, client_id: myClientId });

    }

    // When echo comes back, replace temp message socket.on('server.new_message', (message) => { if (message.sender_type === 'customer' && message.content === lastSentMessage) { replaceTempMessage(tempId, message); } }); ```

  4. Handle Connection States:

    ```javascript let messageQueue = [];

    function sendMessage(text) { if (socket.connected && authenticated) { socket.emit('client.send_message', { message: text, client_id: myClientId }); } else { // Queue message to send when reconnected messageQueue.push(text); showStatus('Message queued - reconnecting...'); } }

    socket.on('server.auth_success', () => { // Send queued messages after reconnection messageQueue.forEach(text => sendMessage(text)); messageQueue = []; }); ```


Error Handling

Server Error Event:

socket.on('server.error', (error) => {
  // Handle various error types
  // {
  //   "error": "message_empty" | "unauthenticated" | "failed_to_store_message"
  // }

  switch(error.error) {
    case 'message_empty':
      console.error('Cannot send empty message');
      break;
    case 'unauthenticated':
      console.error('Not authenticated - reconnecting...');
      reconnectWithAuth();
      break;
    case 'failed_to_store_message':
      console.error('Message failed to save - retry');
      retryLastMessage();
      break;
  }

});

The client_id exists to "cookie" and identify a user session. We recommend using the following format structure to generate a secure, random and persistent client_id:

client_{random_component}_{timestamp}

An example would be: client_h3x8k9m2p_1703001234567

Generation Algorithm

  1. Random Component (9-12 characters)
// JavaScript
Math.random().toString(36).substr(2, 9)
# python
import secrets
secrets.token_urlsafe(9)[:9]
// Java
UUID.randomUUID().toString().substring(0, 9)
// PHP
substr(bin2hex(random_bytes(6)), 0, 9)
  1. Timestamp Component

  2. Use milliseconds since Unix epoch for uniqueness

  3. Provides chronological ordering and additional entropy

Complete Implementation Examples

In browser with JavaScript:

function generateClientId() {
  const random = Math.random().toString(36).substr(2, 9);
  const timestamp = Date.now();
  return `client_${random}_${timestamp}`;
}

function getOrCreateClientId() {
  // Check cookie first
  let clientId = getCookie('np_client_id');

  if (!clientId) {
    // Fallback to localStorage
    clientId = localStorage.getItem('np_client_id');

    if (!clientId) {
      // Generate new ID
      clientId = generateClientId();
    }

    // Store in both cookie and localStorage
    setCookie('np_client_id', clientId, 365); // 365 days
    localStorage.setItem('np_client_id', clientId);
  }

  return clientId;

}

In Python:

import secrets
import time

def generate_client_id():
    random_part = secrets.token_urlsafe(9)[:9]
    timestamp = int(time.time() * 1000)
    return f"client_{random_part}_{timestamp}"

Mobile (React Native/Flutter)

// React Native with AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage';

async function getOrCreateClientId() {
  let clientId = await AsyncStorage.getItem('np_client_id');

  if (!clientId) {
    const random = Math.random().toString(36).substr(2, 9);
    const timestamp = Date.now();
    clientId = `client_${random}_${timestamp}`;
    await AsyncStorage.setItem('np_client_id', clientId);
  }

  return clientId;

}