API y MCP de su propia centralita
Su propia centralita Cenaly está abierta a sus programas y asistentes de IA:
- CRM, sistema de contabilidad, su propio sitio web — REST API: registro de llamadas, grabaciones y transcripciones, llamada con un clic, extensiones, desvío de llamadas, reglas de llamadas;
- Claude Code, Claude Desktop, Cursor — servidor MCP remoto mediante clave;
- ChatGPT, claude.ai, Perplexity, Cursor y VS Code — el mismo servidor MCP sin clave: usted inicia sesión en su cuenta y autoriza el acceso (OAuth).
Todo funciona en nombre del propietario de la cuenta y solo con los permisos que este haya otorgado. Cada cambio queda visible en el registro de acciones de la centralita con el nombre de la clave o de la aplicación.
Direcciones#
| Qué | Dirección |
|---|---|
| REST API | https://api.cenaly.com/pbx-api/v1 |
| Servidor MCP (streamable HTTP) | https://api.cenaly.com/pbx-api/mcp |
| Documentación de métodos | api.cenaly.com/pbx-api/v1/docs |
| Descripción OpenAPI 3.1 | api.cenaly.com/pbx-api/v1/openapi.json |
| Metadatos OAuth | https://api.cenaly.com/.well-known/oauth-authorization-server/pbx-api |
Clave y permisos#
La clave la crea solo el propietario de la cuenta: Telefonía → API y MCP → Claves → Crear clave. Un empleado con el rol «Administrador de la centralita» no ve las claves — la clave actúa en nombre del propietario.
- La clave con formato
cpx_…se muestra una sola vez — justo después de crearla, junto con fragmentos listos para conectarse. Solo guardamos su huella (SHA-256); no es posible recuperar la clave — solo emitir una nueva. - La clave se puede limitar por locales (si no, todos los locales, incluidos los futuros) y por plazo (30, 90, 365 días o indefinido).
- «Permisos» cambia los permisos y los locales de la clave al vuelo: la integración sigue funcionando y los nuevos permisos se aplican desde la siguiente solicitud.
- «Revocar» — de inmediato y para siempre: la siguiente solicitud con esta clave recibirá 401.
- Claves y conexiones activas — hasta 20 por cuenta.
Los permisos se eligen con un conjunto predefinido o con casillas:
| Permiso | Qué otorga | Conjuntos |
|---|---|---|
calls:read |
registro de llamadas y ficha de llamada con su ruta | CRM, asistente de IA, solo lectura, todo |
recordings:read |
enlace a la grabación de la conversación (10 minutos) y transcripción | CRM, asistente de IA, solo lectura, todo |
calls:dial |
llamada con un clic: primero suena el teléfono del empleado y, tras la respuesta, la centralita marca el número | CRM, asistente de IA, todo |
extensions:read |
extensiones, grupos, desvío de llamadas, «no molestar» | CRM, asistente de IA, solo lectura, todo |
extensions:write |
cambiar el desvío personal y «no molestar» | asistente de IA, todo |
presence:read |
quién está libre, llamando o hablando | asistente de IA, solo lectura, todo |
reports:read |
métricas del centro de llamadas por periodo: nivel de servicio, espera, abandonadas, AHT, FCR, CSAT, dónde perdemos llamadas, desgloses por colas, líneas y empleados | solo lectura, todo |
routes:read |
números, reglas de entrada y salida, historial de reglas | solo lectura, todo |
routes:write |
crear, cambiar, eliminar y reordenar reglas de llamadas, revertir desde el historial | solo con casilla aparte |
config:export |
exportación de la configuración de la centralita en formato de archivo | solo con casilla aparte |
config:import |
carga de la configuración desde tablas: vista previa y luego aplicación | solo con casilla aparte |
webhooks:write |
crear, cambiar, comprobar y eliminar webhooks de eventos de llamadas | solo con casilla aparte |
daynight:write |
cerrar el local hasta el final del día o hasta una fecha; abrir — lo mismo que *28 desde el teléfono |
solo con casilla aparte |
Los permisos que cambian algo o consumen minutos están marcados en ámbar en la ventana. Los tres últimos permisos no forman parte de ningún conjunto, ni siquiera de «Todo» — solo se pueden conceder de forma consciente, con una casilla aparte.
ChatGPT, claude.ai, Perplexity, Cursor, VS Code: conexión sin clave#
Los asistentes en la nube no saben insertar una clave en la cabecera — conectan el servidor MCP por OAuth. No hace falta crear una clave.
claude.ai: Ajustes → Conectores → Añadir su propio conector → pegue https://api.cenaly.com/pbx-api/mcp → Conectar.
ChatGPT: Ajustes → Aplicaciones y conectores → Avanzado → active el modo desarrollador → Crear → pegue https://api.cenaly.com/pbx-api/mcp, autenticación — OAuth.
Perplexity: Ajustes de la cuenta → Conectores → «+ Su propio conector» → Remoto → pegue https://api.cenaly.com/pbx-api/mcp, autenticación — OAuth.
Cursor y VS Code: añada un servidor MCP remoto (HTTP) con la dirección https://api.cenaly.com/pbx-api/mcp sin cabecera con clave — la ventana de inicio de sesión se abrirá en el navegador. En Cursor: .cursor/mcp.json → {"mcpServers": {"cenaly-pbx": {"url": "https://api.cenaly.com/pbx-api/mcp"}}}; en VS Code: el comando «MCP: Add Server» → HTTP.
Después, el asistente abrirá la ventana de inicio de sesión de Cenaly. Tras iniciar sesión verá:
- quién solicita el acceso — la aplicación se nombra por la dirección a la que volverá la respuesta: «Conector verificado · chatgpt.com» (lo mismo — claude.ai, perplexity.ai, vscode.dev, Cursor), «Programa en este equipo» o la advertencia «Dirección desconocida». El nombre que envió la aplicación se muestra en letra pequeña — lo puede escribir cualquiera;
- permisos — está marcado lo que solicita la aplicación (si pide «todo seguido», se marca el conjunto «Asistente de IA»); los permisos «solo con casilla aparte» nunca se marcan de antemano;
- locales — todos o los seleccionados.
«Permitir» le devuelve al asistente y este ve de inmediato las herramientas de la centralita. La conexión aparece en Telefonía → API y MCP → Claves como una línea con la etiqueta OAuth y la dirección de la aplicación — ahí mismo se cambian los permisos y se revoca el acceso. Volver a conectar la misma aplicación actualiza sus permisos en lugar de crear una segunda conexión.
Solo el propietario de la cuenta puede autorizar conexiones.
Claude Code, Claude Desktop, Cursor: conexión por clave#
Claude Code — con un solo comando:
claude mcp add --transport http telephony https://api.cenaly.com/pbx-api/mcp --header "Authorization: Bearer cpx_…"
Claude Desktop, Cursor (.cursor/mcp.json) y otros clientes con archivo de configuración:
{
"mcpServers": {
"telephony": {
"type": "http",
"url": "https://api.cenaly.com/pbx-api/mcp",
"headers": { "Authorization": "Bearer cpx_…" }
}
}
}
Los fragmentos listos para pegar con su clave se muestran justo después de crear la clave. El asistente solo ve las herramientas permitidas por la clave y puede responder a «quién llamó hoy y no logró comunicarse», «vuelve a llamar a este cliente desde el teléfono de Anna», «actívame el desvío al móvil».
REST API#
La clave va en el encabezado Authorization: Bearer <clave> (o X-Api-Key). Si la clave tiene un solo local, el parámetro location puede omitirse; si tiene varios, la respuesta 400 location_required los enumerará.
| Método | Ruta | Permiso | Qué hace |
|---|---|---|---|
| GET | /v1/me |
— | permisos, locales y límites de la clave |
| GET | /v1/locations |
— | locales de la clave y su centralita |
| GET | /v1/calls |
calls:read |
registro de llamadas: since, until, direction, status (answered, missed, ai), number, extension, limit hasta 100, cursor |
| GET | /v1/calls/{id} |
calls:read |
llamada con su ruta: qué regla se activó, qué teléfono sonó, quién respondió |
| GET | /v1/calls/{id}/recording |
recordings:read |
enlace a la grabación durante 10 minutos |
| GET | /v1/calls/{id}/transcript |
recordings:read |
transcripción y resumen de la conversación |
| POST | /v1/dial |
calls:dial |
llamada en un clic: { "extension": "101", "number": "+995555123456" } |
| GET | /v1/dial/{commandId} |
calls:dial |
cómo va la llamada en un clic |
| GET | /v1/extensions |
extensions:read |
extensiones, grupos, desvío, «no molestar» |
| PUT | /v1/extensions/{ext}/forwarding |
extensions:write |
desvío personal de la extensión |
| PUT | /v1/extensions/{ext}/dnd |
extensions:write |
«no molestar»: { "on": true } |
| GET | /v1/presence |
presence:read |
quién está libre ahora mismo |
| GET | /v1/metrics |
reports:read |
métricas del centro de llamadas por periodo: from, to (fechas locales del local YYYY-MM-DD, por defecto los últimos 7 días), compare (prev, year, prev,year o vacío), sections (metrics,loss,queues,lines,operators,directions,heatmap,daily), callbacks=true — además la velocidad de devolución de llamada |
| GET | /v1/routes |
routes:read |
números, reglas de entrada y de salida |
| GET | /v1/routes/history |
routes:read |
historial de cambios de las reglas |
| POST, PUT, DELETE | /v1/routes/inbound…, /v1/routes/outbound… |
routes:write |
reglas de llamadas de una en una: crear, modificar los campos enviados, eliminar, reordenar, revertir |
| GET, POST | /v1/config/export, /v1/config/import |
config:export, config:import |
configuración en formato de archivo; la carga — vista previa y luego aplicación |
| GET | /v1/events |
calls:read |
eventos de llamadas del local de los últimos 7 días (hasta 2000): since (id del evento o momento), types, limit (hasta 500 por respuesta) |
| GET, POST, PATCH, DELETE | /v1/webhooks… |
webhooks:write |
webhooks: listar, crear, modificar, eliminar; …/rotate-secret, …/test, …/deliveries |
| GET, PUT | /v1/open-closed |
routes:read, daynight:write |
si está cerrado y hasta cuándo; cerrar hasta el final del día { "closed": true }, hasta una fecha { "closed": true, "until": "2026-10-05T09:00" } o abrir |
La descripción completa de los campos está en la página de documentación y en OpenAPI.
Quién llamó hoy y no logró comunicarse:
curl -H "Authorization: Bearer cpx_…" \
"https://api.cenaly.com/pbx-api/v1/calls?status=missed&since=$(date -u +%Y-%m-%dT00:00:00Z)"
Devolver la llamada a un cliente desde el teléfono de la extensión 101:
curl -X POST -H "Authorization: Bearer cpx_…" -H "Content-Type: application/json" \
-d '{"extension":"101","number":"+995555123456"}' "https://api.cenaly.com/pbx-api/v1/dial"
Métricas de septiembre con comparación con agosto y con septiembre del año pasado — para su propio panel de BI:
curl -H "Authorization: Bearer cpx_…" \
"https://api.cenaly.com/pbx-api/v1/metrics?from=2026-09-01&to=2026-09-30&compare=prev,year"
En la respuesta, metrics — las mismas cifras que en la pantalla Telefonía → Registro de llamadas → Analítica: entrantes, atendidas y perdidas, nivel de servicio (umbral y objetivo — de la configuración de la cola), espera media y máxima, proporción de abandonadas sin cortas, AHT, FCR, proporción de transferencias y del bot, CSAT. compare.prev y compare.year — los mismos campos del periodo anterior y del mismo periodo de hace un año. La comparación con el año pasado se calcula para periodos de hasta 400 días.
Activar el desvío al móvil si el empleado no responde en 15 segundos:
curl -X PUT -H "Authorization: Bearer cpx_…" -H "Content-Type: application/json" \
-d '{"forward":{"noAnswer":{"kind":"number","number":"+995599000111"},"noAnswerSec":15}}' \
"https://api.cenaly.com/pbx-api/v1/extensions/101/forwarding"
Herramientas MCP#
Las mismas operaciones que en REST. El cliente solo ve las herramientas permitidas por la clave o la conexión:
| Herramientas | Permiso |
|---|---|
list_locations |
— |
list_calls, get_call |
calls:read |
get_call_recording, get_call_transcript |
recordings:read |
call_number, get_dial_status |
calls:dial |
list_extensions |
extensions:read |
set_forwarding, set_do_not_disturb |
extensions:write |
get_presence |
presence:read |
get_call_metrics |
reports:read |
list_routes, list_route_history |
routes:read |
save_inbound_rule, delete_inbound_rule, move_inbound_rule, save_outbound_rule, delete_outbound_rule, move_outbound_rule, set_outbound_calls, restore_route_version |
routes:write |
export_config |
config:export |
import_config |
config:import |
get_call_events |
calls:read |
list_webhooks, save_webhook, delete_webhook, rotate_webhook_secret, test_webhook, list_webhook_deliveries |
webhooks:write |
get_open_closed |
routes:read |
set_open_closed |
daynight:write |
El servidor funciona sin sesiones (streamable HTTP, JSON-RPC 2.0): initialize, tools/list, tools/call.
OAuth para desarrolladores de sus propios conectores#
El servidor de autorización sigue la especificación de autorización MCP: sirve cualquier cliente que la admita:
- Una solicitud a MCP sin token recibe 401 con la cabecera
WWW-Authenticate: Bearer resource_metadata="https://api.cenaly.com/.well-known/oauth-protected-resource/pbx-api/mcp". - Los metadatos del recurso (RFC 9728) indican el servidor de autorización —
https://api.cenaly.com/pbx-api; sus metadatos (RFC 8414) están enhttps://api.cenaly.com/.well-known/oauth-authorization-server/pbx-api(y…/pbx-api/.well-known/openid-configuration). - Registro dinámico del cliente (RFC 7591):
POST /pbx-api/oauth/register. La dirección de retorno es sololocalhost/127.0.0.1(cualquier puerto) o la dirección de un conector conocido (claude.ai, claude.com, chatgpt.com, chat.openai.com, vscode.dev, www.perplexity.ai, enterprise.perplexity.ai; en Cursor, además exactamentecursor://anysphere.cursor-mcp/oauth/callback). El método de acceso del cliente esnone(cliente público),client_secret_postoclient_secret_basic. - Código de autorización con PKCE (solo
S256):GET /pbx-api/oauth/authorize→ ventana de acceso y consentimiento del propietario → retorno a su dirección concode,stateeiss. El parámetroresourcees la dirección del servidor MCP. - Tokens:
POST /pbx-api/oauth/token. El código es de un solo uso y dura 2 minutos (uncode_verifierincorrecto lo anula); el token de acceso dura 1 hora; el token de actualización dura 90 días, en cada actualización se emite uno nuevo y el anterior deja de funcionar. - Revocación:
POST /pbx-api/oauth/revoke(RFC 7009) anula la conexión por completo, igual que «Revocar» en la ventana «API y MCP».
El token de acceso se acepta en cualquier lugar donde se acepte la clave: tanto en MCP como en REST, con los permisos y Local que haya autorizado el propietario.
Límites y errores#
- Por clave o conexión: 120 solicitudes por minuto y 20 000 al día (REST y MCP juntos).
- Llamada en un clic: aparte, 10 por minuto y 300 al día.
- El registro: hasta 100 llamadas por solicitud; en el registro de la central del Local, las últimas 400 llamadas de 90 días.
| Código | Error | Qué hacer |
|---|---|---|
| 401 | api_key_missing, api_key_invalid |
no hay clave, está revocada o caducada; el token OAuth ha caducado: actualícelo |
| 403 | scope_required |
la clave no tiene el permiso: añádalo en «Permisos» |
| 400 | location_required |
la clave tiene varios Local: indique location |
| 404 | location_not_found |
el Local no existe o la clave está limitada a otros Local |
| 429 | rate_limited |
límite agotado, vuelva a intentarlo dentro de Retry-After segundos |
Eventos de llamadas#
En tiempo real — webhooks de la centralita: Telefonía → API y MCP → Webhooks o POST /v1/webhooks con una clave con el permiso webhooks:write. A su dirección https llega en segundos un POST por cada evento: call.started (ha comenzado: dirección, quién, a dónde, línea), call.ringing (a quién le suena — uno por cada teléfono), call.answered (quién ha respondido), call.transferred (quién y a quién ha transferido), call.ended (resultado, duración, conversación, quién respondió). El webhook se puede limitar por locales y tipos de eventos.
{ "id": "1727771234.56:2", "type": "call.answered", "createdAt": "2026-10-01T10:00:05Z",
"locationId": "…", "webhookId": "wh_…", "attempt": 1,
"data": { "callId": "1727771234.56", "seq": 2, "direction": "inbound",
"from": { "number": "+995555123456" }, "party": { "extension": "101", "name": "Анна" } } }
Firma — cabecera X-Cenaly-Signature: t=<tiempo unix>,v1=<HMAC-SHA256 de "t.cuerpo" con el secreto del webhook>. Recalcule, compare en tiempo constante y rechace t con más de 5 minutos. El secreto whsec_… se muestra una sola vez; «Nuevo secreto» anula el anterior de inmediato.
Entrega — responda 2xx en 10 segundos. De lo contrario, reintentamos con pausas de 10 s, 30 s, 2, 5, 10, 15 y 15 minutos (8 intentos, alrededor de una hora); el reintento lleva el mismo id — descarte duplicados por él. El registro de entregas (código de respuesta, hora, intento) está en la ventana del webhook y en GET /v1/webhooks/{id}/deliveries; «Comprobar» envía un webhook.ping de servicio. Si la dirección no ha aceptado ninguna entrega durante 3 días seguidos, el webhook se desactiva solo: al propietario le llega un correo, en la ventana «Webhooks» aparece la marca «Desactivado automáticamente» y el botón «Activar» (o PATCH /v1/webhooks/{id} con { "enabled": true }).
Sin su propio servidor — el mismo flujo por sondeo: GET /v1/events?since=<id del último evento> o la herramienta MCP get_call_events (eventos del local de los últimos 7 días, hasta 2000; por respuesta, hasta 500, more: true — siga pasando páginas con el mismo since).
Tras procesar la grabación — la llamada con transcripción y análisis llega mediante un webhook del origen «Webhook» en la sección «Llamadas».
«Abierto / cerrado» — PUT /v1/open-closed con { "closed": true } cierra el local hasta el final del día (como *28 desde el teléfono: las llamadas se rigen por las reglas de «fuera de horario»), false lo abre; GET muestra si está cerrado y por quién. Con "until" — cerrado hasta ese momento (no más de 30 días), después se abrirá solo: la hora sin desplazamiento (2026-10-05T09:00) se lee en la zona horaria del local; false también lo anula. Se requiere la casilla «Cerrar con el código *28» activada en el enrutamiento (de lo contrario, 409).
Seguridad#
- La clave se muestra una sola vez y en nuestro sistema solo se guarda su huella; una clave filtrada se revoca con un solo botón.
- La conexión OAuth solo la autoriza el propietario, y se ve a qué dirección se concede el acceso; la conexión autorizada se puede limitar por permisos y locales, y revocar en cualquier momento.
- Los números de teléfono son datos personales de sus clientes: conceda permisos sobre el registro y las grabaciones solo a aquellas integraciones que realmente los necesiten.
- Todos los cambios realizados con una clave o conexión quedan en el registro de acciones de la centralita con el nombre de la clave o de la aplicación.