#Recipe: Agents and the browser softphone
One agent (SIP account agent_{agent_ref}) per CRM user. Creating returns a long-lived SIP
password once (for desk phones/apps). Browsers use POST /v1/agents/{id}/softphone-config, which
returns the WSS URL, ICE/TURN servers and a password that expires (default 1 hour).
Browser side (see /docs/softphone.md):
import { Softphone } from '@voice/softphone';
const phone = new Softphone({ getConfig: () => fetch('/crm/softphone-config').then((r) => r.json()) });
phone.on('incoming', (call) => { if (call.platformCallId === expectedCallId) phone.answer(call.id); });
await phone.start(); // registers; refreshes credentials before they expire
#TypeScript (SDK)
// Create a SIP account per CRM user and give the browser short-lived softphone credentials.
import { createVoiceClient } from '@voice/sdk';
const voice = createVoiceClient({ baseUrl: process.env.VOICE_API_URL!, apiKey: process.env.VOICE_API_KEY! });
const crmUserId = process.env.DOCS_AGENT_REF ?? `docs-${Date.now()}`;
// Idempotent "ensure agent": create, or reuse when it already exists (409 CONFLICT).
const created = await voice.POST('/v1/agents', { body: { agent_ref: crmUserId, display_name: 'Docs Example' } });
if (created.error && created.error.error.code !== 'CONFLICT') throw new Error(created.error.error.message);
// Your backend calls this and hands the result to the browser softphone widget.
// The password expires (default 1 h); the widget asks your backend again before then.
const { data: config, error } = await voice.POST('/v1/agents/{id}/softphone-config', { params: { path: { id: crmUserId } } });
if (error) throw new Error(error.error.message);
console.log('websocket', config.sip.websocket_url, 'user', config.sip.username, 'expires', config.sip.expires_at);
console.log('ice servers', config.ice_servers.length);
#curl
curl -sS -X POST "$VOICE_API_URL/v1/agents" \
-H "Authorization: Bearer $VOICE_API_KEY" -H "Content-Type: application/json" \
-d "{\"agent_ref\":\"docs-curl-$(date +%s)\",\"display_name\":\"Docs (curl)\"}"
#Notes
LIMIT_EXCEEDED(details.limit = max_agents): the workspace agent limit is reached.registration.stateon the agent (andagent.registered/agent.unregisteredevents) shows whether the softphone is online.
This example is executed against the sandbox in CI (tests/e2e/specs/m6-docs.spec.ts).