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
POST /hooks/events— webhook interno del carrier. No lo expongas ni lo documentes hacia afuera; no aplica autenticación de agente.POST /api/users/sync— usado por tu backend (no por el navegador del agente) para crear/desactivar/actualizar agentes, protegido con el mismo headerx-sync-secretque/api/agents/session. No es parte del flujo del agente.{ "action": "create", "email", "name", "team_name"?, "needs_webphone"?: true, "area_code"? }— el email tiene que estar pre-aprobado de nuestro lado antes de llamar esto. El secret prueba que sos vos, no que ese email en particular es un agente real esperado. Si mandás uncreatepara un email que tu contacto en 2CXVoIP todavía no pre-aprobó, la respuesta es403 { "error": "Email no pre-aprobado por el admin de esta organización" }y no se crea nada ni se gasta dinero. Coordiná la lista (email + nombre, opcionalmente área) con tu contacto antes de sincronizar — elnameque él haya cargado gana sobre el que mandes acá. Conneeds_webphone: true(default), intentamos comprar y asignar un número real automáticamente; pasáarea_code(ej."916") si te importa de qué zona (si no mandás uno, usamos el que haya quedado guardado en la pre-aprobación). Esa misma pre-aprobación es donde queda fijado el password 2CXVoIP de ese agente (verPOST /api/agents/sessionen §1) — te lo compartimos junto con la confirmación de la pre-aprobación, no hace falta pedirlo aparte. La respuesta traeline(el número, si se asignó) ypending_did(true si quedó en cola para asignación manual).{ "action": "deactivate", "email" }— soft-delete del agente (status →inactive) y desasigna su línea. El número nunca se libera de vuelta al carrier desde este endpoint — solo queda sin asignar, así que nada se borra ni deja de facturarse por accidente. Liberar un número de verdad es una acción deliberada de nuestro lado, no algo que un sync pueda disparar.{ "action": "reactivate", "email" }— deshace un deactivate.{ "action": "update", "email", "name"?, "role"?, "team_name"? }— empuja un cambio que pasó de tu lado (ej. el agente cambió de equipo) para que quede reflejado acá sin que un admin toque el panel. Solo se mueven así name/role/team — status y asignación de línea siguen yendo por create/deactivate/swap-line a propósito.
GET /api/agents— roster server-to-server, misma autenticaciónx-sync-secret. Devuelve todos los agentes de tu organización con su línea actual, para que tu CRM refleje la lista de agentes del panel sin necesitar un JWT de admin:{ "agents": [ { "id", "email", "name", "role", "status", "last_seen", "team", "line" } ] }
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.