@tsgwarp/webrtc
v0.1.0
Published
WARP Voice browser calling SDK — add a phone to your web app in minutes. Wraps SIP-over-WSS + WebRTC behind a clean Device/Call API.
Maintainers
Readme
@tsgwarp/webrtc
Add a phone to your web app in ~10 minutes. @tsgwarp/webrtc is the WARP Voice browser calling
SDK — the Twilio Voice JS / @telnyx/webrtc experience for the WARP platform. It hides SIP and
WebRTC entirely behind a small WarpClient / Call API. Your code never touches SDP, ICE, digest
auth, or a WebSocket.
Install. Until @tsgwarp/webrtc is published to your registry, install the hosted tarball:
npm install https://tsg-voice-artifacts-staging.s3.amazonaws.com/sdk/tsg@tsgwarp/webrtc-0.1.0.tgzOnce published it's the usual:
npm install @tsgwarp/webrtcIt ships as ESM (bundle it with Vite / webpack / esbuild) with a CommonJS build for tooling that
needs it. The only runtime dependency is sip.js (the SIP-over-WSS engine —
see Architecture).
The 5-line client
import { WarpClient } from '@tsgwarp/webrtc';
const phone = new WarpClient({ token, apiBase: 'https://api.warp.tsgglobal.net' });
phone.on('registered', () => phone.call('+15551234567'));
phone.on('incoming', (call) => call.answer());
await phone.register();token is the short-lived envelope your backend fetched from WARP (below). That's the whole
browser side of an outbound + inbound softphone.
The tiny server side (mint a token)
Your browser must never hold a long-lived API key. Your backend authenticates the logged-in user and asks WARP for a short-lived (default 5-minute), single-connection WebRTC token:
// POST /v1/connections/:id/tokens on the WARP Connections API.
app.get('/warp-token', async (req, res) => {
const r = await fetch(
`https://api.warp.tsgglobal.net/v1/connections/${CONNECTION_ID}/tokens`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${WARP_API_KEY}` },
body: JSON.stringify({ identity: req.user.id, ttl_seconds: 300 }),
},
);
res.json(await r.json()); // { token, identity, sip_domain, expires_in, wss, ice }
});The browser fetches /warp-token and passes the whole JSON to new WarpClient({ token }).
CONNECTION_ID is a Connection of type: "webrtc" (create one with
POST /v1/connections { name, type: "webrtc" }). Full runnable version:
examples/server-token.js.
What a token is
POST /v1/connections/:id/tokens returns an ephemeral SIP digest credential — a fresh
web-<rand> username + random secret, minted per session, installed live on the WARP SBCs, and
auto-expiring. Your account's real password never reaches the browser (the Twilio capability model):
{
"identity": "web-5f9475dabcdb",
"password": "10GGC6u8sdoIr2k_JO8TmNc9",
"sip_domain": "acme.warp.tsgglobal.net",
"expires_in": 300,
"wss": "wss://acme.warp.tsgglobal.net",
"ice": "/v1/webrtc/config"
}The SDK registers with identity + password (SIP digest over WSS — the golden SBC validates the
MD5(identity:sip_domain:password) HA1, not a JWT). Pass the whole envelope to WarpClient
(recommended — nothing is re-derived); pass apiBase so the relative ice path resolves.
API
new WarpClient(options) — alias Device
| option | type | notes |
| --- | --- | --- |
| token | string \| object | required — JWT string or the token envelope |
| apiBase | string | customer-api base URL; required only if the token's ice is a relative path |
| wss | string | override the SBC WSS URL (else derived from the token realm) |
| audioElement | HTMLAudioElement \| string | element (or id) for remote audio; auto-created if omitted |
| autoAnswer | boolean | auto-answer inbound calls (kiosk/agent mode) |
| logLevel | 'error'\|'warn'\|'log'\|'debug' | passed to sip.js |
Methods: register(), call(destination, { audio, video }) → Call, updateToken(token)
(re-REGISTER with a fresh token before the old one expires), disconnect().
Events: registered, unregistered, incoming (→ Call), call (outbound Call created),
disconnect, connected, state (idle→connecting→registered→…), ice, warn, error.
Call
Methods: answer({ audio, video }), hangup(), mute(on?) / unmute(), hold(on?) /
resume(), sendDigits('123#').
Properties: direction ('inbound'|'outbound'), remoteIdentity, state, muted, held,
remoteStream, endReason.
Events: state (connecting→ringing→active→held→ended), plus a convenience event per
state (call.on('active', …), call.on('ended', …)), track, mute, dtmf, error.
destination may be E.164 (+15551234567), a bare number, or a full sip: URI. Bare numbers are
routed through the account realm; the SBC applies the dialplan / LCR.
React
import { useWarpPhone } from '@tsgwarp/webrtc/react';
function Dialer({ token }) {
const { status, dial, hangup, incoming, activeCall, callState, muted, mute } =
useWarpPhone({ token, apiBase: 'https://api.warp.tsgglobal.net' });
if (incoming) return <button onClick={() => incoming.answer()}>Answer {incoming.remoteIdentity}</button>;
if (activeCall) return (
<>
<span>{callState}</span>
<button onClick={hangup}>Hang up</button>
<button onClick={() => mute(!muted)}>{muted ? 'Unmute' : 'Mute'}</button>
</>
);
return <button disabled={status !== 'registered'} onClick={() => dial('+15551234567')}>Call</button>;
}The hook builds the client, registers on mount (autoRegister, default on), tears it down on
unmount, and re-registers when the token changes. react is an optional peer dependency.
Full component: examples/react-dialer.jsx.
Architecture
your code ──▶ WarpClient ──▶ sip.js UserAgent ──▶ WSS ──▶ WARP SBC (proto_wss :5067)
│ │
├── GET /v1/webrtc/config (STUN/TURN) └── registers as a SIP endpoint
│ at <account>.warp.tsgglobal.net
└── getUserMedia + RTCPeerConnection ──▶ coturn TURN relay ──▶ RTP mediaSIP-over-WS engine: sip.js (sip.js@^0.21.2). Chosen over JsSIP because
it's the more actively maintained modern codebase, has a clean SessionDescriptionHandler
extension point for injecting our ICE config, ships first-class TypeScript types, and is already
the engine used by the WARP reference softphone (services/webrtc-sample/index.html) — so the SBC
WSS behaviour is already exercised against it. We wrap it entirely; sip.js is an internal
implementation detail and never appears in the public API.
On register() the SDK: resolves the token → fetches ICE/TURN from /v1/webrtc/config (falls back
to public STUN if unreachable) → opens the WSS transport to the SBC → REGISTERs the AoR
sip:<identity>@<realm> → emits registered. Inbound INVITEs become Call objects via the
incoming event; call() creates an outbound Call.
Auth model (token → REGISTER handshake)
The token is an ephemeral SIP digest credential — a fresh random identity + password minted
per session by the customer-api, installed live on the WARP SBCs at mint time, and auto-expiring.
The SDK registers over WSS with:
authorizationUsername = identityauthorizationPassword = password(the short-TTL secret from the token envelope)
The SBC's auth_db validates it exactly like any digest login — it checks the client's digest response
against the stored HA1 = MD5(identity : sip_domain : password) that the mint just installed. When the
token expires, a reaper removes that row from the SBC, so the credential stops working. There is no
JWT and no stored account password in the browser (the golden OpenSIPS build has no jwt.so;
digest is what it actually validates). This is the Twilio capability-token model, implemented with the
SBC's native digest auth.
This is LIVE — the SBC WebRTC path is un-stubbed and two-way audio is proven; register() +
call() work end-to-end against wss://‹account›.warp.tsgglobal.net. Nothing further is required on
the SBC side; the SDK needs no configuration beyond the token envelope.
Notes
- Browsers require HTTPS (or
localhost) forgetUserMedia; serve your app over TLS. - Tokens are short-lived. For long sessions, fetch a fresh token before
expires_inelapses and callphone.updateToken(newToken)(or, in React, pass the new token — the hook re-registers). - TURN credentials from
/v1/webrtc/configare ephemeral (HMAC, ~1h). The SDK fetches them fresh on everyregister().
