Softphone Integration Guide
Everything your application needs to embed the WebRTC softphone iframe and trigger outbound calls via the REST API.
⬡ Architecture
The softphone is a hidden iframe that lives inside your CRM page. It registers to CenterPoint automatically on load and auto-answers incoming calls. Your backend server triggers calls via a REST endpoint — the iframe handles the rest.
/calls/originate.
⬚ Embedding the iframe
Add a single hidden iframe to your CRM page. Generate a signed JWT on your server and inject it into the src. The iframe handles everything else.
<!-- Invisible softphone endpoint — renders nothing to the agent --> <iframe src="https://agam1.zyx.bz/softphone.html?jwt={{ signed_jwt }}" allow="microphone; autoplay" style="display:none" ></iframe>
policy: BLOCKED if this is missing.
JWT refresh
JWTs have a recommended TTL of 1 hour. The iframe decodes the exp claim locally and reloads itself 60 seconds before expiry — no action required on your side. To force a refresh, update the src with a new JWT.
🔑 JWT format
JWTs are signed with your tenant's jwt_secret using HS256. We provide the secret when your tenant is provisioned.
Payload claims
| Claim | Type | Description |
|---|---|---|
tid | string (UUID) | Your tenant ID — provided by us at onboarding |
sub | string | The agent's user ID in your system — must match a provisioned extension |
iat | number | Issued-at timestamp (Unix seconds) — set automatically by most JWT libraries |
exp | number | Expiry timestamp — recommend iat + 3600 (1 hour) |
Signing examples
const jwt = require('jsonwebtoken'); const token = jwt.sign( { tid: 'your-tenant-uuid', sub: agentUserId }, process.env.SOFTPHONE_JWT_SECRET, { algorithm: 'HS256', expiresIn: '1h' } );
import jwt, os from datetime import datetime, timedelta, timezone token = jwt.encode( { "tid": "your-tenant-uuid", "sub": agent_user_id, "exp": datetime.now(timezone.utc) + timedelta(hours=1), }, os.environ["SOFTPHONE_JWT_SECRET"], algorithm="HS256", )
// composer require firebase/php-jwt use Firebase\JWT\JWT; $token = JWT::encode([ 'tid' => 'your-tenant-uuid', 'sub' => $agentUserId, 'exp' => time() + 3600, ], $_ENV['SOFTPHONE_JWT_SECRET'], 'HS256');
jwt_secret must stay on your server. Always sign server-side and inject the token into the iframe src.
↗ Triggering a call
When an agent clicks a contact in your CRM, your backend server calls POST /calls/originate. The service resolves the agent's extension and instructs CenterPoint to dial — the iframe auto-answers without any agent interaction.
POST /calls/originate X-Api-Key: <your api_key> Content-Type: application/json { "agentUserId": "crm-user-id-123", // same value used as JWT sub "customerNumber": "0501234567" }
HTTP 200
{ "success": true, "uniqueid": "1745000000.42" }
postMessage events from the iframe to track call state.
Error responses
| Status | Reason |
|---|---|
400 | Missing agentUserId or customerNumber |
401 | Missing or invalid X-Api-Key |
404 | Agent not provisioned or deactivated |
502 | CenterPoint unreachable — contact support |
Code example
async function clickToDial(agentUserId, customerNumber) { const res = await fetch('https://agam1.zyx.bz/calls/originate', { method: 'POST', headers: { 'X-Api-Key': process.env.SOFTPHONE_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ agentUserId, customerNumber }), }); if (!res.ok) throw new Error(await res.text()); return res.json(); // { success: true, uniqueid: "..." } }
⇄ postMessage events
The iframe posts all call state events to window.parent. Listen for them in your CRM page to update the UI.
window.addEventListener('message', (e) => { // Optional: restrict to the softphone origin // if (e.origin !== 'https://agam1.zyx.bz') return; const { type, ...data } = e.data; switch (type) { case 'registered': console.log('Agent ready'); break; case 'answered': startCallTimer(); break; case 'hangup': endCallTimer(); break; case 'error': console.error('SIP error', data.reason, data.phase); break; } });
Event reference
{ uri }registered fires again.{ from, displayName }{ from, displayName }{ from, displayName }{ reason: "already_in_call" }{ reason }{ reason, phase } — phase: config · startup · accept · initSending commands to the iframe
const iframe = document.querySelector('iframe'); iframe.contentWindow.postMessage({ type: 'hangup' }, '*');
⚿ Security notes
| Concern | How it's handled |
|---|---|
| JWT secret exposure | Per-tenant secrets — your secret cannot be used by other tenants. Never put it in client-side code. |
| API key scope | Your api_key can only trigger calls for your own agents. It has no access to any other tenant's data or admin operations. |
| Transport security | All endpoints are served over HTTPS. Media is encrypted end-to-end (DTLS-SRTP — browser WebRTC standard). |
Rate limiting /calls/originate |
Apply rate limiting in your backend before calling this endpoint to prevent accidental call flooding. |
SOFTPHONE_JWT_SECRET and SOFTPHONE_API_KEY in your environment or secrets manager — never in source code.
▷ Live Demo
Test your credentials end-to-end without writing any backend code. Server-side steps show what your backend would do; client-side steps show what the browser does.
jwt_secret and api_key are processed in this browser tab — never use this demo in production or share your screen with these fields populated.
iframe.contentWindow.postMessage({ type: 'hangup' }, '*')