2CXVoIP Communications API — CRM integration guide
This API gives you: agent login, a session token for the embedded softphone, a realtime channel for calls/SMS/presence, and SMS sending. Your CRM owns the user's session (who's logged in, which tab, per-contact conversation history) — this API only provides the communications infrastructure (phone line, balance, and call events).
Base URL: https://webphone.2cxvoip.com
Every error response uses this shape: { "error": "message" }
1. Authentication
POST /auth/login
// body
{ "email": "agent@yourcompany.com", "password": "..." }
// 200
{
"token": "<jwt, valid 7 days>",
"user": {
"id": "uuid", "email": "...", "name": "...", "role": "agent|supervisor|admin",
"status": "active", "team": "Sales", "line": "+19165551234"
}
}
Store token and send it as Authorization: Bearer <token>
on every subsequent request (including the WebSocket auth message, see §3).
Rate limit: 5 attempts per IP every 15 minutes.
GET /auth/me
Header: Authorization: Bearer <token>. Returns the same shape
as user above — use it to validate the token when your app loads.
2. Embedded softphone
GET /api/webphone/token
Header: Authorization: Bearer <token>.
// 200
{ "session_token": "...", "line": "+19165551234", "expires_in": 900 }
session_token is valid for ~15 minutes — request a new one before
it expires (e.g. every 10 min while the agent is active). If the agent doesn't
have a line assigned yet, the API responds
503 { "error": "No phone line assigned yet..." }.
Softphone widget
Don't install any third-party SDK. Load the 2CXVoIP widget straight from the CDN:
<script src="https://cdn.2cxvoip.com/softphone.js"></script>
This exposes window.TwoCXSoftphone:
TwoCXSoftphone.connect(session_token) // from GET /api/webphone/token above
TwoCXSoftphone.on('ready', () => {...})
TwoCXSoftphone.on('incoming', ({ from }) => {...}) // show your "incoming call" popup
TwoCXSoftphone.on('callStateChange', ({ state }) => {...})
TwoCXSoftphone.on('error', (err) => {...})
TwoCXSoftphone.dial('+19165551234') // outbound call
TwoCXSoftphone.answer()
TwoCXSoftphone.reject()
TwoCXSoftphone.hangup()
TwoCXSoftphone.disconnect()
Honest note: the widget removes any carrier dependency from your own code and
package.json — that's the exposure that matters in practice. It does
not guarantee the browser's network traffic is 100% unrecognizable to someone
deliberately inspecting the Network tab; that's outside what a client-side
wrapper can control.
3. Realtime channel — WS /ws/presence
Connect one socket per logged-in agent. The first message must always be auth:
→ { "type": "auth", "token": "<jwt from /auth/login>" }
← { "type": "auth_ok", "userId": "uuid" }
// or { "type": "auth_error" } followed by the socket closing
Messages you can send
{ "type": "status", "status": "available" | "busy" | "in_call" | "offline" }
{ "type": "ping" } // heartbeat, replies with { "type": "pong" }
Events the server pushes
presence_update — full snapshot whenever any agent's status changes (useful for a wallboard):
{ "type": "presence_update", "agents": [
{ "id": "uuid", "name": "...", "email": "...", "role": "agent",
"team": "Sales", "line": "+1916...", "status": "available" }
] }
call_incoming — a call is ringing on the agent's line. This is
just the signal to show your "incoming call" popup with caller info — the
softphone (browser widget, authenticated with session_token) is
what actually answers/rings:
{ "type": "call_incoming", "from": "+1...", "to": "+1916...", "callControlId": "..." }
sms_inbound — an inbound SMS already saved, to refresh the contact's conversation thread:
{ "type": "sms_inbound", "conversationId": "uuid", "from": "+1...", "to": "+1916...",
"text": "...", "contactId": "uuid" }
Every event is delivered only to the socket of the agent who owns the line — there's no broadcast between agents.
GET /api/presence
Initial snapshot (same shape as presence_update.agents) for when
your app loads and doesn't have the WebSocket connected yet.
4. SMS
POST /api/sms/send
// body
{ "contact_id": "uuid", "body": "message text" }
Sends from the authenticated agent's assigned line. 404 if the
contact doesn't exist, 503 if the agent has no line.
GET /api/contacts/:id/thread
Full history (SMS + calls) for a contact:
{
"contact": { "id": "...", "phone_e164": "...", "name": "...", "email": "..." },
"messages": [{ "id", "direction", "body", "sent_at", "agent_name" }],
"calls": [{ "id", "direction", "status", "duration_seconds", "started_at", "ended_at", "agent_name" }]
}
Useful if your CRM doesn't have its own conversation store yet and wants to lean on this endpoint — not required, just a convenience.
5. What is NOT part of this API
POST /hooks/events— internal carrier webhook. Don't expose or document it externally; it doesn't use agent authentication.POST /api/users/sync— used by your backend (not the agent's browser) to create/deactivate agents, protected by anx-sync-secretheader shared out of band. Not part of the agent flow.
6. CORS
Your frontend's domain needs to be on the server's allowlist before you can call this API from the browser. Email your domain(s) and your client number to support@2cxvoip.com to get them added.