Skip to main content
Zendesk 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.
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.

What you get

How it works

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.

Three app locations, three jobs

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.
All three locations are required. ZAF exposes ticket.id only to an instance that is in ticket context, and the top bar is global. Without the ticket_sidebar instance 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 outbound call flow

Five-step outbound call flow: 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.Five-step outbound call flow: 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.
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.

Diagnosing from the CDR

hangup_cause_name in the CDR tells you where a call stopped:

Inbound calls

A call to one of your Vobiz numbers hits the same /call-answer webhook, which branches on the inboundNumber parameter:
  • Not dialable, or one of your own account DIDs → bridge to the agent’s WebRTC endpoint with <User>.
  • A dialable external number → bridge to that phone with <Number>.
Deciding “is this one of our own DIDs” needs the account’s number list, which the backend caches in memory the first time /numbers is 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.

Requirements

Every test places a real call and bills your balance. An outbound call bills two legs — the customer’s and the agent’s.

Step 1: Create a SIP endpoint per agent

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:
Two things to catch here:
  1. 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.
  2. The password is never returned again. Record it now.
Then map the endpoint in backend/agents.json:
agents.json
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.

Step 2: Run the backend

The one value worth setting deliberately is RECORDING_TOKEN_SECRET:
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:

Step 3: Expose the backend

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.
Confirm it reaches you and returns XML:
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.

Step 4: Run the Zendesk app

zcli.apps.config.json
The filename matters. zcli reads zcli.apps.config.json. A file named zcli.json is silently ignored, and you are prompted for the values instead.
Then open Zendesk with the apps server attached:
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.

Step 5: Place a test call

  1. Open the softphone from the top bar.
  2. Enter your Vobiz Auth ID and Auth Token and sign in.
  3. Pick the number to call from.
  4. Wait for the status line to read Registered — the Call button stays disabled until the SIP endpoint is up, precisely so you cannot dial into a dead bridge.
  5. Type a number and press Call.
Confirm it landed in Vobiz with GET /Call/, or read the CDR.

Installing permanently

zcli apps:server serves the app to your browser only, for as long as it runs. To install it for the whole account:
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.
Packaging needs two files the repository does not ship: assets/logo-small.png, which Zendesk requires, and a top-bar icon at assets/icon_top_bar.svg. Add your own branding before packaging.

App settings

Set in Zendesk → Admin Center → Apps → Vobiz Calling App → Settings:

Backend routes

Full request and response shapes are in the repository’s backend contract. In summary: 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.
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.

Replacing the backend with your own

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.

How a call reaches a ticket

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

Where credentials live

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.

Troubleshooting

Scope today

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.

Next steps