2CXVoIPDevelopers
2CXVoIP · Documentación para desarrolladores

2CXVoIP Communications API — guía de integración para el CRM

Esta API te da: login de agentes, un token de sesión para el softphone embebido, un canal en tiempo real para llamadas/SMS/presencia, y envío de SMS. La sesión del usuario (quién está logueado, en qué pestaña, su historial de conversación por contacto) la maneja tu CRM — esta API solo provee la infraestructura de comunicaciones (línea telefónica, saldo, y los eventos de la llamada).

Base URL: https://webphone.2cxvoip.com

Todas las respuestas de error usan el formato: { "error": "mensaje" }

1. Autenticación

POST /auth/login

// body
{ "email": "agente@tuempresa.com", "password": "..." }
// 200
{
  "token": "<jwt, válido 7 días>",
  "user": {
    "id": "uuid", "email": "...", "name": "...", "role": "agent|supervisor|admin",
    "status": "active", "team": "Ventas", "line": "+19165551234"
  }
}

Guarda token y mándalo como Authorization: Bearer <token> en cada request subsecuente (incluyendo el mensaje de auth del WebSocket, ver §3).

Rate limit: 5 intentos por IP cada 15 minutos.

POST /api/agents/session — server-to-server, requiere el password 2CXVoIP del agente

Tu backend llama esto por cada agente (nunca desde su navegador) para obtener el JWT + el token del softphone en una sola llamada. Requiere dos credenciales distintas: el secret de tu organización (x-sync-secret, igual que en /api/users/sync) y el password 2CXVoIP de ese agente específico — tener el secret ya no alcanza para pedir sesión de cualquier email de tu organización, tiene que ser exactamente ese agente.

Ese password no lo elige ni lo escribe el agente — te lo compartimos nosotros por fuera (el mismo momento en que pre-aprobamos su email, ver §5) y tu backend lo guarda/reenvía en cada request, igual que ya hacías con el secret. Si lo perdiste o necesitás rotarlo, pedinoslo — se puede regenerar en cualquier momento sin tocar el email ni el resto de los datos del agente.

Header: x-sync-secret: <el secret de tu organización>
Body:   { "email": "agente@tuempresa.com", "password": "<el password que te compartimos para este agente>" }
// 200
{
  "token": "<jwt, válido 7 días — igual que /auth/login>",
  "user": { "id", "email", "name", "role", "status", "team", "line" },
  "webphone": { "session_token": "...", "line": "+1916...", "expires_in": 900 }
  // webphone es null si el agente todavía no tiene línea/credential asignada
}

400 si falta email o password, 401 si el password no coincide, 404 si el email no existe en tu organización (nunca resuelve agentes de otro cliente), 403 si el agente no está active todavía.

Cambio de contrato (2026-09-04): antes este endpoint no pedía password, solo el secret de la organización — cualquiera con el secret podía pedir sesión para cualquier email adivinado, sin autenticar realmente al agente. Si ya tenías una integración contra la versión vieja, vas a tener que agregar el campo password a cada llamada.

GET /auth/me

Header: Authorization: Bearer <token>. Devuelve el mismo shape que user arriba — úsalo para validar el token al cargar tu app.

2. Softphone embebido

GET /api/webphone/token

Header: Authorization: Bearer <token>.

// 200
{ "session_token": "...", "line": "+19165551234", "expires_in": 900 }

session_token es válido ~15 minutos — pide uno nuevo antes de que expire (por ejemplo cada 10 min mientras el agente esté activo). Si el agente todavía no tiene línea asignada, la API responde 503 { "error": "No phone line assigned yet..." }.

Widget del softphone

No instales ningún SDK de terceros. Carga el widget de 2CXVoIP directo desde el CDN:

<script src="https://cdn.2cxvoip.com/softphone.js"></script>

Esto expone window.TwoCXSoftphone:

TwoCXSoftphone.connect(session_token, line)  // ambos vienen del GET /api/webphone/token de arriba
TwoCXSoftphone.on('ready', () => {...})
TwoCXSoftphone.on('incoming', ({ from }) => {...})   // muestra tu popup de llamada entrante
TwoCXSoftphone.on('callStateChange', ({ state }) => {...})
TwoCXSoftphone.on('error', (err) => {...})

