Freshdesk integration

    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.

    What you get

    Click-to-callClick any phone number in Freshdesk and the panel opens and dials
    Outbound callsDial from the panel with a number picked from your Vobiz account
    Inbound callsEnable once, and calls to the agent's number ring the panel
    Recording playbackRecent recordings listed and playable in the panel
    Browser audioCalls run over WebRTC in the tab - no desk phone, no desktop app

    How it works

    text
    Freshdesk --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:

    js
    client.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:

    CallPurpose
    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 outbound call flow

    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.

    Inbound calls

    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.

    Requirements

    RequirementDetail
    FreshdeskGrowth, Pro or Enterprise, with admin access
    A calling backendYou run this. The app holds no Vobiz credentials - see The calling backend below
    Vobiz accountAuth ID, Auth Token, and at least one number - console.vobiz.ai
    Freshworks CLIfdk, pinned to the Node version in manifest.json

    The calling backend

    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:

    1. Vobiz Auth Tokens are account-level API credentials. Never expose X-Auth-Token in client-side code - see authentication. The backend holds the token and the browser never sees it.
    2. An <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.

    RouteCalled byPurpose
    GET /agent/{agentId}BrowserThe SIP identity the panel registers as. sipUser must be a full user@domain
    GET /session/{agentId}BrowserSession restore when the panel loads
    POST /loginBrowserValidate credentials against Vobiz, store them server-side, return the account's numbers
    POST /select-numberBrowserSet the caller ID for this agent
    POST /start-callBrowserDial the customer; the agent is bridged in later
    GET /call-status/{callUuid}BrowserPolled every 3 seconds while a call is up
    POST /setup-inboundBrowserCreate the Vobiz application and attach the agent's number
    GET /recordings/{agentId}BrowserList recent recordings
    GET /recording-audio/{agentId}/{recordingId}BrowserPlayable audio, assigned straight to an <audio> element
    GET|POST /answerVobizReturns <Dial><User>sip:...</User></Dial> to bridge the agent in
    GET|POST /inbound-answerVobizThe XML for an inbound call. A 20-second no-answer timeout to voicemail is a sensible default

    Security requirements

    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 agentId as 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 /recordings hands 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.

    Step 1: Pack the app

    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:

    bash
    nvm 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
    bash
    git 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 pack enforces 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 use fdk pack --skip-coverage; do not use that flag for a Marketplace submission.

    Step 2: Create the custom app

    1. In Freshdesk, go to Admin → Apps.
    2. Click Go to Developer Portal.
    3. Click Create New App and choose Custom App.
    4. Select your Freshdesk account.
    5. Upload the zip from dist/.
    6. Fill in the app details - name, description, and your team's support email.
    7. Save and Publish, then Promote to Live.

    Step 3: Install and configure

    1. Back in Freshdesk, go to Admin → Apps → Manage Apps.
    2. Filter to Custom.
    3. Find the app and click Install.
    4. Fill in the settings:
    SettingRequiredValue
    Calling backend URLYesThe HTTPS base URL of your backend, no trailing slash
    Agent identityYesThe identity for this agent. Every agent needs a different one
    SIP registrar URLNoLeave 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.

    Step 4: Open the panel

    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:

    1. Log in with your Vobiz Auth ID and Auth Token, and pick the number to call from. These are entered by the agent at runtime and are not stored by the app.
    2. Make a call by typing a number and clicking Call, or by clicking any phone number in Freshdesk.
    3. The first call prompts for microphone permission, since audio runs through the browser.
    4. Receive calls by clicking Enable inbound calls to this panel once.

    Develop without a Vobiz account

    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.

    bash
    npm 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:

    SettingValue
    Calling backend URLhttp://localhost:8092
    Agent identityanything, e.g. priya
    SIP registrar URLleave 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.

    Tests

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

    Binding inbound audio

    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:

    js
    session.on("peerconnection", e => bindTrack(e.peerconnection)); bindTrack(session.connection); // no-op on the incoming path

    Troubleshooting

    SymptomCauseResolution
    "Not configured"Setup was skipped or incompleteRe-enter the settings under Manage Apps → Custom → Settings → Configure
    "Backend URL must start with https://"The URL is missing its schemeEnter the full https://... URL
    "Cannot reach the calling backend"The backend is down, or the URL is wrongConfirm it is running and reachable over public HTTPS
    Stuck on "Connecting..."Same as above, or the agent identity does not existCheck the identity matches one your backend knows
    "Registration failed"The SIP credentials from your backend were rejectedCheck what /agent/{agentId} returns
    Call button stays disabledNot logged in, or SIP is not registeredThe hint under the button says which
    Connected but no audioMicrophone permission was blockedAllow the microphone in the address bar, then reload
    Calls ring the wrong personTwo agents share an identityGive each agent their own
    The panel icon never appearsCached assetsHard 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.

    Scope today

    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:

    • The CTI sidebar has no page context. 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.
    • The Freshdesk API key should be a secure iparam, which means calls using it must go through client.request.invokeTemplate rather than fetch. Secure iparams are only substituted into request-template headers and are never readable from front-end JavaScript.

    Next steps