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

# Set up the browser (WebRTC) softphone

> Give your agents a full calling surface inside their browser — from the built-in dashboard softphone to an embedded SIP client in your own app — with caller-ID control, credentials you can rotate, and a mobile push path for inbound rings.

# Set up the browser (WebRTC) softphone

The browser softphone lets an agent place and answer PSTN calls without
leaving the browser — no desk phone, no desktop SIP app. There are three
ways to put it in front of an agent, and they share the same credential
and infrastructure underneath:

1. **The built-in dashboard softphone** — Orbit ships a ready-made
   softphone inside the dashboard. This is the zero-code path and the
   right starting point for most teams.
2. **An embedded LiveKit-based softphone** (`Softphone` in
   `@devotel/orbit-web-sdk`) — you mount the softphone inside your own
   agent workspace (CRM, contact-centre wall, internal tool) with your
   own UI.
3. **An embedded SIP-over-WSS softphone** (the SIP-register path) — a
   classic SIP client (e.g. JsSIP) that registers straight against
   Orbit's WebRTC-to-SIP gateway, for teams that want standard SIP
   semantics and their own call-control logic.

This guide walks through setup in the order you'll actually do it:
register devices, mint credentials, choose how inbound calls find the
softphone, wire a client, then verify.

<Note>
  The browser softphone only needs a browser with a microphone. Inbound
  push notifications (section 1) are a mobile companion for when agents
  move to the Orbit mobile app — they are not required for the browser
  itself.
</Note>

## 1. Device registration & VoIP push tokens (mobile)

Skip this section if your agents work only in the browser. Register a
**VoIP device** only when the agent also needs inbound calls to ring
their phone as a native call — i.e. the Orbit mobile app, backgrounded
or locked, still rings like a phone call instead of showing a passive
banner.

Registering a VoIP device is a single API call:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/push/voip-devices \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "ios",
    "voip_token": "APNS_VOIP_TOKEN_HEX",
    "bundle_id": "com.yourcompany.orbit-agent"
  }'
```

* `platform` is `ios` or `android`. On iOS `voip_token` is the PushKit
  (CallKit) token; on Android it's the FCM registration token used for
  the high-priority call channel.
* `bundle_id` (iOS only) is the app bundle id — the VoIP push topic is
  `<bundle_id>.voip`.
* The response returns the registered device (`id`, `platform`,
  `last_seen_at`).

Manage the fleet with `GET /push/voip-devices` (list) and
`DELETE /push/voip-devices/:id` (unregister a device). You normally
don't call these by hand — the mobile SDK registers, refreshes, and
removes its own row. A permanently-dead token (the OS revoked it, or
the app was uninstalled) is marked revoked automatically and skipped on
the next inbound ring, so a stale device never blocks the queue's ring
cycle.

<Warning>
  A VoIP token is **not** the same as a regular push token. iOS revokes
  the VoIP entitlement from apps that reuse it for marketing or alert
  pushes, so the two token types are kept strictly separate. Never send a
  non-call push to a `voip_token`.
</Warning>

## 2. Mint softphone credentials

An agent browser needs a short-lived credential before it can connect.
Which endpoint you mint against depends on the client you've chosen —
dashboard, embedded LiveKit, or embedded SIP.

### Built-in dashboard / embedded LiveKit softphone

The softphone's first move is a token request. `POST
/voice/softphone/token` returns a short-lived LiveKit access token plus
the media URL to connect to:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/voice/softphone/token \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Response (abridged):

```json theme={null}
{
  "data": {
    "token": "<livekit access token>",
    "media_url": "wss://..."
  }
}
```

The token is scoped to a per-user room so two agents never share a
session. Placing a call after connecting is `POST
/voice/softphone/dial` with `{ "to": "+15550123", "from": "+15559876"
}` — see the full snippet in section 4.

### Embedded SIP/WSS softphone

A SIP client registers directly over a WebSocket. `POST
/voice/softphone/register` swaps your authenticated API request for a
short-lived SIP credential:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/voice/softphone/register \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "callerIdNumber": "+15559876", "deviceLabel": "agent-desk-04" }'
```

Response (abridged):

```json theme={null}
{
  "data": {
    "sipUsername": "u_agent_x1a2b3",
    "sipPassword": "...",
    "sipDomain": "sip.orbit.devotel.io",
    "wssUrl": "wss://...",
    "iceServers": [{ "urls": "stun:..." }],
    "callerIdNumber": "+15559876",
    "expiresAt": 1716110400000
  }
}
```

Key properties to design around:

* Credentials live for **60 minutes** — re-register before `expiresAt`
  or the session drops.
* `callerIdNumber` must be a number your organization owns (it's 403'd
  otherwise) — this is how outbound CLI spoofing is blocked.
* The username binds to your organization, so a stolen username can't be
  replayed against a different tenant.
* Token issuance is **owner/admin/developer**-scoped — it's not a
  public, anonymous endpoint.

<Note>
  Revoke a credential immediately with `DELETE
    /voice/softphone/credentials/:username` if an agent leaves or a token
  leaks. Revocation is owner-only so a leaked credential stays a
  small-blast-radius event.
</Note>

## 3. Choose how inbound calls reach the softphone

