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:
- An
api_key
which you can fetch and create in your NinjaPear Dashboard Integration page. - 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:
- Initial Authentication - Make an HTTP POST request to
/api/v1/auth
to retrieve a session token. - 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:
- API Key: Get this from your https://nubela.co/dashboard/integration
- 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
- 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.
- Client ID Consistency: Always use the same client_id for a customer across requests to maintain conversation continuity.
- HTTPS Required: Always use HTTPS in production to protect your API key.
- CORS: This endpoint supports CORS, so it can be called directly from browser JavaScript.
- Rate Limiting: Be mindful of rate limits. Cache the token and reuse it rather than requesting a new one for each connection.
Usage Flow
- Call this endpoint once when initializing the chat widget
- Store the returned token temporarily (in memory or sessionStorage)
- Use the token immediately to authenticate your WebSocket connection
- 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
- 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.
- 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.
- One-Time Event: You only need to authenticate once per connection. If the connection drops and reconnects, you must authenticate again.
- Connection State: After server.auth_success, your socket is ready to:
- Send messages via client.send_message
- Receive messages via server.new_message
- 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
-
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); }
}); ```
-
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);
}); ```
-
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); } }); ```
-
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;
}
});
Recommended Practice to generate a client_id
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
- 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)
-
Timestamp Component
-
Use milliseconds since Unix epoch for uniqueness
- 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;
}