A Vobiz calling panel inside Freshdesk - browser-based inbound and outbound calls, click-to-call, and recording playback for agents in 130+ countries.
A calling panel that puts Vobiz telephony inside Freshdesk. Agents place and receive real phone calls from the browser and play back recordings without leaving the helpdesk.
Source code: vobiz-ai/Vobiz-Freshdesk-Calling - the Freshworks app used throughout this guide, plus a zero-dependency mock backend so you can develop the panel without a Vobiz account.
ℹ️ Note: Scope: inbound and outbound, with browser audio over WebRTC. This installs as a custom app on your own account - there is no Marketplace review to wait for, and it is usually live within about thirty minutes of upload.
⚠️ Warning: This replaces your current telephony. Freshdesk allows only one CTI app to be active at a time, and Freshcaller cannot be enabled alongside one. Installing this takes over from whatever telephony the account uses today.
| Click-to-call | Click any phone number in Freshdesk and the panel opens and dials |
| Outbound calls | Dial from the panel with a number picked from your Vobiz account |
| Inbound calls | Enable once, and calls to the agent's number ring the panel |
| Recording playback | Recent recordings listed and playable in the panel |
| Browser audio | Calls run over WebRTC in the tab - no desk phone, no desktop app |
textFreshdesk --cti.triggerDialer--> the panel --HTTPS--> your calling backend --> Vobiz REST API | | +----------- SIP over WebSocket -------------------+ (audio in the browser)
Freshdesk's telephony surface is small. It exposes exactly one event to a calling app:
jsclient.events.on("cti.triggerDialer", cb); // the only cti.* event that exists
It fires when an agent clicks a phone number on a ticket or contact page. Installing a CTI app is what makes those numbers clickable in the first place - the app registers nothing to enable it.
There is no inbound-call event, no agent-presence event and no call-ended event. Everything else flows the other way: the app tells Freshdesk what happened through client.interface.trigger and the Freshdesk REST API. The depth of any Freshdesk CTI integration is therefore a function of how much the app chooses to write back.
The whole Freshworks surface this app uses today:
| Call | Purpose |
|---|---|
app.initialized() | Boot |
client.iparams.get() | Read the installation settings |
client.events.on("cti.triggerDialer") | Receive click-to-call |
client.interface.trigger("show", { id: "softphone" }) | Open the panel when a call starts |
The agent clicks Call, the panel POSTs /start-call, the backend has Vobiz dial the customer first, the customer answering triggers the /answer webhook which returns a <Dial><User> element, and Vobiz then dials into the agent's browser where JsSIP auto-answers.
⚠️ Warning: The order cannot be reversed. The Vobiz REST API cannot originate a call to a registered WebRTC endpoint - it returns
Endpoint Not Registered. So the customer is dialled first and the agent is bridged in second.Tell your agents about the ringback. A customer who answers and hears a second of tone sometimes hangs up, thinking the call failed.
The agent clicks Enable inbound calls to this panel once. The backend creates (or reuses) a Vobiz application pointing at its own /inbound-answer webhook and attaches the agent's selected number to it. Real calls to that number then hit /inbound-answer, which dials sip:... - landing on the same auto-answer path as the outbound bridge leg.
Keep the Freshdesk tab open; the panel has to stay registered to receive the call.
| Requirement | Detail |
|---|---|
| Freshdesk | Growth, Pro or Enterprise, with admin access |
| A calling backend | You run this. The app holds no Vobiz credentials - see The calling backend below |
| Vobiz account | Auth ID, Auth Token, and at least one number - console.vobiz.ai |
| Freshworks CLI | fdk, pinned to the Node version in manifest.json |
The app is a client. It holds no Vobiz credentials and cannot place a call on its own - it talks to a calling backend that you run, which holds the account credentials and drives the Vobiz REST API.
ℹ️ Note: The backend is not in the repository. Its complete contract is in
docs/backend-contract.md. Read it before you start - the obvious implementation of the recording endpoint is unsafe, and the document explains why.
Two reasons the backend exists, both non-negotiable:
X-Auth-Token in client-side code - see authentication. The backend holds the token and the browser never sees it.<audio> element cannot send authentication headers. Recordings are protected, so playback needs a server-side step.Everything below is relative to the backend_url installation parameter. The app requires HTTPS and strips trailing slashes.
| Route | Called by | Purpose |
|---|---|---|
GET /agent/{agentId} | Browser | The SIP identity the panel registers as. sipUser must be a full user@domain |
GET /session/{agentId} | Browser | Session restore when the panel loads |
POST /login | Browser | Validate credentials against Vobiz, store them server-side, return the account's numbers |
POST /select-number | Browser | Set the caller ID for this agent |
POST /start-call | Browser | Dial the customer; the agent is bridged in later |
GET /call-status/{callUuid} | Browser | Polled every 3 seconds while a call is up |
POST /setup-inbound | Browser | Create the Vobiz application and attach the agent's number |
GET /recordings/{agentId} | Browser | List recent recordings |
GET /recording-audio/{agentId}/{recordingId} | Browser | Playable audio, assigned straight to an <audio> element |
GET|POST /answer | Vobiz | Returns <Dial><User>sip:...</User></Dial> to bridge the agent in |
GET|POST /inbound-answer | Vobiz | The XML for an inbound call. A 20-second no-answer timeout to voicemail is a sensible default |
The app sends agentId as a plain string in the path, body or query. If your backend trusts that string, every browser route above is unauthenticated and the agentId is guessable. Somebody who learned your backend URL could then read SIP passwords, place calls billed to your account, rewrite your inbound number routing, and download every recording.
⚠️ Warning: A backend implementing this contract must:
- Authenticate every request. Do not treat
agentIdas proof of identity - issue a per-agent token at login and require it.- Not serve long-lived SIP passwords. Mint short-lived credentials, or gate
/agent/{agentId}behind the same authentication.- Sign recording URLs with a short expiry, and verify the signature on every request. An endpoint that streams any recording to anyone holding the URL is a breach waiting to happen - and
/recordingshands out the IDs.- Scope CORS to the Freshdesk origin. Never
Access-Control-Allow-Origin: *.- Store Auth Tokens encrypted at rest, and never log them.
- Serve over HTTPS with a stable hostname.
If you are adapting an internal prototype, assume it does none of these.
The Freshworks CLI is pinned to a Node version, with a separate CLI build per Node major. Install the pair the app declares in manifest.json:
bashnvm install 24.11.1 && nvm use 24.11.1 npm install https://cdn.freshdev.io/fdk/latest-v24.tgz -g fdk version # 10.1.9
bashgit clone https://github.com/vobiz-ai/Vobiz-Freshdesk-Calling.git cd Vobiz-Freshdesk-Calling fdk validate fdk pack
The installable zip lands in dist/.
ℹ️ Note:
fdk packenforces Freshworks' 80% coverage floor on every metric, and additionally wants local simulation coverage, which is only produced by running the app inside a real Freshdesk account via?dev=true. For local builds usefdk pack --skip-coverage; do not use that flag for a Marketplace submission.
dist/.| Setting | Required | Value |
|---|---|---|
| Calling backend URL | Yes | The HTTPS base URL of your backend, no trailing slash |
| Agent identity | Yes | The identity for this agent. Every agent needs a different one |
| SIP registrar URL | No | Leave blank unless Vobiz support gave you a different one |
All three can be changed later without reinstalling, under Admin → Apps → Manage Apps → Custom → Settings → Configure.
Open any agent page and look for the Vobiz icon at the bottom left. The panel loads on all agent pages, not only tickets - if the icon does not appear, hard refresh.
Then:
The repository ships a mock backend that implements the whole contract with fake data, so the panel can be developed without placing a real call.
bashnpm install npm run mock-backend # terminal 1 -- http://localhost:8092 fdk run # terminal 2 -- http://localhost:10001
Open a Freshdesk page with ?dev=true appended, then set the app's settings at http://localhost:10001/custom_configs:
| Setting | Value |
|---|---|
| Calling backend URL | http://localhost:8092 |
| Agent identity | anything, e.g. priya |
| SIP registrar URL | leave blank |
Any Auth ID and Auth Token are accepted. Use fail as the Auth ID to exercise the error path. The mock backend's README covers pointing it at a real Vobiz SIP endpoint to test actual audio.
bashnpm test # Vitest in jsdom, with coverage fdk validate # platform and lint rules
The tests load the real app/scripts/app.js and the real markup from app/index.html into jsdom with the Freshworks SDK, JsSIP and fetch faked, so they exercise the code that ships rather than a copy of it. The whole app is three files - app/index.html, app/scripts/app.js and app/styles/style.css. No build step, no framework.
Worth knowing before you touch the SIP code. For an incoming session, JsSIP has not built the RTCPeerConnection yet - session.connection is null until the call is answered. Dereferencing it inside the newRTCSession handler throws, and because the throw happens inside that handler it aborts before .answer() runs.
The symptom is misleading: the browser silently never picks up, Vobiz rings the endpoint until it times out, and the far end is never connected. Bind through the peerconnection event instead:
jssession.on("peerconnection", e => bindTrack(e.peerconnection)); bindTrack(session.connection); // no-op on the incoming path
| Symptom | Cause | Resolution |
|---|---|---|
| "Not configured" | Setup was skipped or incomplete | Re-enter the settings under Manage Apps → Custom → Settings → Configure |
| "Backend URL must start with https://" | The URL is missing its scheme | Enter the full https://... URL |
| "Cannot reach the calling backend" | The backend is down, or the URL is wrong | Confirm it is running and reachable over public HTTPS |
| Stuck on "Connecting..." | Same as above, or the agent identity does not exist | Check the identity matches one your backend knows |
| "Registration failed" | The SIP credentials from your backend were rejected | Check what /agent/{agentId} returns |
| Call button stays disabled | Not logged in, or SIP is not registered | The hint under the button says which |
| Connected but no audio | Microphone permission was blocked | Allow the microphone in the address bar, then reload |
| Calls ring the wrong person | Two agents share an identity | Give each agent their own |
| The panel icon never appears | Cached assets | Hard refresh (Ctrl + F5) |
ℹ️ Note: WebRTC inside a Freshdesk iframe. The app registers over SIP-over-WebSocket from inside a Freshworks-hosted iframe, and Freshworks does not publish the Content-Security-Policy applied to app iframes. If your network or the platform blocks the WebSocket, registration fails and the panel reports it. Test on one account before rolling out widely.
If the backend needs allowlisting on your side, see IP whitelisting.
The panel is a softphone that lives inside Freshdesk. Click-to-call, outbound, inbound and recording playback all work today. Writing back into Freshdesk's data - creating tickets from calls, logging calls as ticket notes, popping the contact record on an inbound call, attaching recordings - is on the roadmap in the repository's CHANGELOG. Hold, mute, transfer and conference are not built, and one agent identity is set per installation by an admin.
Two constraints to plan around if you extend it:
client.data.get("ticket") is not available in cti_global_sidebar. Knowing which ticket the agent is viewing needs a separate ticket_background instance, which shares no JavaScript state - so coordination goes through $db or the backend.client.request.invokeTemplate rather than fetch. Secure iparams are only substituted into request-template headers and are never readable from front-end JavaScript.<Dial> reference and <User>Connect 3CX PBX to Vobiz SIP trunking for inbound and outbound calling across 130+ countries - configure a generic SIP trunk in minutes.
Connect an Agora Conversational AI agent to Vobiz SIP trunking - add a Vobiz number to Agora for outbound campaigns and route inbound calls to your agent, including India and 130+ countries.
Bolna ships Vobiz as a built-in telephony provider. Paste two credentials into the Providers screen and your agents place and answer calls on numbers in 130+ countries, reaching 190, with audio streaming rather than a SIP trunk.
We use cookies to run this site and, with your permission, to understand how it's used. Read our Cookie Policy.