Build Ringnity inbox, presence, and calls inside a CRM workspace
This guide is for internal CRM, helpdesk, support desk, and agent workspaces. It covers Agent SDK token flow, package setup, profile, presence, inbox, messages, realtime events, and agent call controls.
Use the Agent SDK when agents should work from an existing CRM instead of the full Ringnity dashboard. The SDK is headless: your CRM owns the UI while Ringnity owns tenant permissions, conversation rules, realtime events, and call state.
Agent Inbox
online#1042
openRefund request
#1043
ringingNeed video support
#1044
newBilling question
Conversation #1043
Assigned to Agent One
Overview
What this SDK is for
Use this SDK when an internal CRM or support workspace needs Ringnity agent functions without opening the full Ringnity dashboard.
Agent / CRM Workspace
This page covers internal users: CRM inbox, replies, internal notes, presence, realtime events, call controls, and agent visibility rules.
Backend Required
Agent SDK tokens must be created by your CRM backend using a private Server API key. Do not create them from browser code.
Not Visitor SDK
Public website visitor chat, AI, and customer-side calls belong to Web/Mobile Visitor SDK docs. Agent SDK is for authenticated internal workspace users.
Server and SDK topology
Architecture that must be set up
The CRM frontend embeds the Agent SDK, but your CRM backend is the trust boundary. The backend validates the internal user and requests a short-lived Agent SDK token from Ringnity.
CRM Workspace
Internal app with Agent SDK
CRM Backend
Owned by the customer or integrator
Ringnity Cloud
Tenant, inbox, calls, realtime
Do not put the Server API key in CRM frontend code
Browser CRM code may receive a short-lived Agent SDK token. It must not contain the Server API key, owner credentials, or long-lived admin credentials.
Overview
Agent workspace capability map
These are internal support capabilities exposed by the Agent SDK. They are for authenticated agents, supervisors, admins, and CRM operators.
Agent Profile
Read the current agent, tenant, role, department, and granted scopes.
Presence
Set agents online, busy, unavailable, or offline from your CRM workspace.
CRM Inbox
List assigned, department, open, and closed conversations within agent visibility rules.
Messages
Read message threads, send replies, add internal notes, and mark messages as read.
Realtime Events
Connect to Ringnity realtime and receive presence, message, conversation, and call events.
Call Controls
Receive incoming calls, accept, reject, hold, resume, transfer, mute, end, and manage video.
Server
Server requirements
The backend endpoint authenticates the internal CRM user, maps that user to a Ringnity agent, requests a scoped Agent SDK token, then returns only that token to the CRM frontend.
Backend checklist
CRM compatibility
Server
Create the Agent SDK token endpoint
Keep secrets on your backend while the CRM frontend receives only a short-lived scoped token. Request the minimum scopes needed for the current workspace.
RINGNITY_API_BASE_URL=https://api.ringnity.com
RINGNITY_SERVER_API_KEY=sk_live_or_test_from_ringnity_dashboardimport express from "express";
const app = express();
app.use(express.json());
async function requireCrmUser(req) {
// Replace this with your CRM session, SSO, or JWT validation.
return {
id: req.body.userId,
ringnityUsername: req.body.username,
};
}
app.post("/ringnity/agent-token", async (req, res) => {
const crmUser = await requireCrmUser(req);
const response = await fetch(
`${process.env.RINGNITY_API_BASE_URL}/api/server/sdk-token/agent`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.RINGNITY_SERVER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
username: crmUser.ringnityUsername,
scopes: [
"agent:profile:read",
"agent:presence:read",
"agent:conversations:read",
"agent:conversations:write",
"agent:messages:read",
"agent:messages:write",
"agent:calls:read",
"agent:calls:write",
"agent:devices:write"
],
expiresIn: 900
}),
},
);
const envelope = await response.json();
if (!response.ok || !envelope.success) {
res.status(502).json({ message: "Ringnity Agent SDK token request failed." });
return;
}
res.json({
token: envelope.data.token,
expiresIn: envelope.data.expiresIn,
tokenType: "Bearer",
});
});
app.listen(3000);CRM receives
{
"token": "eyJhbGciOi...",
"expiresIn": 900,
"tokenType": "Bearer"
}Token rules
Default expiry should be short, for example 900 seconds.
Maximum expiry should remain limited, for example 3600 seconds.
Server API key should include only the permission required to create agent tokens.
Server
Token lifecycle
Agent tokens should follow the CRM session, not replace it. Your CRM remains the source of truth for internal user authentication.
Create
CRM backend validates the internal user and returns a short-lived scoped Agent SDK token.
Use
CRM frontend sends the token to Ringnity for profile, presence, inbox, messages, realtime, and calls.
Refresh
When expired, ask the CRM backend for a new token and recreate or refresh the SDK client.
Server
Security checklist
Use this checklist before sharing an Agent SDK CRM build with real agents.
CRM Setup
Download what you need
Current downloadable SDK bundle: 0.2.0-beta.0. Use the Agent SDK in the CRM frontend and the Server API SDK in the CRM backend token endpoint.
Agent SDK
TypeScript SDK for CRM and internal support workspaces, with profile, presence, inbox, realtime events, and call controls.
Server API SDK
Backend helper package for creating Agent SDK tokens. Keep the Server API key inside this backend only.
OpenAPI Contract
Use the live contract for exact REST schemas, fields, and status codes.
Bruno Collection
API smoke-test collection for backend, SDK token, chat, AI, calls, reports, and webhooks.
CRM Setup
Install the Agent SDK
During preview, download the source package and install it from a local package path. When public package publishing is enabled, this section can switch to npm coordinates.
# Pilot install from a local source package
npm install ./ringnity-agent-sdk
# or, after public package publishing
npm install @ringnity/agent-sdkCRM Setup
Initialize SDK
Create the SDK from the CRM app shell, support module, route loader, or workspace provider. The token request must go to your CRM backend.
import { RingnityAgent } from '@ringnity/agent-sdk';
async function fetchAgentSdkToken() {
const response = await fetch('/ringnity/agent-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
userId: currentCrmUser.id,
username: currentCrmUser.ringnityUsername,
}),
});
if (!response.ok) {
throw new Error('Ringnity Agent SDK token request failed');
}
const payload = await response.json();
return payload.data ?? payload;
}
const agent = await RingnityAgent.create({
tokenProvider: fetchAgentSdkToken,
onError: (error) => {
console.warn(error.code, error.message);
},
});Workspace APIs
Profile and presence
Use profile and presence to show the current Ringnity agent, tenant, role, department, scope list, and live availability status.
const me = await agent.profile.get();
const presence = await agent.presence.get();
updatePresenceBadge(presence.status);Workspace APIs
CRM inbox
Use the inbox API when your CRM owns conversation lists, filters, tabs, pagination, assignment badges, and routing states.
const inbox = await agent.conversations.list({
status: 'open',
limit: 20,
});
for (const conversation of inbox.conversations) {
renderInboxRow({
id: conversation.conversationId,
number: conversation.conversationNumber,
subject: conversation.subject,
priority: conversation.priority,
assignedAgentId: conversation.assignedAgentId,
lastMessageAt: conversation.lastMessageAt,
});
}
if (inbox.nextCursor) {
const nextPage = await agent.conversations.list({
status: 'open',
cursor: inbox.nextCursor,
});
}Workspace APIs
Messages and internal notes
Read a conversation thread, send public replies, create internal notes, and mark messages as read from the CRM UI.
const conversationId = selectedConversationId;
const thread = await agent.conversations.messages(conversationId, {
limit: 50,
});
renderThread(thread.messages);
await agent.conversations.reply(conversationId, {
body: 'Hi, how can I help?',
});
await agent.conversations.reply(conversationId, {
body: 'Customer asked about billing.',
messageType: 'internal_note',
});
const lastMessage = thread.messages.at(-1);
if (lastMessage) {
await agent.conversations.markRead(conversationId, lastMessage.messageId);
}Workspace APIs
Realtime events
Connect realtime only while the workspace needs live updates. Disconnect when the CRM tab closes or the user leaves the agent workspace.
await agent.connect();
const offConversation = agent.on('conversation.updated', (conversation) => {
updateInboxRow(conversation);
});
const offMessage = agent.on('chat.message.created', (message) => {
appendMessage(message);
});
const offError = agent.on('error', (error) => {
showWorkspaceError(error.code, error.message);
});
await agent.conversations.subscribe(selectedConversationId);
// When the CRM page or tab closes:
offConversation();
offMessage();
offError();
agent.disconnect();Workspace APIs
Conversation visibility rules
Agent SDK follows Ringnity tenant visibility. Your CRM should not display conversations the Agent SDK cannot read or write.
Calls
Go online for calls
When the agent is ready to receive Ringnity calls, connect realtime and register the agent as available for voice or video.
await agent.calls.goOnline({
accepts: 'voice',
});
// For video-capable queues:
await agent.calls.goOnline({
accepts: 'video',
video: true,
});
// When the agent takes a break:
await agent.calls.goOffline();Calls
Incoming call popup
Your CRM owns the popup and buttons. The SDK handles realtime call events, media permission, call setup, and cleanup.
agent.calls.onIncomingCall((call) => {
showIncomingCallPopup({
customerName: call.customer.name ?? 'Customer',
type: call.type,
onAccept: async () => {
await call.accept({
video: call.type === 'video',
view: {
local: document.querySelector('#agent-video'),
remote: document.querySelector('#customer-video'),
},
});
showActiveCallPanel(call);
},
onReject: () => call.reject('Agent declined'),
});
});Calls
Active call controls
Wire your own CRM buttons to the current call object. Keep controls disabled until the call is accepted or active.
document.querySelector('#mute').onclick = () => call.mute();
document.querySelector('#unmute').onclick = () => call.unmute();
document.querySelector('#hold').onclick = () => call.hold();
document.querySelector('#resume').onclick = () => call.resume();
document.querySelector('#transfer').onclick = () =>
call.transfer({ departmentId: selectedDepartmentId });
document.querySelector('#end').onclick = () => call.end();Calls
Video view
Attach local and remote media elements when the CRM owns the video layout.
call.attachView({
local: document.querySelector('#agent-video'),
remote: document.querySelector('#customer-video'),
});
document.querySelector('#accept').onclick = () =>
call.accept({ video: call.type === 'video' });Calls
Team groups and audio/video call rooms
Use one Agent SDK runtime for startup/reconnect recovery, group REST, realtime control, and managed media across TypeScript, Flutter, Android/Kotlin, Swift, and React Native. Ringnity owns authorization, call policy, direct-to-group upgrades, and durable chat; LiveKit owns only media signaling and tracks. Group calls are pre-GA: Business entitlement and team.calls.capabilities() are authoritative while multi-device acceptance and controlled rollout remain in progress.
const { group } = await agent.team.groups.create({
title: 'Customer Success',
inviteeAgentIds: ['agent-2', 'agent-3'],
clientRequestId: crypto.randomUUID(),
});
// In agent-2's authenticated session:
const invitation = (await invitedAgent.team.groups.invitations())[0];
await invitedAgent.team.groups.acceptInvitation(invitation.invitationId);
const capabilities = await agent.team.calls.capabilities();
const call = await agent.team.calls.createRoom({
sourceConversationId: group.conversationId,
initialMedia: capabilities.videoEnabled ? 'video' : 'voice',
audienceScope: 'selected_members',
inviteeAgentIds: ['agent-2'],
clientRequestId: crypto.randomUUID(),
});
const media = await agent.team.calls.joinRoom(call.callRoomId, {
microphoneEnabled: true,
cameraEnabled: true,
deviceSessionId,
});
media.attachVideo(call.participants[0].callRoomParticipantId, videoElement);
// Events are hints. Recover active and joinable rooms after startup/reconnect.
const visibleRooms = await agent.team.calls.listRooms();
// Promote an answered direct call to 3+ participants without provider choice.
const upgraded = await agent.team.calls.createRoom({
sourceConversationId: activeCall.conversationId,
initialMedia: activeCall.callChannel,
audienceScope: 'selected_members',
inviteeAgentIds: ['agent-3'],
upgradeFromCallSessionId: activeCall.id,
clientRequestId: crypto.randomUUID(),
});
// Leave only yourself; hosts use endRoom() to end for everyone.
await agent.team.calls.leaveRoom(call.callRoomId);Operations
Lifecycle
Keep setup predictable by treating Agent SDK as a runtime service owned by the CRM workspace.
Agent opens the CRM workspace.
CRM frontend asks the CRM backend for an Agent SDK token.
CRM backend validates the internal user and maps it to a Ringnity agent.
CRM backend calls Ringnity Server API and returns a short-lived Agent SDK token.
CRM frontend creates RingnityAgent with the token.
Agent profile, presence, inbox, messages, and calls become available.
CRM connects realtime only while the workspace needs live updates.
Agent goes online for calls when ready to receive customer calls.
CRM disconnects realtime and refreshes the token when the page closes or token expires.
Operations
Agent SDK object model
Use this cheat sheet when mapping CRM screens to SDK calls. Keep CRM UI names friendly, but wire the implementation through these public methods.
Read the current agent, tenant, role, department, and scopes.
Read whether the agent is online, busy, offline, unavailable, or in an active call.
Show open, closed, or department-filtered conversations.
Read a single conversation summary.
Read the message thread for one conversation.
Subscribe to realtime updates for one conversation.
Send an agent reply or internal note.
Mark a message as read by this agent.
Accept or transfer a conversation.
Soft-delete an attachment message for auditability.
Close a conversation and record the reason.
Open or close the realtime workspace connection.
Listen to ready, conversation, message, and call events.
Make this agent available for Ringnity calls.
Mark the agent unavailable and close the live workspace connection.
Run your own UI when a customer call arrives.
Accept a customer call and let the SDK handle call setup.
Decline an incoming call.
End the active call and clean up local media.
Put an active call on hold or resume it.
Transfer the call to another department.
Control the local microphone.
Choose where optional video appears in your CRM page.
Use direct Team chat without a Dashboard session.
Create and manage membership-scoped Team groups.
Handle durable invitations, roles, and membership lifecycle.
Read video rollout and ephemeral STUN/TURN configuration.
Recover and control selected or all-member managed call rooms.
Join the backend-selected media session and keep leave separate from end-for-everyone.
Control an agent-to-agent audio or video call.
Exchange and recover ephemeral Team SDP/ICE through the authenticated socket.
Register or disable an Agent SDK push subscription.
Operations
Full call playground
Use the plain HTML playground when you want to test the token endpoint, go-online button, incoming popup, active call panel, transfer input, and optional video containers in one place.
The repository includes a CRM call page at sun_webrtc_sdk/examples/agent-call-playground. It is useful before embedding the Agent SDK into a real CRM route.
cd sun_webrtc_sdk/examples/agent-call-playground
npm install --package-lock=false
npm run dev
# Then update TOKEN_ENDPOINT in index.html to your backend route:
# /ringnity/agent-token
# or, for the current demo customer backend:
# https://sadavir.id/ringnity/agent-tokenOperations
Scopes
Request only the scopes your CRM workspace needs. For read-only inbox previews, avoid write and call scopes.
Operations
Troubleshooting
Most Agent SDK failures come from expired tokens, missing scopes, CRM user mapping, or realtime connection setup.
AGENT_TOKEN_UNAVAILABLE
Check that the token provider returned token and expiresIn from the CRM backend.
AGENT_SDK_TOKEN_INVALID
Check expiry, signing key, tenant, username, and whether the CRM user maps to an existing Ringnity agent.
AGENT_SDK_SCOPE_DENIED
Request the minimum scope required for the method being called.
AGENT_SDK_CONVERSATION_NOT_FOUND
Check visibility rules, department assignment, and conversation id.
AGENT_REALTIME_CONNECT_FAILED
Check websocket connectivity, token validity, API base URL, and proxy settings.
CALL_ACCEPT_FAILED
Check browser microphone/camera permissions and whether local/remote media elements are valid.
Operations
Exact API reference
Agent SDK methods above are the recommended public surface. When you need exact request fields, response shapes, status codes, or auth schemes, use the Scalar API reference generated from OpenAPI.
Operations
Full example
Use this as the shape for a CRM workspace page with inbox, thread, realtime updates, and incoming calls.
import { RingnityAgent } from '@ringnity/agent-sdk';
async function fetchAgentSdkToken() {
const response = await fetch('/ringnity/agent-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
userId: currentCrmUser.id,
username: currentCrmUser.ringnityUsername,
}),
});
const payload = await response.json();
return payload.data ?? payload;
}
const agent = await RingnityAgent.create({
tokenProvider: fetchAgentSdkToken,
});
await agent.connect();
agent.on('chat.message.created', (message) => {
appendMessage(message);
});
agent.on('conversation.updated', (conversation) => {
updateInboxRow(conversation);
});
const inbox = await agent.conversations.list({
status: 'open',
limit: 20,
});
renderInbox(inbox.conversations);
async function openConversation(conversationId) {
const thread = await agent.conversations.messages(conversationId);
renderThread(thread.messages);
await agent.conversations.subscribe(conversationId);
}
async function sendReply(conversationId, body) {
await agent.conversations.reply(conversationId, { body });
}
await agent.calls.goOnline();
agent.calls.onIncomingCall((call) => {
showIncomingCallPopup({
customerName: call.customer.name ?? 'Customer',
onAccept: async () => {
await call.accept({
video: call.type === 'video',
view: {
local: document.querySelector('#agent-video'),
remote: document.querySelector('#customer-video'),
},
});
showActiveCallPanel(call);
},
onReject: () => call.reject('Agent declined'),
});
});
window.addEventListener('beforeunload', () => {
agent.disconnect();
});Related docs and examples
Last updated: June 30, 2026.
Turn Your Website Into a Real-Time Call Center
Let customers call your team directly from your website, no phone numbers and no apps required. Just add one <script>.
