AGI (Asterisk Gateway Interface)
Durante la llamada - Lógica de dialplan externa
- Script ejecutado por el dialplan
- Control síncrono del canal
- Ideal para: IVRs dinámicos, validaciones, lógica de negocio
Control y monitoreo en tiempo real de Asterisk mediante TCP/IP: eventos, acciones, seguridad y casos de uso profesionales
En la Presentación 4 vimos la arquitectura de Asterisk y mencionamos tres interfaces clave para integración externa:
AGI (Asterisk Gateway Interface)
Durante la llamada - Lógica de dialplan externa
AMI (Asterisk Manager Interface)
Fuera de la llamada - Monitoreo y control remoto
ARI (Asterisk REST Interface)
Control total - Aplicaciones de telefonía externas
AMI (Asterisk Manager Interface) es una interfaz cliente-servidor sobre TCP/IP que permite:
✅ Monitorear el estado de Asterisk en tiempo real
✅ Controlar la PBX remotamente (originar llamadas, colgar canales, recargar configuración)
✅ Recibir eventos sobre cambios en el sistema (nuevos canales, hangups, registros)
✅ Ejecutar comandos CLI desde aplicaciones externas
✅ Gestionar colas, extensiones, voicemail, etc.
Comandos enviados desde el cliente hacia Asterisk para realizar operaciones.
Sintaxis:
Action: <NombreAccion>ActionID: <identificador-opcional><Parámetro1>: <valor1><Parámetro2>: <valor2>Ejemplo - Originar una llamada:
Action: OriginateChannel: PJSIP/1001Exten: 9876543210Context: salidas-celularPriority: 1CallerID: "Marketing <1001>"ActionID: call-001Asterisk responde a cada Action con un Response.
Sintaxis:
Response: Success|Error|FollowsActionID: <mismo-id-del-action>Message: <descripción>Ejemplo - Respuesta exitosa:
Response: SuccessActionID: call-001Message: Originate successfully queuedMensajes asíncronos enviados por Asterisk cuando ocurre algo en el sistema.
Sintaxis:
Event: <NombreEvento><Campo1>: <valor1><Campo2>: <valor2>Ejemplo - Nuevo canal creado:
Event: NewchannelPrivilege: call,allChannel: PJSIP/1001-00000001ChannelState: 0ChannelStateDesc: DownCallerIDNum: 1001CallerIDName: Juan PerezUniqueid: 1710876543.1Linkedid: 1710876543.1| Action | Propósito | Caso de Uso |
|---|---|---|
| Originate | Originar llamada | Click-to-call, marcadores automáticos |
| Hangup | Colgar canal | Terminar llamadas problemáticas |
| PJSIPShowEndpoints | Listar endpoints PJSIP | Monitoreo de extensiones |
| QueueStatus | Estado de colas | Dashboard de call center |
| QueueAdd/Remove | Gestionar agentes | Administración dinámica de colas |
| Command | Ejecutar comando CLI | Automatización de tareas |
| Reload | Recargar módulos | Aplicar cambios sin reiniciar |
| CoreShowChannels | Listar canales activos | Monitoreo de llamadas en curso |
| GetVar | Obtener variable de canal | Debugging y análisis |
| Redirect | Transferir llamada | Enrutamiento dinámico |
| Event | Cuándo se Dispara | Uso |
|---|---|---|
| Newchannel | Se crea un nuevo canal | Detectar inicio de llamada |
| Newstate | Cambia el estado del canal | Seguir progreso (Ringing, Up, etc.) |
| Hangup | Canal es colgado | Detectar fin de llamada |
| DialBegin | Inicia marcado | Monitorear intentos de llamada |
| DialEnd | Termina marcado | Ver resultado (ANSWER, BUSY, etc.) |
| Event | Cuándo se Dispara | Uso |
|---|---|---|
| PeerStatus | Endpoint se registra/desregistra | Monitoreo de disponibilidad |
| ContactStatus | Cambia estado de contacto | Ver latencia y calidad |
| ContactStatusDetail | Detalles de contacto | Información completa de registro |
| Event | Cuándo se Dispara | Uso |
|---|---|---|
| QueueMemberAdded | Agente se agrega a cola | Tracking de agentes |
| QueueMemberRemoved | Agente se remueve | Gestión de disponibilidad |
| QueueMemberStatus | Cambia estado de agente | Monitoreo en tiempo real |
| AgentCalled | Agente recibe llamada | Métricas de distribución |
| AgentComplete | Agente termina llamada | Cálculo de tiempos |
; /etc/asterisk/manager.conf
[general]enabled = yes ; Habilitar AMIport = 5038 ; Puerto TCP (estándar)bindaddr = 0.0.0.0 ; Escuchar en todas las interfaces
; === SEGURIDAD ===; Agregar timestamp a eventos (útil para logs)timestampevents = yes
; Tiempo máximo para autenticación (segundos)authtimeout = 30
; Máximo de sesiones sin autenticar simultáneasauthlimit = 50
; === OPTIMIZACIÓN DE RENDIMIENTO ===; Deshabilitar eventos globalmente que no necesites; Esto previene que se generen, ahorrando CPUdisabledevents = Newexten,VarSet
; === USUARIOS AMI ===; Cada aplicación debe tener su propio usuario
[panel_web]secret = Panel2025Secure!; Denegar todo por defectodeny = 0.0.0.0/0.0.0.0; Permitir solo desde localhost y red localpermit = 127.0.0.1/255.255.255.0permit = 192.168.1.0/255.255.255.0
; Permisos de lectura (recibir eventos)read = system,call,log,verbose,agent,user,config,dtmf,reporting,cdr,dialplan
; Permisos de escritura (ejecutar acciones)write = system,call,agent,user,config,command,reporting,originate
[monitor_only]secret = Monitor2025!deny = 0.0.0.0/0.0.0.0permit = 192.168.1.100/255.255.255.255
; Solo lectura - para dashboards de monitoreoread = system,call,agent,reporting,cdrwrite = ; Sin permisos de escritura| Clase | Lectura (read) | Escritura (write) |
|---|---|---|
| system | Eventos del sistema | Shutdown, Restart, Reload |
| call | Eventos de canales | Hangup, Redirect, Atxfer |
| log | Eventos de logging | - |
| verbose | Mensajes verbose | - |
| agent | Eventos de colas/agentes | QueueAdd, QueueRemove |
| user | UserEvent | Enviar UserEvent |
| config | - | Leer/escribir archivos config |
| command | - | Ejecutar comandos CLI |
| dtmf | Eventos DTMF | - |
| reporting | Información del sistema | - |
| cdr | Eventos CDR | - |
| dialplan | Newexten, VarSet | - |
| originate | - | Originar llamadas |
| all | Todos los eventos | Todas las acciones |
En un sistema con 100 extensiones y 50 llamadas simultáneas, AMI puede generar miles de eventos por minuto. Muchos de estos eventos no son relevantes para ninguna aplicación y consumen recursos innecesariamente.
disabledevents (Global - RECOMENDADO)La forma más eficiente de optimizar rendimiento. Desactiva la generación de eventos a nivel global.
disabledevents✅ Previene la generación - El evento nunca se crea
✅ Ahorra CPU - No se ejecuta código de generación
✅ Ahorra memoria - No se serializa ni se mantiene en buffer
✅ Afecta a todos los usuarios - Configuración centralizada
✅ Ideal para eventos que NADIE necesita
manager.conf[general]enabled = yesport = 5038bindaddr = 0.0.0.0
; === OPTIMIZACIÓN CRÍTICA DE RENDIMIENTO ===; Deshabilitar eventos que NINGÚN cliente necesita; Estos eventos se generan miles de veces por segundo en sistemas con tráfico
; Eventos de dialplan (muy frecuentes, raramente útiles); - Newexten: Se dispara en cada paso del dialplan; - VarSet: Se dispara cada vez que se establece una variabledisabledevents = Newexten,VarSet
; Eventos RTP (CRÍTICO en sistemas con muchas llamadas); - RTCPSent/RTCPReceived: Se generan cada 5 segundos por cada canal activo; - En un sistema con 50 llamadas = 600 eventos RTP por minuto; Si NINGÚN cliente AMI necesita estadísticas RTP, deshabilitarlos aquí:;disabledevents = Newexten,VarSet,RTCPSent,RTCPReceivedEscenario: Call center con 100 agentes, 80 llamadas simultáneas promedio
Sin disabledevents:
Newexten: ~500 eventos/minutoVarSet: ~800 eventos/minutoRTCPSent: ~480 eventos/minutoRTCPReceived: ~480 eventos/minutoTOTAL: ~2,260 eventos/minuto generados (pero no usados)Con disabledevents = Newexten,VarSet,RTCPSent,RTCPReceived:
TOTAL: 0 eventos innecesarios generadosReducción de carga CPU: ~15-25%eventfilter (Por Usuario - Granular)Usa esto cuando diferentes usuarios AMI necesitan eventos diferentes. Los eventos SÍ se generan, pero se filtran antes de enviarlos.
eventfilter✅ Granular - Cada usuario tiene filtros diferentes
✅ Flexible - Incluir/excluir por nombre, header, regex
✅ Ideal cuando algunos usuarios SÍ necesitan el evento
⚠️ El evento SÍ se genera - Consume CPU/memoria
⚠️ Solo filtra el envío - No previene la generación
eventfilter(<criterios>) = [expresión]Criterios disponibles:
action(include|exclude) - Incluir o excluirname(<nombre_evento>) - Filtrar por nombre exacto (usa hash, muy eficiente)header(<nombre_header>) - Filtrar por header específicomethod(regex|exact|starts_with|ends_with|contains|none) - Método de comparación1. Dashboard general - Solo eventos de canales PJSIP
[dashboard]secret = Dashboard2025!read = system,call,agent,reportingwrite = originate
; Incluir solo eventos de llamadaseventfilter(action(include),name(Newchannel)) =eventfilter(action(include),name(Hangup)) =eventfilter(action(include),name(DialBegin)) =eventfilter(action(include),name(DialEnd)) =
; Excluir canales Local/ (internos)eventfilter(action(exclude),header(Channel),method(starts_with)) = Local/2. Monitor de cola específica
[queue_monitor_soporte]secret = QueueMon2025!read = agent,callwrite =
; Solo eventos de la cola "soporte"eventfilter(action(include),name(QueueMemberStatus),header(Queue),method(exact)) = soporteeventfilter(action(include),name(AgentCalled),header(Queue),method(exact)) = soporteeventfilter(action(include),name(AgentComplete),header(Queue),method(exact)) = soporte3. Monitor de extensión específica
[extension_monitor_1001]secret = ExtMon2025!read = callwrite =
; Solo eventos del endpoint 1001eventfilter(action(include),name(Newchannel),header(CallerIDNum),method(exact)) = 1001eventfilter(action(include),name(Hangup),header(CallerIDNum),method(exact)) = 1001eventfilter(action(include),name(PeerStatus),header(Peer),method(contains)) = 1001| Criterio | disabledevents | eventfilter |
|---|---|---|
| Rendimiento | ⭐⭐⭐⭐⭐ Máximo | ⭐⭐⭐ Bueno |
| Alcance | Global (todos los usuarios) | Por usuario |
| Generación del evento | ❌ NO se genera | ✅ SÍ se genera |
| Consumo CPU | Mínimo | Moderado (evalúa filtros) |
| Consumo memoria | Mínimo | Moderado (serializa eventos) |
| Flexibilidad | Baja (todo o nada) | Alta (granular) |
| Uso ideal | Eventos que NADIE necesita | Eventos que ALGUNOS necesitan |
[general]enabled = yesport = 5038
; === PASO 1: Deshabilitar eventos que NADIE necesita (máximo rendimiento) ===disabledevents = Newexten,VarSet,RTCPSent,RTCPReceived
; === PASO 2: Usuarios con filtros granulares ===
[dashboard_general]secret = Dashboard2025!read = system,call,agent,reportingwrite = originate; Este usuario recibe todos los eventos (excepto los deshabilitados globalmente)
[monitor_cola_ventas]secret = QueueVentas2025!read = agent,callwrite =; Este usuario solo recibe eventos de la cola "ventas"eventfilter(action(include),name(QueueMemberStatus),header(Queue),method(exact)) = ventaseventfilter(action(include),name(AgentCalled),header(Queue),method(exact)) = ventaseventfilter(action(include),name(AgentComplete),header(Queue),method(exact)) = ventas# Conectar a AMItelnet localhost 5038
# Asterisk responde con banner:# Asterisk Call Manager/9.2.0
# AutenticarseAction: LoginUsername: panel_webSecret: Panel2025Secure!
# Respuesta:# Response: Success# Message: Authentication accepted
# Listar endpointsAction: PJSIPShowEndpoints
# Cerrar sesiónAction: LogoffUn agente de soporte ve un ticket en el CRM y hace clic en el número del cliente. El sistema debe:
import socketimport time
def ami_connect(host, port, username, secret): """Conectar y autenticar en AMI""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port))
# Leer banner banner = sock.recv(1024).decode() print(f"Conectado: {banner}")
# Autenticar login = f"Action: Login\r\nUsername: {username}\r\nSecret: {secret}\r\n\r\n" sock.send(login.encode())
response = sock.recv(1024).decode() if "Success" in response: print("✅ Autenticación exitosa") return sock else: raise Exception("❌ Autenticación fallida")
def originate_call(sock, agent_ext, customer_number): """Originar llamada click-to-call""" action_id = f"call-{int(time.time())}"
action = f"""Action: OriginateChannel: PJSIP/{agent_ext}Exten: {customer_number}Context: salidas-celularPriority: 1CallerID: "Cliente <{customer_number}>"Timeout: 30000ActionID: {action_id}
"""
sock.send(action.encode()) print(f"📞 Llamada iniciada: {agent_ext} → {customer_number}")
# Leer respuesta response = sock.recv(4096).decode() print(f"Respuesta: {response}")
# Usoif __name__ == "__main__": sock = ami_connect("localhost", 5038, "panel_web", "Panel2025Secure!")
# Click-to-call: agente 1001 llama a cliente originate_call(sock, "1001", "987654321")
# Cerrar conexión sock.send(b"Action: Logoff\r\n\r\n") sock.close()El panel web del Taller 9 implementa este patrón:
// Frontend: Consumir eventos AMI vía SSEconst eventSource = new EventSource('/api/ami/events');
eventSource.addEventListener('PeerStatus', (event) => { const data = JSON.parse(event.data);
// Actualizar UI según estado if (data.PeerStatus === 'Reachable') { updateExtensionStatus(data.Peer, 'online', data.Time); } else { updateExtensionStatus(data.Peer, 'offline'); }});
eventSource.addEventListener('Newchannel', (event) => { const data = JSON.parse(event.data); addActiveCall(data.Channel, data.CallerIDNum);});
eventSource.addEventListener('Hangup', (event) => { const data = JSON.parse(event.data); removeActiveCall(data.Channel);});// Backend: Convertir eventos AMI a SSEimport { EventEmitter } from 'events';import { amiManager } from '@/lib/ami-manager';
export async function GET(request: Request) { const stream = new ReadableStream({ start(controller) { const encoder = new TextEncoder();
// Escuchar eventos AMI const handleEvent = (event: any) => { const sseData = `event: ${event.Event}\ndata: ${JSON.stringify(event)}\n\n`; controller.enqueue(encoder.encode(sseData)); };
amiManager.on('PeerStatus', handleEvent); amiManager.on('Newchannel', handleEvent); amiManager.on('Hangup', handleEvent);
// Cleanup al cerrar conexión request.signal.addEventListener('abort', () => { amiManager.off('PeerStatus', handleEvent); amiManager.off('Newchannel', handleEvent); amiManager.off('Hangup', handleEvent); }); } });
return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' } });}✅ Firewall estricto
# Solo permitir AMI desde localhostufw deny 5038ufw allow from 127.0.0.1 to any port 5038
# Si necesitas acceso remoto, usa VPN o SSH tunnelssh -L 5038:localhost:5038 user@asterisk-server✅ Contraseñas fuertes
# Generar contraseña seguraopenssl rand -base64 32✅ Permisos granulares
; Nunca uses esto en producción:; read = all; write = all
; Usa permisos específicos:read = call,agent,reportingwrite = originate✅ Deshabilitar eventos innecesarios
[general]; Prevenir generación de eventos que no usasdisabledevents = Newexten,VarSet,RTCPSent,RTCPReceived✅ Logs de auditoría
[general]; Registrar todas las conexiones AMIdisplayconnects = yes✅ TLS para conexiones remotas
[general]tlsenable = yestlsbindaddr = 0.0.0.0:5039tlscertfile = /etc/asterisk/keys/asterisk.pemtlsprivatekey = /etc/asterisk/keys/asterisk.key# Verificar puerto escuchandonetstat -tlnp | grep 5038
# Verificar en CLIasterisk -rx "manager show settings"asterisk -rx "manager show users"asterisk -rx "manager show connected"# Ver logs en tiempo realtail -f /var/log/asterisk/full | grep -i manager
# Habilitar debug de AMIasterisk -rx "manager set debug on"
# Ver comandos disponiblesasterisk -rx "manager show commands"| Error | Causa | Solución |
|---|---|---|
| Connection refused | AMI no habilitado | enabled = yes en manager.conf |
| Authentication failed | Credenciales incorrectas | Verificar username/secret |
| Permission denied | Permisos insuficientes | Ajustar read/write en usuario |
| No events received | Filtros muy restrictivos | Revisar eventfilter |
| Connection timeout | Firewall bloqueando | Verificar ufw/iptables |
✅ Ideal para:
❌ No usar para:
🚀 Taller 9 - Panel de Administración Web con Next.js
¿Listos para construir un panel profesional? En el próximo taller, implementaremos un dashboard completo con Next.js que usa AMI para monitoreo en tiempo real, gestión de usuarios con roles, y visualización de reportes CDR/CEL.
Verás AMI en acción con: