Connect to Hoory WebSockets
WebSockets establish a continuous connection between the client and server, enabling bi-directional communication. Hoory utilizes this connection to provide real-time updates about platform events. To connect to the Hoory WebSocket, simply provide a token and follow the setup instructions outlined in this guide.
Why should I use a WebSocket connection?β
A WebSocket connection allows for real-time data updates, making it ideal for clients such as an Android or iOS client SDK for Hoory. This helps update the dashboard without the need to reload the page. Hence, it can enhance the user experience and improve an agent's productivity.
How to set up a WebSocket connection with Hoory?β
To set up a WebSocket connection with Hoory, you need to initiate a connection with the authentication PubSub token provided by Hoory. The URL for the connection is wss://app.hoory.com/cable
.
A PubSub token is a token that is used to authenticate a client when connecting to a PubSub (publish-subscribe) service. The client must present this token to the service in order to establish a connection and begin publishing or subscribing to messages.
There are two types of PubSub tokens available in Hoory, as listed below.
- User PubSub Token: This token has the privileges of an agent/admin and would receive all of the events listed later on the page.
- Contact PubSub Token: Hoory generates a unique PubSub token for each session a contact has. This token can be used to connect to the WebSocket and receive real-time updates for the same session. When a contact is created through the public APIs, the
pubsub_token
is included in the response payload. This token only grants access to events related to the current session, such asconversation.created
,conversation.status_changed
,message.created
,message.updated
,conversation_typing_on
,conversation_typing_off
andpresence.update
.
Please refer Client APIs to build real time customer facing integrations using Hoory.
Note: This token may be rotated regularly based on your installation type. Please ensure that you are using the latest token.
How to connect to Hoory WebSocket?β
To connect to the Hoory WebSocket, use the command subscribe
and include your pubSubToken
, accountId
, and userId
(if using a user token) in the connection request. Here is an example of how you can connect with Hoory.
// Add a helper method to convert JSON to a string
const stringify = (payload = {}) => JSON.stringify(payload);
const pubSubToken = "<contact/user-pub-sub-token>";
const accountId = "<your-account-id-in-integer>";
const userId = "<user-id-in-integer-if-using-user-token>";
const connection = new WebSocket("wss://app.hoory.com/cable");
connection.send(
stringify({
command: "subscribe",
identifier: stringify({
channel: "RoomChannel",
pubsub_token: pubSubToken,
account_id: accountId,
user_id: userId,
}),
})
);
// The expected string in connection.send is of the format:
// {"command":"subscribe","identifier":"{\\"channel\\":\\"RoomChannel\\",\\"pubsub_token\\":\\"your-pubsub-token\\",\\"account_id\\": account_id_integer,\\"user_id\\":user_id_integer }"}
Publishing presence to the WebSocket serverβ
To keep your usersβ status online in Hoory, you can send a presence update event to Hoory every 30 seconds. This action would keep the status of the agent/contact online.
How to update the presence of an agent/admin?
To update the presence of an agent or admin, send the following payload to the server:
const userPayload = stringify({
command: "message",
identifier: stringify({
channel: "RoomChannel",
pubsub_token: "<user-pubsub-token>",
account_id: accountId,
user_id: userId,
}),
data: stringify({ action: "update_presence" }),
});
connection.send(userPayload);
// The expected string in connection.send is of the format:
// {"command":"message","identifier":"{\\"channel\\":\\"RoomChannel\\",\\"pubsub_token\\":\\"your-pubsub-token\\",\\"account_id\\": account_id_integer,\\"user_id\\":user_id_integer ","data":"{\\"action\\":\\"update_presence\\"}"}
How to update the presence of a contact?
To update the presence of a contact, send the following payload to the server:
const agentPayload = stringify({
command: "message",
identifier: stringify({
channel: "RoomChannel",
pubsub_token: "<user-pubsub-token>",
}),
data: stringify({ action: "update_presence" }),
});
connection.send(agentPayload);
// The expected string in connection.send is of the format:
// {"command":"message","identifier":"{\\"channel\\":\\"RoomChannel\\",\\"pubsub_token\\":\\"your-pubsub-token\\","data":"{\\"action\\":\\"update_presence\\"}"}
WebSocket Payloadβ
Objectsβ
An event can contain any of the following objects as payload. Different types of objects supported in Hoory are as follows.
Conversation
The following payload will be returned for a conversation.
{
"additional_attributes": {
"browser": {
"device_name": "string",
"browser_name": "string",
"platform_name": "string",
"browser_version": "string",
"platform_version": "string"
},
"referer": "string",
"initiated_at": {
"timestamp": "iso-datetime"
}
},
"can_reply": "boolean",
"channel": "string",
"id": "integer",
"inbox_id": "integer",
"contact_inbox": {
"id": "integer",
"contact_id": "integer",
"inbox_id": "integer",
"source_id": "string",
"created_at": "datetime",
"updated_at": "datetime",
"hmac_verified": "boolean"
},
"messages": ["Array of message objects"],
"meta": {
"sender": {
// Contact Object
},
"assignee": {
// User Object
}
},
"status": "string",
"unread_count": "integer",
"agent_last_seen_at": "unix-timestamp",
"contact_last_seen_at": "unix-timestamp",
"timestamp": "unix-timestamp",
"account_id": "integer"
}
Contact
The following payload will be returned for a contact.
{
"additional_attributes": "object",
"custom_attributes": "object",
"email": "string",
"id": "integer",
"identifier": "string or null",
"name": "string",
"phone_number": "string or null",
"thumbnail": "string"
}
User
The following payload will be returned for an agent/admin.
{
"id": "integer",
"name": "string",
"available_name": "string",
"avatar_url": "string",
"availability_status": "string",
"thumbnail": "string"
}
Message
The following payload will be returned for a message.
{
"id": "integer",
"content": "string",
"account_id": "integer",
"inbox_id": "integer",
"message_type": "integer",
"created_at": "unix-timestamp",
"updated_at": "datetime",
"private": "boolean",
"status": "string",
"source_id": "string / null",
"content_type": "string",
"content_attributes": "object",
"sender_type": "string",
"sender_id": "integer",
"external_source_ids": "object",
"sender": {
"type": "string - contact/user"
// User or Contact Object
}
}
Notification
The following payload will be returned for a notification.
{
"id": "integer",
"notification_type": "string",
"primary_actor_type": "string",
"primary_actor_id": "integer",
"primary_actor": {
"can_reply": "boolean",
"channel": "string",
"id": "integer",
"inbox_id": "integer",
"meta": {
"assignee": {
"id": "integer",
"name": "string",
"available_name": "string",
"avatar_url": "string",
"type": "user",
"availability_status": "string",
"thumbnail": "string"
},
"hmac_verified": "boolean"
},
"agent_last_seen_at": "unix-timestamp",
"contact_last_seen_at": "unix-timestamp",
"timestamp": "unix-timestamp"
},
"read_at": "unix-timestamp",
"secondary_actor": "object/null",
"created_at": "unix-timestamp",
"account_id": "integer",
"push_message_title": "string"
}
Identifierβ
Each event will have an identifier
attribute in the following format.
{
"identifier": "{\\"channel\\":\\"RoomChannel\\",\\"pubsub_token\\":\\"token\\",\\"account_id\\":id,\\"user_id\\":user_id}"
}
Messageβ
Each event will include a message
attribute which we return the event name as well as the data associated with it. To see the list of events, refer the documentation below.
Types of Eventsβ
conversation.createdβ
This event is triggered when a new conversation is initiated. If subscribing to the contact's PubSub token, this event will only include data related to the specific session associated with the PubSub token.
Available to: agent/admin, contact
{
"message": {
"event": "conversation.created",
"data": {
// Conversation object will be available here
}
}
}
conversation.readβ
This event is triggered and sent to the agents/admins who have access to the inbox, when a contact has read a message.
Available to: agent/admin
{
"message": {
"event": "conversation.read",
"data": {
// Conversation object will be available here
}
}
}
message.createdβ
This event is triggered and sent to the agents, admins, contacts when a new message is created in a conversation they have access to.
Available to: agent/admin, contact
{
"message": {
"event": "message.created",
"data": {
// Message object will be available here
}
}
}
message.updatedβ
This event is triggered and sent to the agents, admins, contacts when a message is updated in a conversation they have access to.
Available to: agent/admin, contact
{
"message": {
"event": "message.updated",
"data": {
// Message object will be available here
}
}
}
conversation.status_changedβ
This event is sent to the agents, admins, contacts when a conversation status is updated.
Available to: agent/admin, contact
{
"message": {
"event": "conversation.status_changed",
"data": {
// Conversation object will be available here
}
}
}
conversation.typing_onβ
This event is sent to the agents, admins, contacts when a contact or an agent starts typing a response.
Available to: agent/admin, contact
{
"message": {
"event": "conversation.typing_on",
"data": {
"conversation": {
// Conversation object will be available here
},
"user": {
// Contact / Agent,Admin User object will be available here.
},
"is_private": "boolean", // Shows whether the agent is typing a private note or not.
"account_id": "integer"
}
}
}
conversation.typing_offβ
This event is sent to the agents, admins, contacts when a contact or an agent ends typing a response.
Available to: agent/admin, contact
{
"message": {
"event": "conversation.typing_off",
"data": {
"conversation": {
// Conversation object will be available here
},
"user": {
// Contact / User object will be available here.
},
"account_id": "integer"
}
}
}
assignee.changedβ
This event is sent to the agents/admins with access to an inbox when the assigned agent is changed.
Available to: agent/admin
{
"message": {
"event": "assignee.changed",
"data": {
// Conversation object will be available here
}
}
}
team.changedβ
This event is sent to the agents/admins with access to an inbox when the assigned team is changed.
Available to: agent/admin
{
"message": {
"event": "team.changed",
"data": {
// Conversation object will be available here
}
}
}
conversation.contact_changedβ
This event is sent to the agents/admins when two contacts are merged all their conversations are consolidated under one contact.
Available to: agent/admin
{
"message": {
"event": "conversation.contact_changed",
"data": {
// Conversation object will be available here
}
}
}
contact.createdβ
This event is sent to the agents/admins when a contact is created.
Available to: agent/admin
{
"message": {
"event": "contact.created",
"data": {
// Contact object will be available here
}
}
}
contact.updatedβ
This event is sent to the agents/admins when a contact is updated.
Available to: agent/admin
{
"message": {
"event": "contact.updated",
"data": {
// Contact object will be available here
}
}
}
presence.updateβ
Available for both agent and the contact, this event provides real-time updates on the availability status of the users in the system. The event delivered to contacts will not include information about other contacts' availability status.
Available to: agent/admin
{
"message": {
"event": "presence.update",
"data": {
"account_id": "integer",
"users": {
"user-id": "string"
},
"contacts": {
"contact-id": "string"
}
}
}
}
notification_createdβ
This event is sent to the agents/admins when a notification is created.
Example Implementation in Node.jsβ
const EventEmitter = require("events");
const WS = require("ws");
const axios = require("axios");
class Contact {
/**
* @param {Object} args
* @param {string} args.identifier
* @param {string} args.phone_number
* @param {Object} [args.email]
* @param {Object} [args.custom_attributes]
*/
}
class ConversationResponse {
/**
* @param {Object} args
* @param {number} args.id
*/
}
class ContactResponse {
/**
* @param {Object} args
* @param {string} args.source_id
* @param {string} args.pubsub_token
* @param {number} args.id
* @param {string} args.name
* @param {string} args.email
* @param {string} args.phone_number
*/
}
class MessageData {
/**
* @param {Object} args
* @param {string} args.content
* @param {number} args.message_type
* @param {boolean} args.private
*/
}
class InboxService extends EventEmitter {
constructor() {
super();
this.url = "http://app.hoory.com";
this.wsUrl = "wss://app.hoory.com";
/** @type {string} */
this.inboxIdentifier = null;
/** @type {ContactResponse} */
this.contactResponse = null;
/** @type {ConversationResponse} */
this.conversationResponse = null;
}
/**
* @param {string} inboxIdentifier
* @param {Contact} contact
* @returns {Promise<void>}
*/
async init(inboxIdentifier, contact) {
this.inboxIdentifier = inboxIdentifier;
let response = await this.createContact(contact);
this.contactResponse = new ContactResponse(response.data);
response = await this.createConversation(this.contactResponse.source_id);
this.conversationResponse = new ConversationResponse(response.data);
this.wsSubscribe(this.contactResponse.pubsub_token);
}
/**
* @param {Inbox} inbox
* @param {Contact} contact
* @returns {Promise<Response>}
*/
async createContact(contact) {
const response = await axios.post(
`${this.url}/public/api/v1/inboxes/${this.inboxIdentifier}/contacts`,
contact
);
return response;
}
/**
* @param {Inbox} inbox
* @returns {Promise<Response>}
*/
async createConversation() {
const response = await axios.post(
`${this.url}/public/api/v1/inboxes/${this.inboxIdentifier}/contacts/${this.contactResponse.source_id}/conversations`,
{}
);
return response;
}
/**
* @param {string} content
* @returns {Promise<Response>}
*/
async createMessage(content) {
const response = await axios.post(
`${this.url}/public/api/v1/inboxes/${this.inboxIdentifier}/contacts/${this.contactResponse.source_id}/conversations/${this.conversationResponse.id}/messages`,
{
content,
}
);
return response;
}
/**
* @param {ContactPubsubToken} ContactPubsubToken
* @returns {void}
*/
wsSubscribe(ContactPubsubToken) {
const ws = new WS(`${this.wsUrl}/cable`);
ws.on("error", console.error);
ws.once("open", () => {
ws.send(
JSON.stringify({
command: "subscribe",
identifier: JSON.stringify({
channel: "RoomChannel",
pubsub_token: ContactPubsubToken,
}),
})
);
ws.on("message", (data) => {
this.onMessage(data);
});
});
}
/**
* @param {string} data - JSON string
* @returns {void}
*/
onMessage(data) {
const message = JSON.parse(data).message;
console.info(`Received message: ${message.event}`);
this.emit("message", message);
}
}
module.exports = { InboxService };