Put a Vobiz softphone in the Zendesk top bar - click-to-call, screen pop, call logging to tickets, and recording playback for agents in 130+ countries.
A softphone in the Zendesk top bar. Agents place and take real phone calls in the browser, see who is calling before they answer, and every call is written to the ticket they are working on.
Source code: vobiz-ai/Vobiz-Zendesk-Calling - the ZAF v2 app and the Node backend used throughout this guide. Both ship in the one repository.
ℹ️ Note: Scope: inbound and outbound, with browser audio over WebRTC. Call logs are written through the standard Tickets API, so this works on any Zendesk Support plan rather than requiring Talk Partner Edition.
| Click-to-call | Click a phone number anywhere in Zendesk and the softphone opens and dials |
| Screen pop | An inbound call is looked up by number, so the agent sees who it is before answering |
| Call logging | Direction, duration and notes written to the ticket the agent is viewing |
| Recordings | Playable from the ticket, behind an expiring link that carries no credentials |
| Transcripts | An optional webhook appends an AI transcript as a private comment |
| Browser audio | Calls run over WebRTC in the tab - no desk phone, no desktop app |
The browser carries the audio; the backend carries the credentials. Your Vobiz Auth Token never reaches a third party and is never written into a ticket.
textZendesk agent browser Vobiz top_bar the softphone --HTTPS--> backend --REST--> api.vobiz.ai ticket_sidebar resolves the ticket ^ | background relays click-to-dial +---- webhooks ------+ | +------ SIP over WebSocket ------> wss://registrar.vobiz.ai:5063
A ZAF app can run in several places at once, and each instance is a separate iframe with its own JavaScript context. They share no variables and talk only through ZAF messaging.
| Location | File | Job |
|---|---|---|
top_bar | assets/index.html | The softphone: SIP registration, dialpad, call UI, logging |
background | assets/background.html | Invisible. Listens for Zendesk's voice.dialout and relays it |
ticket_sidebar | assets/sidebar.html | Reports which ticket the agent is on |
⚠️ Warning: All three locations are required. ZAF exposes
ticket.idonly to an instance that is in ticket context, and the top bar is global. Without theticket_sidebarinstance the app can never resolve a ticket, so every call log creates a brand-new ticket instead of commenting on the one the agent is looking at.
Click-to-dial arrives as Zendesk's voice.dialout event, which only reaches the background instance. That instance finds the top_bar instance and forwards the number to it over an app-internal event. voice.dialout fires only on accounts with Talk Partner Edition enabled - on other plans the manual dialpad still works.
The agent clicks Call, the app POSTs /start-call, the backend has Vobiz dial the customer first, the customer answering triggers the /call-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.
hangup_cause_name in the CDR tells you where a call stopped:
| Value | Meaning |
|---|---|
Normal Hangup | The call completed |
End Of XML Instructions | Vobiz fetched and ran the webhook, but the <Dial> had nothing to reach - usually an unregistered SIP endpoint |
No Answer / Busy | The customer leg never connected, so the webhook was never fetched |
A call to one of your Vobiz numbers hits the same /call-answer webhook, which branches on the inboundNumber parameter:
<User>.<Number>.ℹ️ Note: Deciding "is this one of our own DIDs" needs the account's number list, which the backend caches in memory the first time
/numbersis called. Straight after a restart the cache is empty, so the backend treats an unwarmed cache as "route to the agent" - otherwise an account DID would be misread as external, and the platform would dial its own inbound number in a loop.
The <Number> branch is a complete path for routing calls to an agent's mobile or desk phone instead of the browser. The backend supports it today; adding it to the UI is a front-end change plus threading inboundNumber through /start-call.
| Requirement | Detail |
|---|---|
| Zendesk Support | Admin access, to install the app |
| Vobiz account | Auth ID, Auth Token, and at least one number - console.vobiz.ai |
| Node | 18 or newer, to run the backend |
| A tunnel | cloudflared or ngrok. Vobiz must reach your answer webhook from the public internet |
⚠️ Warning: Every test places a real call and bills your balance. An outbound call bills two legs - the customer's and the agent's.
The softphone registers to Vobiz as a SIP endpoint. Each agent needs their own, or calls ring the wrong person. Create one in the console under Voice → Endpoints, or via the API:
bashcurl -X POST "https://api.vobiz.ai/api/v1/Account/$VOBIZ_AUTH_ID/Endpoint/" \ -H "X-Auth-ID: $VOBIZ_AUTH_ID" \ -H "X-Auth-Token: $VOBIZ_AUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{"username":"agent-priya","password":"<a strong password>","alias":"Priya"}'
json{ "alias": "Priya", "endpoint_id": "375555448816999", "username": "agent-priya1187694299145202883643" }
⚠️ Warning: Two things to catch here:
- Vobiz appends a numeric suffix to the username you asked for. Use the value it returns, not the one you sent - registration is rejected otherwise.
- The password is never returned again. Record it now.
Then map the endpoint in backend/agents.json:
json{ "priya": { "name": "Priya Sharma", "sipUser": "agent-priya1187694299145202883643", "sipPassword": "<the password you chose>", "displayName": "Priya Sharma" } }
The key - priya - is what goes in the app's agent_id setting. agents.json holds SIP passwords and is gitignored; keep it that way.
bashcd backend npm install cp .env.example .env cp agents.json.example agents.json npm start # -> http://localhost:8092
The one value worth setting deliberately is RECORDING_TOKEN_SECRET:
bashopenssl rand -hex 32
Without it a random key is generated at each boot, and recording links written into tickets before the last restart stop resolving. Everything else in .env can stay blank - agents sign in through the app and their credentials are sent per request, so the server holds none at rest.
Check it is serving the SIP identity:
bashcurl http://localhost:8092/agent/priya # {"name":"Priya Sharma","sipUser":"agent-priya118...","sipPassword":"..."}
When the customer answers, Vobiz fetches /call-answer to find out what to do next, so that URL has to be reachable from the public internet.
bashcloudflared tunnel --url http://localhost:8092
Confirm it reaches you and returns XML:
bashcurl "https://<your-tunnel>/call-answer?agentId=priya"
xml<?xml version="1.0" encoding="UTF-8"?> <Response> <Dial record="true" redirect="false"> <User>agent-priya1187694299145202883643</User> </Dial> </Response>
ℹ️ Note: Quick tunnels get a new hostname on every restart, and the old one is recycled to somebody else. Fine for development; use a named tunnel or a real host for anything permanent.
If your backend sits behind an IP allowlist, see IP whitelisting.
bashcd ../zendesk-app cp zcli.apps.config.json.example zcli.apps.config.json
json{ "parameters": { "backend_url": "https://your-tunnel.example.com", "agent_id": "priya" } }
⚠️ Warning: The filename matters.
zclireadszcli.apps.config.json. A file namedzcli.jsonis silently ignored, and you are prompted for the values instead.
bashnpx @zendesk/zcli apps:server
Then open Zendesk with the apps server attached:
texthttps://<your-subdomain>.zendesk.com/agent/dashboard?zcli_apps=true
The softphone appears in the top bar. If nothing loads, your browser may be blocking requests to localhost:4567 - Chrome asks for Local Network Access permission, so allow it.
| Stage | What you see |
|---|---|
| Dialling | The customer's phone rings |
| They answer | They hear a moment of ringback while you are bridged in |
| Bridged | Your browser rings, audio connects both ways |
| Hang up | End Call, then Log Call writes it to the open ticket |
Confirm it landed in Vobiz with GET /Call/, or read the CDR.
zcli apps:server serves the app to your browser only, for as long as it runs. To install it for the whole account:
bashcd zendesk-app npx @zendesk/zcli apps:package
Upload the resulting zip under Admin Center → Apps and integrations → Zendesk Support apps → Upload private app, then fill in the settings below. You also need a stable backend_url - a quick tunnel is not good enough.
ℹ️ Note: Packaging needs two files the repository does not ship:
assets/logo-small.png, which Zendesk requires, and a top-bar icon atassets/icon_top_bar.svg. Add your own branding before packaging.
Set in Zendesk → Admin Center → Apps → Vobiz Calling App → Settings:
| Setting | Required | Purpose |
|---|---|---|
backend_url | Yes | Public HTTPS base URL of your backend |
agent_id | Yes | Which entry in agents.json this agent is. One per agent |
zendesk_subdomain | No | Only if the backend writes call logs instead of the app |
zendesk_email | No | Paired with the API token |
zendesk_api_token | No | Stored as a secure setting |
Full request and response shapes are in the repository's backend contract. In summary:
| Route | Called by | Purpose |
|---|---|---|
GET /agent/:agentId | Browser | The SIP identity the softphone registers with |
GET /numbers | Browser | Account numbers, cached in memory for the /call-answer branch |
POST /start-call | Browser | Dials the customer; the agent is bridged in later |
POST /hangup-call | Browser | Terminates a live call over REST |
POST /sync-call | Browser | Writes the call log into Zendesk and mints the recording link |
GET /play-recording | Browser | Streams a recording from a sealed token |
POST /transcription-ready | Webhook | Appends an AI transcript as a private comment |
GET|POST /call-answer | Vobiz | Returns the <Dial> XML that bridges the agent in |
Credentials may be supplied three ways, in order of precedence: the request body or query string, the X-Auth-ID / X-Auth-Token headers, then the backend environment. The app uses the first; the third exists for unattended testing.
⚠️ Warning: The backend's routes are unauthenticated. Do not expose it to the public internet beyond the webhook paths Vobiz needs. Read the repository's SECURITY.md before deploying.
The minimum for outbound calling with browser audio is three routes: GET /agent/:agentId to return a SIP identity, POST /start-call to dial the customer with answer_url pointing at your own webhook, and GET /call-answer to return <Dial><User>...</User></Dial>. The rest add number selection, REST hangup, ticket logging and recording playback; the app degrades without them rather than breaking.
There is no Zendesk "call" object available to a normal app, so calls are recorded as ordinary tickets and comments. The app resolves the ticket the agent clicked from, else whatever ticket_sidebar reports, then posts to /sync-call, which either comments on that ticket or creates a new one.
If the backend write fails, the app falls back to writing through the agent's own Zendesk session. That fallback is the better path in one respect worth knowing: it attributes the comment to the agent, whereas the backend path attributes everything to a single shared API user.
Screen pop is a ZAF search from the browser - a type:user phone:... query, then routeTo the matched user.
A recording link written into a ticket is readable by every agent, every export and every audit log, forever. It therefore cannot carry credentials.
text/sync-call seals {authId, authToken, callUuid, exp} with AES-256-GCM | the ticket gets: {backend}/play-recording?token=... | /play-recording opens the token, fetches the audio from Vobiz with the credentials inside it, and streams it back
Tokens expire after RECORDING_TOKEN_TTL_MS (30 days by default). A missing recording returns 404 rather than a 200 with an HTML body, so an <audio> element can tell the difference. See call recording for what is retained on the platform side.
| Credential | Where it lives | Notes |
|---|---|---|
| Vobiz Auth ID / Token | The agent's browser session | Sent per request. Never stored server-side, never in a ticket |
| SIP password | backend/agents.json | Server-side only, gitignored |
| Recording token key | RECORDING_TOKEN_SECRET | Encrypts credentials into recording links |
| Zendesk API token | Zendesk app setting, marked secure | Optional. Only if the backend writes logs itself |
The deliberate trade-off: agents type their own Vobiz credentials into the panel. That keeps one shared account token out of the app, but the credentials do pass through the backend on each request - so the backend must be trusted and reachable only over HTTPS.
| Symptom | Cause | Resolution |
|---|---|---|
| "Registration failed" | SIP credentials wrong or missing | curl http://localhost:8092/agent/<agent_id>. If sipPassword is absent the agents.json entry is incomplete; if the username omits the numeric suffix Vobiz returned, registration is rejected |
| Customer answers, hears silence, call drops | The SIP endpoint was not registered when the call bridged | Keep the softphone open and registered before dialling. The CDR shows End Of XML Instructions |
| Call never connects, nothing in the backend log | Vobiz could not reach the webhook | curl "https://<tunnel>/call-answer?agentId=priya" from outside your network. If the tunnel restarted, its hostname changed - update backend_url |
| Call logs create a new ticket instead of using the open one | The ticket_sidebar location is missing, or the agent is not on a ticket page | Confirm all three locations installed |
| "This recording link is invalid or has expired" | Link older than the TTL, or the backend restarted without RECORDING_TOKEN_SECRET | Set the secret in .env - regenerating the key invalidates every link ever issued |
| Calls ring the wrong agent | Two agents share an agent_id | Each agent needs their own key in agents.json and their own SIP endpoint |
A typo in agent_id silently routes to the wrong endpoint | Unknown IDs fall back to a synthesised agent rather than returning 404 | Convenient in development, worth tightening in production |
The app is a working softphone plus ticket logging. Not yet built: hold, mute, transfer and conference; routing a call to an agent's mobile instead of the browser (the backend's <Number> branch is ready, the UI is not); and Talk Partner Edition reporting, since logs go through the standard Tickets API. It installs as a private app rather than from the Zendesk Marketplace.
<Dial> reference, <User> and <Number>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.