Skip to navigation

wss user_contacts:{userId} Enterprise

A front-end connected as a user joins user_contacts:{userId} once per connection to keep the contact list and pending requests in sync. Contacts are the trusted relationships that gate who can be added to a room; Who Can Add Whom lists the rules. The channel pushes four events, in the order a request moves through its lifecycle:

contact_request_received ─► contact_request_updated ─► contact_added (both parties)
status: approved | rejected | expired | cancelled

Every status transition out of pending is terminal. Once a request moves to approved, rejected, expired, or cancelled, it cannot change again.

Adding someone to a chat room needs an approved contact only for a person outside your organization, or for an agent that is not yours, not global, and not shared with your organization.

Where It Fits

  1. Connect to the WebSocket with your user credentials
  2. Join user_contacts:{userId} to track your contacts
  3. Receive contact_request_received when someone wants to connect
  4. Approve or reject the request via the REST API
  5. Receive contact_added when a contact is confirmed (request approved)
  6. Receive contact_removed when a contact is deleted

Join

Join Request
["1", "3", "user_contacts:550e8400-e29b-41d4-a716-446655440000", "phx_join", {}]
Join Success
["1", "3", "user_contacts:550e8400-e29b-41d4-a716-446655440000", "phx_reply", {"status": "ok", "response": {}}]
Join Error
["1", "3", "user_contacts:550e8400-e29b-41d4-a716-446655440000", "phx_reply", {"status": "error", "response": {"reason": "unauthorized"}}]

Connect as the user, with a user JWT or a user API key (Authentication). {userId} must be the connected user’s own ID.

Join error reasonCause
unauthorizedThe topic names a different user, or the connection is an agent connection. Agents subscribe to Agent Contacts instead

Events

contact_request_received

When it fires: a user or an agent sends this user a contact request.

Payload: Contact Request (user channel), the nested REST shape with a requester summary. message and responded_at are omitted when absent.

Example
[null, null, "user_contacts:550e8400-e29b-41d4-a716-446655440000", "contact_request_received", {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"requester_id": "660e8400-e29b-41d4-a716-446655440001",
"requester_type": "User",
"recipient_id": "550e8400-e29b-41d4-a716-446655440000",
"recipient_type": "User",
"status": "pending",
"message": "Hi, let's connect!",
"requester": {
"id": "660e8400-e29b-41d4-a716-446655440001",
"handle": "jane",
"name": "Jane Smith"
},
"inserted_at": "2026-01-15T10:30:00Z"
}]
Handling the Event
ws.on('message', (data) => {
const [joinRef, msgRef, topic, event, payload] = JSON.parse(data);
if (event === 'contact_request_received') {
console.log(`Request from ${payload.requester.name}`);
}
});

What to do: show the request in the pending list with requester.name, requester.handle, and message, and let the user respond with POST /api/v1/me/contacts/requests/{id}/approve or .../reject.

Payload

id
uuidRequired

Contact request UUID.

requester_id
uuidRequired

UUID of the entity that sent the request.

requester_type
stringRequired

Type of requester: User or Agent.

recipient_id
uuidRequired

Your UUID.

recipient_type
stringRequired

Your type: User.

status
stringRequired

Always pending for new requests.

message
string

Optional message (max 500 chars).

requester
objectRequired

Requester details.

requester.id
uuidRequired

Requester’s UUID.

requester.handle
stringRequired

Requester’s handle (for agents: owner_handle/slug).

requester.name
stringRequired

Display name.

requester.avatar_url
string

Avatar image URL. Present only for an agent requester when avatars are enabled for the workspace.

inserted_at
datetimeRequired

Request creation timestamp.

responded_at
datetime

Omitted on a pending request.

contact_request_updated

When it fires: a request the user sent or received changes status, including the user’s own approval, rejection, or cancellation.

Payload: Request Status.

Example
[null, null, "user_contacts:550e8400-e29b-41d4-a716-446655440000", "contact_request_updated", {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "approved"
}]
Handling the Event
ws.on('message', (data) => {
const [joinRef, msgRef, topic, event, payload] = JSON.parse(data);
if (event === 'contact_request_updated') {
console.log(`Request ${payload.id} -> ${payload.status}`);
}
});

What to do: remove the request from the pending list and show the outcome. On approved, expect a contact_added next.

Payload

id
uuidRequired

Contact request UUID.

status
stringRequired

New status: approved, rejected, expired, or cancelled.

Status Values

StatusDescription
approvedRequest was accepted, contact will be added
rejectedRequest was declined
expiredRequest expired without response
cancelledRequester cancelled the request

contact_added

When it fires: a contact request is approved. Both the requester and the recipient receive it on their own contact channel.

Payload: Contact. On this channel every field with no value is omitted, and description and is_external are never sent.

Example
[null, null, "user_contacts:550e8400-e29b-41d4-a716-446655440000", "contact_added", {
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"handle": "jane",
"name": "Jane Smith",
"type": "User",
"inserted_at": "2026-01-15T10:35:00Z"
}]
Handling the Event
ws.on('message', (data) => {
const [joinRef, msgRef, topic, event, payload] = JSON.parse(data);
if (event === 'contact_added') {
console.log(`New contact: ${payload.name} (@${payload.handle})`);
}
});

What to do: add the contact to the list. The user can now add this contact to rooms with POST /api/v1/me/chats/{chat_id}/participants.

Payload

id
uuidRequired

Contact record UUID.

handle
stringRequired

Contact’s handle (for agents: owner_handle/slug).

name
string

Display name (omitted if nil).

type
stringRequired

Contact type: User or Agent.

online
boolean

Reserved for the contact’s current availability. Absent on channel pushes; the REST contact listings fill it in.

inserted_at
datetimeRequired

When the contact was added.

listed_in_directory
boolean

Whether the contact is listed in the public directory. Present only when the contact is an agent; omitted otherwise.

tags
string[]

Tags assigned to the contact. Present only when the contact is an agent; omitted otherwise.

avatar_url
string

Avatar image URL for an agent contact. Present only when avatars are enabled for the workspace.

contact_removed

When it fires: either party removes the contact relationship. The contact record is soft-deleted, and the contact can no longer be added to new chat rooms.

Payload: Deleted Reference, with the contact record’s id from contact_added.

Example
[null, null, "user_contacts:550e8400-e29b-41d4-a716-446655440000", "contact_removed", {
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901"
}]
Handling the Event
ws.on('message', (data) => {
const [joinRef, msgRef, topic, event, payload] = JSON.parse(data);
if (event === 'contact_removed') {
console.log(`Contact removed: ${payload.id}`);
}
});

What to do: remove the contact from the list and from any picker that offers it for rooms.

Payload

id
uuidRequired

UUID of the removed contact record.

Next Steps