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

# Voice SDK integration

> Add voice support to your web, iOS, or Android app. Authenticate customers, connect AI or human teams, and manage call controls and session status.

<Warning>
  The Voice SDK is an opt-in preview. Enable app calling in your organization’s Voice & Telephony settings before integrating. Obtain the preview packages from your OpenCX contact; public package registry availability is not implied by this guide.
</Warning>

Add a **Call support** button to your app. Customers can speak with an AI phone agent, wait for a human team, or remain on the same call while the AI hands off to a human. Your app controls the interface; the SDK handles audio and call controls.

The SDK supports customer-initiated calls while your support screen is active. Incoming-call push notifications, system call-screen integration, and automatic background-call support are not included. Configure and test any app background audio behavior separately.

## Package availability

Use the package version confirmed by your OpenCX contact. Until that version is published, continue using the supplied preview packages described below.

After public release, the installation locations are:

| Platform | Package location |
| - | - |
| Web | `npm install @opencx/voice@0.1.0` |
| iOS | Add `https://github.com/opencx-labs/voice-ios` in Xcode and select version `0.1.0`. |
| Android | Add `implementation("cx.open:voice-android:0.1.0")` from Maven Central. |

For Android, include `google()`, `mavenCentral()`, and `maven("https://jitpack.io")` in your dependency repositories so the complete dependency graph can resolve. All three SDKs use the MIT license.

Installing a package does not enable calling for your organization. The organization’s app calling setting and your authenticated server integration are still required.

## Enable app calling

1. Open **Settings → Voice & Telephony** in the organization your app will use.
2. Under **App calling**, turn on **Allow calls from apps**. You need permission to edit Voice & Telephony settings (`settings-channels-voice:write`).
3. Configure your server with an API key that has `phone:write`, then follow the authentication steps below.

New organizations have app calling turned off. Changes take effect for new calls without a deployment and are recorded in the organization’s audit log. This setting is independent of telephone-number calling.

Turning it off blocks new call creation and prevents previously issued credentials from starting a call. Active calls can finish, reconnect, and hang up within their existing duration limit. Turning it back on allows new calls; create fresh credentials for attempts that already ended.

The toggle does not replace API-key scopes, customer authentication, contact blocking, or destination checks.

## Authenticate on your server

Your server must authenticate the customer and resolve their OpenCX contact. It then chooses an authorized phone agent or support team and calls [Create an app voice call](/api-reference/phone/create_web_call) with an API key holding `phone:write`.

Never put a permanent API key in a website or mobile app. Do not accept an arbitrary contact ID or privileged routing destination from the client. Membership in your app must be checked before returning call credentials.

```typescript theme={"dark"}
// Inside your authenticated server route:
// contactId is resolved from the signed-in customer, not the request body.
const response = await fetch('https://api.open.cx/phone/web-calls', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.OPENCX_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    requestId, // UUID; keep the same value when retrying this request
    contactId,
    destination: { type: 'agent', id: phoneAgentId },
    maxDurationSeconds: 1800,
  }),
});
if (!response.ok) {
  // Return an appropriate error to your app; never log returned credentials.
  throw new Error(`Call creation failed (${response.status})`);
}
const credentials = await response.json();
// Respond only to the authenticated customer, with Cache-Control: no-store.
```

Use `{ type: 'team', id: teamId }` to start with a human team. The team must belong to your organization and have support enabled. App calls use that team's routing and queue settings. Direct team calls and AI transfers to a specific team do not spill into unrelated teams when nobody is available. Team eligibility is checked again when a human accepts, so a ringing notification does not preserve access after they leave the team.

For AI calls, configure the agent's languages and permitted human-team transfer destinations in the dashboard. An AI team-transfer destination with no selected team rings eligible, available agents across your organization. App-call agents support team and end-call destinations. Agents with telephone-number or external telephony transfer destinations are rejected; use a dedicated app-support agent if your phone agent needs those destinations. No dialed phone number is available to select a country flow: choose the appropriate destination on your server.

Call creation returns `callId`, `sessionId`, `controlToken`, and `expiresAt`. Pass this OpenCX credentials object to the SDK unchanged. Treat `controlToken` as a secret; do not log it, place it in page URLs, or store it persistently. It authorizes only this call. The SDK establishes the audio connection and activates the call automatically. An abandoned connection attempt expires automatically, and active calls have a server-enforced duration limit.

Your app uses only the OpenCX SDK's call methods, states, and errors. No separate audio-service account, credentials, client initialization, or event handling is required. Audio connection details are managed internally by the SDK and are not part of the supported integration contract.

<Note>
  Update preview SDK packages and the matching backend together. Earlier preview responses included `serverUrl` and `participantToken`; these fields are no longer returned by call creation. Remove app code that reads those fields or creates its own audio connection, and pass the OpenCX credentials directly to the updated SDK. Start a new call after updating.
</Note>

Reuse `requestId` only for retries of the same creation attempt. Different settings with the same ID fail. After a call starts or ends, use a new request ID for another call. One customer can have one active app call at a time.

## Web