Inbound calls must be routed so they land on an agent's browser. You
have three tenant-owned options — use the one that matches how your
phone numbers are set up.

* **DNIS → queue → softphone.** The most common path: your phone
  number's inbound routing sends the call to an ACD queue, and the
  queue offers it to an available agent's browser softphone (and, if
  they're on the mobile app, a VoIP push — section 1). You set this up
  per number in the dashboard's number routing, or through the flows
  builder.
* **DNIS → SIP routing.** Route inbound calls to the SIP domain your
  softphone registrations live on. Use this when your inbound traffic
  already lands on SIP (for example a DID forwarded to your SIP URI)
  and you want the registered softphone to answer directly.
* **The caller's softphone dials out.** For outbound-driven teams the
  inbound question never arises — the agent initiates calls and any
  inbound answer is on a number that routes to a queue (back to option
  one).

Whatever you choose, the softphone's own credential never changes —
sections 1, 2, and 4 are identical under any inbound topology. Inbound
routing is a property of the *number*, not the softphone.

## 4. Full JS snippet — mic, VoIP push + browser fallback

Here is a complete, copy-pasteable browser softphone. It works with
both embedded options (LiveKit and SIP-over-WSS) and handles the
browser-fallback reality: some agents will be on browsers or networks
where the primary media path is unavailable, so you always wire a
fallback.

```js theme={null}
import { OrbitSoftphone } from "@devotel/orbit-web-sdk";

const softphone = new OrbitSoftphone({
  apiBaseUrl: "https://api.orbit.devotel.io",
  apiKey: "YOUR_API_KEY", // serve from your backend — never commit it
  // Optional: force a media URL. Omit to use the token response.
  // mediaUrl: "wss://...",
  onCallStateChange: (s) => console.log("softphone state:", s),
  onError: (e) => handleSoftphoneFailure(e), // ← fallback lives here
});

// 1. Mic is gated on the browser's getUserMedia. Ask for it on a user
//    gesture (a click) — not page load — so the permission prompt is
//    attributed and the mic isn't held open on an idle tab.
async function armMicrophone() {
  await navigator.mediaDevices.getUserMedia({ audio: true });
  await softphone.connect(); // mints token + publishes mic
}

// 2. Place an outbound call.
async function callCustomer(e164) {
  return softphone.dial(e164); // returns call_id when ringing
}

// 3. Fallback: if the browser/media path can't connect, hand the call
//    to a click-to-call bridge (the caller's desk phone or mobile
//    rings, and the agent talks there). This is also the path when an
//    enterprise network blocks WebRTC entirely.
function handleSoftphoneFailure(err) {
  console.warn("WebRTC unavailable, falling back:", err.message);
  // show "call me on my device" UI → POST /voice/click-to-call instead
}

// VoIP push (mobile only): the browser cannot receive a VoIP push -
// mobile handles it natively. From the browser, the inbound ring just
// arrives when an inbound call is routed to the agent's room.
```

<Warning>
  WebRTC needs a `getUserMedia` grant and an HTTPS page. A softphone that
  "does nothing" is almost always one of: an HTTP (non-secure) page, a
  denied mic prompt, or a corporate network blocking UDP 3478.
</Warning>

## 5. Verification & troubleshooting

**Verify the setup end to end:**

1. Mint a credential (section 2), connect the client (section 4), and
   look for a room `connect` / SIP `registered` event.
2. Place an outbound call to your own mobile — you should hear ring,
   then two-way audio on answer.
3. Have an inbound call routed to your number (section 3) and confirm
   the softphone rings and answers.
4. If you registered a mobile VoIP device (section 1), background the
   mobile app and confirm a native incoming-call UI appears.

**Troubleshooting rules, in the order you'll hit them:**

| Symptom                                                    | Check                                                                                                         |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `403 Organisation does not own phone number …` on register | `callerIdNumber` isn't an active number in your account (it's an ownership gate, not a formatting bug).       |
| `SERVICE_UNAVAILABLE` / "softphone is not configured"      | The operator hasn't set the SIP WSS/HMAC config server-side — the browser can't proceed.                      |
| Mic prompt never appears                                   | Page isn't HTTPS, or you're requesting on page load instead of a user gesture.                                |
| Register/connect succeeds, call is silent                  | Corporate network blocked UDP/TURN — the ICE/TURN fallback should have kicked in; verify the ICE path.        |
| Session dies after \~60 min                                | The credential isn't being re-minted on `expiresAt` — refresh it.                                             |
| Mobile rings but browser doesn't (or vice versa)           | Both are expected to ring simultaneously — that's the parallel ring, not a fault.                             |
| Inbound doesn't reach the softphone                        | The number isn't routed to the queue or SIP URI your softphone sits on — fix in section 3, not in the client. |

Also confirm you aren't reusing the wrong endpoint: register (SIP/WSS)
↔ SIP clients, token ↔ LiveKit clients, `voip-devices` ↔ mobile push
only.

## Where to go next

* [SIP trunk setup](/guides/sip-trunk-setup) — if you want to bring
  your own carrier/trunk alongside the browser softphone.
* [Voice queues](/guides/voice-queues) — the ACD queue inbound calls
  route through before a softphone answers.
* [Web SDK](/sdks/web) — full event map for `Softphone` and the other
  SDK surfaces.