TwoCXSoftphone.dial('+19165551234')          // llamada saliente
TwoCXSoftphone.answer()
TwoCXSoftphone.reject()
TwoCXSoftphone.hangup()
TwoCXSoftphone.disconnect()

Mute / hold

TwoCXSoftphone.mute()          // o unmute() / toggleMute()
TwoCXSoftphone.isMuted()       // boolean, se puede leer en cualquier momento

TwoCXSoftphone.hold()          // devuelve una Promise — o unhold() / toggleHold()
TwoCXSoftphone.isOnHold()      // boolean — también se refleja como callStateChange({ state: 'held' })

TwoCXSoftphone.on('muteStateChange', ({ muted }) => {...})

hold()/unhold() son async — el carrier confirma el hold antes de que callStateChange dispare con state: 'held'. Reflejá eso en tu UI en vez de asumir que el pedido se aplicó al instante.

DTMF (teclado durante la llamada)

TwoCXSoftphone.dtmf('1')       // una tecla por llamada: 0-9, *, #

El teclado (botones 0-9, *, #) lo armás vos del lado de tu UI, y llamás a dtmf() en cada tecla presionada. El tono viaja de par a par sobre la llamada WebRTC ya conectada — nunca pasa por nuestro backend, así que no hay evento ni respuesta que esperar. Solo funciona con una llamada activa; llamarlo sin llamada activa no hace nada.

Librería de sonidos

Automático, no requiere nada de tu lado. El widget conecta un ringtone para llamadas entrantes y un tono de ringback para llamadas salientes directo en el SDK del carrier. Los dos son tonos sintetizados (sin audio con licencia de por medio) y los dos sonan en el navegador del agente — no se le envían al que llama. El hold (hold()) es distinto: es una acción de señalización real que se manda al carrier, que reproduce su propia música de espera a quien esté conectado con esa pierna — no es algo que genere este widget.

Diagnóstico de calidad de llamada

El widget expone el monitor de red/media en tiempo real que ya trae el SDK del carrier, solo para la pierna del agente (no tiene visibilidad de la pierna del que llama):

TwoCXSoftphone.on('qualityWarning', (event) => {
  const w = event.warning
  console.log(w.name, w.message)   // ej. HIGH_JITTER, HIGH_PACKET_LOSS, LOW_MOS, ICE_CONNECTIVITY_LOST
})

Usalo para mostrarle al agente un aviso de "tu conexión está inestable", o para loguear estos eventos de calidad del lado tuyo y correlacionarlos después. Es solo diagnóstico — el SDK ya intenta recuperarse solo (ICE restart, reconexión); no hace falta reaccionar para mantener la llamada viva.

Nota honesta: el widget saca cualquier dependencia del carrier de tu propio código y package.json — es la exposición práctica que más importa. No garantiza que el tráfico de red del navegador sea 100% irreconocible ante alguien inspeccionando la pestaña Network a propósito; eso queda fuera de lo que un wrapper del lado del cliente puede controlar.

Si además querés que estos avisos queden guardados de nuestro lado (para que un supervisor los vea después en el panel, no solo en tu console.log), reenvialos con TwoCXClient — TwoCXSoftphone no tiene token ni cliente REST propio, así que esta única línea de conexión queda de tu lado:

TwoCXSoftphone.on('qualityWarning', (event) =>
  client.reportQualityEvent(TwoCXSoftphone.getActiveCallControlId(), event.warning.name, event.warning.message))

Transferencia con consulta (interna o externa, beta)

Usa window.TwoCXClient (ver abajo), no el widget del softphone directamente — es una acción REST contra nuestra propia API, no una llamada al SDK del carrier. Solo funciona en una llamada entrante (un cliente que llama); una llamada saliente que hace el agente no tiene una pierna del que llama separada para poner en espera, así que todavía no hay nada que transferir con consulta ahí.

Pasá exactamente uno de targetUserId (un agente de esta plataforma) o targetNumber (cualquier número PSTN, E.164 — ej. alguien que no está en esta plataforma) — misma mecánica en los dos casos.

const callId = TwoCXSoftphone.getActiveCallControlId()
const { consultCallControlId, targetAgent, targetNumber } =
  await client.transferConsult(callId, { targetUserId })   // o { targetNumber: '+15551234567' }
// el cliente queda en espera (escucha la música de espera del carrier); vos quedás
// conectado en privado con el destino para confirmar antes de conectarlo

client.on('transfer_status', ({ status }) => {
  // 'consult_answered' — atendió, mostrá tu UI de completar/cancelar
  // 'consult_failed' — rechazó/colgó; el cliente ya está restaurado,
  //                     no hace falta hacer nada de tu lado
})

await client.transferComplete(callId)   // conecta cliente <-> targetAgent, te desconecta a vos
// o:
await client.transferCancel(callId)     // cancela, restaura al cliente con vos

Todavía sin probar en vivo contra una llamada real de dos agentes — tratalo como beta hasta confirmarlo.

Música de espera real (no solo comfort noise)

El hold del proveedor, disparado del lado del cliente con TwoCXSoftphone.hold(), solo produce "comfort noise" — un ruido de fondo apenas perceptible para que no se corte por timeout, no música. Para música real de espera en una llamada entrante, usá TwoCXClient en su lugar:

const callId = TwoCXSoftphone.getActiveCallControlId()
await client.holdCall(callId)    // o unholdCall(callId)

Misma limitación de solo-entrantes que la transferencia de arriba — cae a TwoCXSoftphone.hold() para llamadas salientes.

3. Canal en tiempo real — WS /ws/presence

Conecta un socket por agente logueado. Primer mensaje siempre debe ser auth:

→ { "type": "auth", "token": "<jwt de /auth/login>" }
← { "type": "auth_ok", "userId": "uuid" }
   // o { "type": "auth_error" } seguido de cierre del socket

Mensajes que puedes enviar

{ "type": "status", "status": "available" | "busy" | "in_call" | "offline" }
{ "type": "ping" }   // heartbeat, responde { "type": "pong" }

Eventos que el servidor te empuja

presence_update — snapshot completo cada vez que cualquier agente cambia de estado (útil para un wallboard):

{ "type": "presence_update", "agents": [
  { "id": "uuid", "name": "...", "email": "...", "role": "agent",
    "team": "Ventas", "line": "+1916...", "status": "available" }
] }

call_incoming — una llamada está entrando a la línea del agente. Es solo la señal para mostrar el popup de "llamada entrante" con datos del caller en tu UI — el softphone (widget del navegador, autenticado con session_token) es quien realmente contesta/timbra:

{ "type": "call_incoming", "from": "+1...", "to": "+1916...", "callControlId": "..." }

sms_inbound — mensaje SMS entrante ya guardado, para refrescar el hilo de conversación del contacto:

{ "type": "sms_inbound", "conversationId": "uuid", "from": "+1...", "to": "+1916...",
  "text": "...", "contactId": "uuid" }

Cada evento solo se entrega al socket del agente dueño de la línea — no hay broadcast entre agentes.

GET /api/presence

Snapshot inicial (mismo shape que presence_update.agents) para cuando tu app carga y todavía no tiene el WebSocket conectado.

4. SMS

POST /api/sms/send

// body — opción A: contacto existente
{ "contact_id": "uuid", "body": "texto del mensaje" }

// body — opción B: número nuevo (crea el contacto si no existe)
{ "to": "+1916...", "name": "opcional", "body": "texto del mensaje" }

Envía desde la línea asignada al agente autenticado. Pasá contact_id para responder dentro de un hilo existente, o to para arrancar uno nuevo — el contacto se crea automáticamente si ese número todavía no existe. 404 si contact_id no existe, 503 si el agente no tiene línea.

// 201
{ "message": {...}, "conversation_id": "uuid", "contact_id": "uuid" }

Guardá el contact_id devuelto para futuras respuestas en el mismo hilo.

GET /api/contacts/:id/thread

Historial completo (SMS + llamadas) de un contacto:

{
  "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" }]
}

Útil si tu CRM todavía no tiene su propio store de conversaciones y quiere apoyarse en este endpoint — no es obligatorio, es una conveniencia.

5. Lo que NO es parte de esta API

6. CORS

Tu dominio de frontend debe estar en el allowlist del servidor antes de poder llamar esta API desde el navegador. Escribí a support@2cxvoip.com con tu(s) dominio(s) y tu número de cliente para que los agreguemos.