> ## Documentation Index
> Fetch the complete documentation index at: https://www.vobiz.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Zendesk integration

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

<img className="block w-14 h-14 rounded-xl mb-4" src="https://mintcdn.com/vobizai/_O6EfSQk_Nz6SEBB/images/zendesk/logo.svg?fit=max&auto=format&n=_O6EfSQk_Nz6SEBB&q=85&s=b6e82d0cb9f17dd1bd8b935b8810554e" alt="Zendesk" width="512" height="512" data-path="images/zendesk/logo.svg" />

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](https://github.com/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.
</Note>

## What you get

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

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

```text theme={null}
Zendesk 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
```

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

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

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

<div className="my-6">
  <img className="block dark:hidden w-full max-w-2xl mx-auto" src="https://mintcdn.com/vobizai/_O6EfSQk_Nz6SEBB/images/zendesk/outbound-flow-light.svg?fit=max&auto=format&n=_O6EfSQk_Nz6SEBB&q=85&s=a7fcb5398840d0d515ae92086bf59e92" alt="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." width="720" height="550" data-path="images/zendesk/outbound-flow-light.svg" />

  <img className="hidden dark:block w-full max-w-2xl mx-auto" src="https://mintcdn.com/vobizai/_O6EfSQk_Nz6SEBB/images/zendesk/outbound-flow-dark.svg?fit=max&auto=format&n=_O6EfSQk_Nz6SEBB&q=85&s=0484f010d72d5235490535be3d8dc9cc" alt="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." width="720" height="550" data-path="images/zendesk/outbound-flow-dark.svg" />
</div>

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

### Diagnosing from the CDR

`hangup_cause_name` in the [CDR](/docs/cdr/get-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>`](/docs/xml/dial) had nothing to reach — usually an unregistered SIP endpoint |
| `No Answer` / `Busy`      | The customer leg never connected, so the webhook was never fetched                                                           |

## 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>`](/docs/xml/dial/user).
* A dialable external number → bridge to that phone with [`<Number>`](/docs/xml/dial/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 `/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.
</Note>

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

| Requirement     | Detail                                                                                      |
| --------------- | ------------------------------------------------------------------------------------------- |
| Zendesk Support | Admin access, to install the app                                                            |
| Vobiz account   | Auth ID, Auth Token, and at least one number — [console.vobiz.ai](https://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.
</Warning>

## 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](/docs/endpoint/create-endpoint):

```bash theme={null}
curl -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 theme={null}
{
  "alias": "Priya",
  "endpoint_id": "375555448816999",
  "username": "agent-priya1187694299145202883643"
}
```

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

Then map the endpoint in `backend/agents.json`:

```json agents.json theme={null}
{
  "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.

## Step 2: Run the backend

```bash theme={null}
cd 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`:

```bash theme={null}
openssl 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:

```bash theme={null}
curl http://localhost:8092/agent/priya
# {"name":"Priya Sharma","sipUser":"agent-priya118…","sipPassword":"…"}
```

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

```bash theme={null}
cloudflared tunnel --url http://localhost:8092
```

Confirm it reaches you and returns XML:

```bash theme={null}
curl "https://<your-tunnel>/call-answer?agentId=priya"
```

```xml theme={null}
<?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.
</Note>

If your backend sits behind an IP allowlist, see [IP whitelisting](/docs/concepts/ip-whitelisting).

## Step 4: Run the Zendesk app

```bash theme={null}
cd ../zendesk-app
cp zcli.apps.config.json.example zcli.apps.config.json
```

```json zcli.apps.config.json theme={null}
{
  "parameters": {
    "backend_url": "https://your-tunnel.example.com",
    "agent_id": "priya"
  }
}
```

<Warning>
  **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.
</Warning>

```bash theme={null}
npx @zendesk/zcli apps:server
```

Then open Zendesk with the apps server attached:

```text theme={null}
https://<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.

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

| 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/`](/docs/call/make-call), or read the [CDR](/docs/cdr/get-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:

```bash theme={null}
cd 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 at `assets/icon_top_bar.svg`. Add your own branding before packaging.
</Note>

## App settings

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                                |

## Backend routes

Full request and response shapes are in the repository's [backend contract](https://github.com/vobiz-ai/Vobiz-Zendesk-Calling/blob/main/docs/backend-contract.md). 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](https://github.com/vobiz-ai/Vobiz-Zendesk-Calling/blob/main/SECURITY.md) before deploying.
</Warning>

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

## Recording links

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 theme={null}
/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](/docs/recording) for what is retained on the platform side.

## Where credentials live

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

## Troubleshooting

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

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

* Clone the app and backend: [vobiz-ai/Vobiz-Zendesk-Calling](https://github.com/vobiz-ai/Vobiz-Zendesk-Calling)
* Read the [`<Dial>` reference](/docs/xml/dial), [`<User>`](/docs/xml/dial/user) and [`<Number>`](/docs/xml/dial/number)
* Create the agents' SIP endpoints: [Endpoints API](/docs/endpoint) or [console guide](/docs/platform/voice/endpoints)
* Same pattern in another helpdesk? See [Freshdesk](/docs/integrations/freshdesk)
