Agent CRM SDK Implementation

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.

crm-workspace

Agent Inbox

online

#1042

open

Refund request

#1043

ringing

Need video support

#1044

new

Billing question

Conversation #1043

Assigned to Agent One

Can you help me by video?
Yes. I will start a secure session.
Incoming video call
Reply as agent...

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

No Server API key
Uses short-lived agent token

CRM Backend

Owned by the customer or integrator

Validates CRM user
Creates Agent SDK token

Ringnity Cloud

Tenant, inbox, calls, realtime

Validates token scopes
Processes workspace actions
1. CRM requests token
2. Backend validates user
3. Backend calls Ringnity
4. Agent token returns
5. CRM opens inbox/calls

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

Ready

Read the current agent, tenant, role, department, and granted scopes.

Presence

Ready

Set agents online, busy, unavailable, or offline from your CRM workspace.

CRM Inbox

Ready

List assigned, department, open, and closed conversations within agent visibility rules.

Messages

Ready

Read message threads, send replies, add internal notes, and mark messages as read.

Realtime Events

Ready

Connect to Ringnity realtime and receive presence, message, conversation, and call events.

Call Controls

Ready

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

Ringnity tenant with at least one workspace agent.
Server API key with permission to create Agent SDK tokens.
CRM backend endpoint, for example POST /ringnity/agent-token.
CRM authentication that maps the logged-in CRM user to a Ringnity agent username or id.
HTTPS in staging and production.
No Server API key inside browser bundles, CRM frontend source, logs, or local storage.

CRM compatibility

RuntimeBrowser CRM or internal web app
Package@ringnity/agent-sdk
BackendRequired for Agent SDK token creation
RealtimeSocket connection while workspace is active
CallsMicrophone and camera browser permissions
SecurityShort-lived scoped token per CRM user

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.

.env
RINGNITY_API_BASE_URL=https://api.ringnity.com
RINGNITY_SERVER_API_KEY=sk_live_or_test_from_ringnity_dashboard
server.js
import 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

json
{
  "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.

Server API key is stored only in backend environment variables or secret manager.
CRM backend verifies the internal user before issuing an Agent SDK token.
CRM user is mapped to the intended Ringnity username or agent id.
Agent token scopes are limited to the workspace features being used.
Token endpoint is protected by HTTPS, authentication, CSRF strategy, and rate limiting.
Logs do not print full Agent SDK token, Server API key, or Ringnity credential values.

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.

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.

bash
# Pilot install from a local source package
npm install ./ringnity-agent-sdk

# or, after public package publishing
npm install @ringnity/agent-sdk

CRM 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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

The conversation is assigned to the agent.
The agent is an active participant in the conversation.
The conversation is unassigned and belongs to the agent department.
The conversation is unassigned and has no department.
Supervisor or admin roles can read tenant conversations without the regular agent filter.

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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

1

Agent opens the CRM workspace.

2

CRM frontend asks the CRM backend for an Agent SDK token.

3

CRM backend validates the internal user and maps it to a Ringnity agent.

4

CRM backend calls Ringnity Server API and returns a short-lived Agent SDK token.

5

CRM frontend creates RingnityAgent with the token.

6

Agent profile, presence, inbox, messages, and calls become available.

7

CRM connects realtime only while the workspace needs live updates.

8

Agent goes online for calls when ready to receive customer calls.

9

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.

agent.profile.get()

Read the current agent, tenant, role, department, and scopes.

agent.presence.get()

Read whether the agent is online, busy, offline, unavailable, or in an active call.

agent.conversations.list(options)

Show open, closed, or department-filtered conversations.

agent.conversations.get(id)

Read a single conversation summary.

agent.conversations.messages(id, options)

Read the message thread for one conversation.

agent.conversations.subscribe(id)

Subscribe to realtime updates for one conversation.

agent.conversations.reply(id, input)

Send an agent reply or internal note.

agent.conversations.markRead(id, messageId)

Mark a message as read by this agent.

agent.conversations.assign(id, input)

Accept or transfer a conversation.

agent.conversations.deleteAttachment(id, messageId, input)

Soft-delete an attachment message for auditability.

agent.conversations.close(id, input)

Close a conversation and record the reason.

agent.connect() / agent.disconnect()

Open or close the realtime workspace connection.

agent.on(event, handler)

Listen to ready, conversation, message, and call events.

agent.calls.goOnline(options)

Make this agent available for Ringnity calls.

agent.calls.goOffline()

Mark the agent unavailable and close the live workspace connection.

agent.calls.onIncomingCall(handler)

Run your own UI when a customer call arrives.

call.accept(options)

Accept a customer call and let the SDK handle call setup.

call.reject(reason)

Decline an incoming call.

call.end()

End the active call and clean up local media.

call.hold() / call.resume()

Put an active call on hold or resume it.

call.transfer({ departmentId })

Transfer the call to another department.

call.mute() / call.unmute()

Control the local microphone.

call.attachView({ local, remote })

Choose where optional video appears in your CRM page.

agent.team.chats.list() / open() / send()

Use direct Team chat without a Dashboard session.

agent.team.groups.create() / list() / invite()

Create and manage membership-scoped Team groups.

agent.team.groups.acceptInvitation() / updateMember() / leave()

Handle durable invitations, roles, and membership lifecycle.

agent.team.calls.capabilities() / iceServers()

Read video rollout and ephemeral STUN/TURN configuration.

agent.team.calls.listRooms() / createRoom() / invite()

Recover and control selected or all-member managed call rooms.

agent.team.calls.joinRoom() / leaveRoom() / endRoom()

Join the backend-selected media session and keep leave separate from end-for-everyone.

agent.team.calls.create() / accept() / decline() / end()

Control an agent-to-agent audio or video call.

agent.team.calls.signal() / syncSignals()

Exchange and recover ephemeral Team SDP/ICE through the authenticated socket.

agent.devices.register() / unregister()

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.

bash
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-token

Operations

Scopes

Request only the scopes your CRM workspace needs. For read-only inbox previews, avoid write and call 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

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.

TypeScript
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();
});

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>.