Choose an npm integration or a script tag for a plain HTML or server-rendered app. Both provide the same calling methods and events; your app supplies the buttons and interface.

### npm integration

Use Node.js 22.22 or later for the npm tooling. Install the preview package supplied by OpenCX:

```bash theme={"dark"}
npm install ./opencx-voice-0.1.0.tgz
```

```typescript theme={"dark"}
import { OpenCXVoice } from '@opencx/voice';

const voice = new OpenCXVoice({
  getToken: async ({ requestId, signal }) => {
    const response = await fetch('/api/support/voice-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ requestId }),
      signal,
    });
    if (!response.ok) throw new Error('Unable to start support call');
    return response.json();
  },
});

voice.on('stateChanged', state => updateCallStatus(state));
voice.on('error', error => showCallError(error.message));

// Run from the customer's button click.
await voice.start();
await voice.setMuted(true);
voice.setSpeakerEnabled(false);
await voice.end();
// On permanent component removal:
await voice.dispose();
```

`setMicrophone(deviceId)` and `setAudioOutput(deviceId)` select audio devices where the browser supports them. Choose the microphone before calling `start()`, or wait for `start()` to finish before changing it. If audio playback is blocked, show a button that calls `resumeAudio()`. Microphone access requires HTTPS (localhost is allowed for development). Embedded frames require microphone permission from the host page. Customers can refuse or revoke microphone access; display the SDK error and offer another support channel.

`setMuted()` requires an active call on web, iOS, and Android. Disable mute controls before a call starts and after it ends, and handle failures instead of updating the UI as though mute succeeded.

SDK errors use OpenCX types: `VoiceError` on web and iOS, and `VoiceException` on Android. Handle these alongside call states; do not depend on internal audio error messages or types. On web, `call_failed` reports an operation that could not complete without exposing transport-specific error details.

Subscribe before calling `start()`. The states are `idle`, `connecting`, `waiting`, `queued`, `connected`, `reconnecting`, and `ended`. A queue notification or ringing agent is not proof a human answered. `connected` reflects an observed ready support participant. An `ended` state means the local microphone is stopped; if server hangup fails, an error is also emitted and server cleanup remains responsible for the room.

### Script tag: no npm or build step

<Note>
  The standalone browser bundle starts with web SDK version `0.2.0`. Confirm this version is available before using its download URL; version `0.1.0` does not contain the bundle. Native SDK versions do not need to change for this integration.
</Note>

Download `dist/opencx-voice.min.js` and `dist/opencx-voice.min.js.LEGAL.txt` from the confirmed release of `@opencx/voice`. Copy both files to your website's `/scripts/` directory. No Node.js runtime, npm installation, or JavaScript build step is required in your application. Your existing server can use any language.

Add these tags and controls to your HTML template. The SDK must load before your own script:

```html theme={"dark"}
<script defer src="/scripts/opencx-voice.min.js"></script>
<script defer src="/scripts/support-calling.js"></script>

<button id="call-support" type="button">Call support</button>
<button id="end-call" type="button" disabled>End call</button>
<button id="resume-audio" type="button">Resume audio</button>
<p id="call-status" role="status">idle</p>
<p id="call-error" role="alert"></p>
```

Create `/scripts/support-calling.js`:

```javascript theme={"dark"}
const status = document.getElementById('call-status');
const errorMessage = document.getElementById('call-error');
const callButton = document.getElementById('call-support');
const endButton = document.getElementById('end-call');
const voice = new OpenCXVoice.OpenCXVoice({
  getToken: async ({ requestId, signal }) => {
    const response = await fetch('/api/support/voice-token', {
      method: 'POST',
      credentials: 'same-origin',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ requestId }),
      cache: 'no-store',
      signal,
    });
    if (!response.ok) throw new Error('Unable to start a support call');
    return response.json();
  },
});
voice.on('stateChanged', (state) => {
  status.textContent = state;
  const idle = state === 'idle' || state === 'ended';
  callButton.disabled = !idle;
  endButton.disabled = idle;
});
voice.on('error', (error) => { errorMessage.textContent = error.message; });
async function perform(action) {
  errorMessage.textContent = '';
  try {
    await action();
  } catch (error) {
    errorMessage.textContent = error instanceof Error ? error.message : 'Call action failed';
  }
}
callButton.onclick = () => perform(() => voice.start());
endButton.onclick = () => perform(() => voice.end());
document.getElementById('resume-audio').onclick = () => perform(() => voice.resumeAudio());
```

Implement `/api/support/voice-token` on **your server**, following [Authenticate on your server](#authenticate-on-your-server). Require a signed-in customer, apply your application's CSRF protection, and resolve the contact and permitted destination there. Forward `requestId` to call creation and return the credentials only to that customer with `Cache-Control: no-store`. The permanent OpenCX API key must never appear in the template or browser script.

The script exposes `OpenCXVoice.OpenCXVoice` and `OpenCXVoice.VoiceError`. The same `setMuted`, `setSpeakerEnabled`, device-selection, and cleanup methods shown above apply. When your app removes the calling screen, await `voice.dispose()` to release the microphone and end the call.

After version `0.2.0` is published, an alternative to self-hosting is the exact URL `https://cdn.jsdelivr.net/npm/@opencx/voice@0.2.0/dist/opencx-voice.min.js`. Keep the version pinned. Copy `integrity` from that release's `dist/browser-integrity.json` into the script tag's `integrity` attribute and set `crossorigin="anonymous"`. Do not reuse a hash from a different version or modified file. Keep the legal notices with any redistributed copy.

Use HTTPS and a modern browser with microphone and real-time audio support; localhost is supported for development. This installation method does not add support for obsolete browsers. Start calls from a user action, and offer Resume audio if playback is blocked. Embedded frames need microphone permission from the parent page.

If your app sets a Content Security Policy, permit the chosen script source, your credential route, the OpenCX API, and the audio connection endpoints supplied for your integration. The bundle does not require `unsafe-eval` or inline scripts. Keep the rest of your policy restricted.

## iOS

Add the supplied `OpenCXVoice` Swift package to your app. Include `NSMicrophoneUsageDescription` in your app's privacy settings. The SDK requests audio permission before creating a call.

```swift theme={"dark"}
import OpenCXVoice

let voice = try OpenCXVoice { requestId in
    // Authenticate with your backend and decode its response:
    try await supportAPI.voiceCredentials(requestId: requestId.uuidString)
}
voice.onStateChanged = { state in updateCallStatus(state.rawValue) }
voice.onError = { error in showCallError(error.localizedDescription) }
try await voice.start()
try await voice.setMuted(true)
try await voice.setSpeakerEnabled(true)
try await voice.end()
```

Keep the instance alive until the call ends. UI callbacks run on the main actor. End the call before discarding its owner. The speaker setting selects speakerphone or the system audio route. Test interruptions, Bluetooth, route changes, and app backgrounding on physical devices before rollout.

## Android

Add the supplied `cx.open:voice-android:0.1.0` package from your configured preview repository. Request `RECORD_AUDIO` permission before starting a call. Use the SDK from the main thread and collect `state` in your app's lifecycle.

```kotlin theme={"dark"}
import cx.open.voice.OpenCXVoice

val voice = OpenCXVoice(context, getToken = { requestId ->
    // Authenticate with your backend and decode with VoiceCredentials.fromJson.
    supportApi.voiceCredentials(requestId)
})
voice.onError = { error -> showCallError(error.message) }
voice.start()
voice.setMuted(true)
voice.end()
// When the owning support screen is permanently removed:
voice.dispose()
```

Do not cancel the only call owner without ending the call. Audio routing follows the system route. Background calling and incoming call alerts require additional app integration and are outside this preview.

## Session history and recording

`sessionId` identifies the OpenCX support session. App calls use the existing voice session history and configured recording behavior. Choosing app calling does not grant permission to read other sessions. Make your recording notice and retention policy available before the customer starts speaking.

## Troubleshooting

| Symptom | Check |
| - | - |
| Creation returns 403 | **Allow calls from apps** in Settings → Voice & Telephony, API key scope, and contact blocking. |
| Creation returns 404 | Contact and destination belong to the same organization; the team has support enabled. |
| Creation returns 409 | An existing active call or reuse of a request ID with different settings. |
| Call stays waiting | Network access, destination readiness, or unavailable support agents. The SDK ends a continuous wait of more than 60 seconds; time already connected or queued does not count toward that wait. |
| Microphone denied | Device/browser permission and the host page's frame permissions. |
| Connected but silent | Device route, muted speaker, or browser playback permission; offer `resumeAudio()` on web. |
| Call ends during a network change | A brief reconnect is supported, but a prolonged disconnect ends the call. Offer a fresh call attempt. |

Test your supported browser and device versions, human acceptance, unavailable teams, queues, hangup during connection, and Wi-Fi/cellular changes before enabling this for customers.

## Testing your integration

Request the SDK source examples from your OpenCX contact before following these checks. The installed packages do not include the test apps or broker. The source examples include setup instructions in `sdks/voice-web/examples/README.md`, `sdks/voice-android/example/README.md`, and `sdks/voice-ios/example/README.md`.

Use a dedicated test contact and support destination. The supplied web and native examples call an authenticated local test broker; the permanent organization API key remains on that server. A random test session token is required to use the broker. This fixed-contact broker is for local testing: your deployed app must authenticate each customer and authorize their contact and destination on your own backend.

For local development, the SDKs accept an HTTP API URL only on `localhost` or `127.0.0.1`. Use HTTPS for other hosts. The iOS sample uses local-network transport settings, and the Android sample allows local test traffic. Keep these development settings out of your production app.

Run the Android example on an emulator and the iOS example on a simulator to check permission handling, call states, mute/unmute, hangup, cancellation, and repeated starts. The supplied native UI checks require a running test broker and an available support destination; compiling an app alone does not verify a call.

Before rollout, also verify two-way speech and human acceptance, unavailable teams, interrupted networks, and server-enforced expiry. Match each result to its OpenCX `sessionId`. Simulators do not establish Bluetooth quality, cellular switching, locked-screen behavior, or performance on physical devices. Complete those checks on the devices your customers use.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.