bitdreamit/laravel-mikopbx

The most complete Laravel package for MikoPBX โ€” CRM-ready call center with auto dialer, campaigns, live agent panel, web softphone, IVR builder, analytics, recordings, blacklist, callbacks & more.

Maintainers

Package info

github.com/bitdreamit/laravel-mikopbx

Language:Blade

pkg:composer/bitdreamit/laravel-mikopbx

Transparency log

Statistics

Installs: 21

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

1.1.9 2026-07-10 13:47 UTC

This package is auto-updated.

Last update: 2026-07-10 17:38:36 UTC


README

The most complete open-source Laravel package for MikoPBX โ€” a full call center CRM platform with a real browser softphone (WebRTC/JsSIP), auto dialer, live agent panel, IVR builder, analytics, recordings, blacklist, callbacks, conference rooms, and system health monitoring.

License: MIT PHP Laravel MikoPBX

Table of Contents

Features

Feature Description
๐Ÿ“ž Live Call Board Real-time active calls with transfer, mute, hangup via AMI
๐Ÿ“ฑ Web Dialer Real browser softphone โ€” WebRTC calling via JsSIP, no desk phone required
๐Ÿ”” Incoming Call Popup Native Answer/Reject alert with ringtone, driven directly by JsSIP's newRTCSession event
๐Ÿ‘ฅ Agent Management Status grid, click-to-call, sync from MikoPBX, DND/away support
๐Ÿ“ข Auto Dialer Create campaigns, upload number lists, voice broadcast, IVR survey
๐ŸŒฟ IVR Builder Visual node editor โ€” Press 1 for Sales, Press 2 for Support
๐Ÿ“Š Analytics Daily trend, peak hours, ASR %, agent performance, Chart.js
๐ŸŽ™๏ธ Recordings Audio player, proxy stream with Bearer auth, download, search by number/date
๐Ÿšซ Blacklist Block inbound/outbound numbers with expiry
๐Ÿ“… Callbacks Schedule, prioritise, attempt, assign to agent
๐ŸŽ™๏ธ Conference Room list, kick/mute participants
โค๏ธ Health Monitor AMI + SIP + REST API status check with 60-second auto-poll
๐Ÿงช Dialer Debug Page Step-by-step diagnostic tool for WebSocket, mic, and SIP registration issues
๐Ÿงช MikoPBXFake Full test double โ€” assertOriginated, assertTransferred etc.

Requirements

Requirement Version Notes
PHP 8.2+ Required for enums
Laravel 11 or 12 Tested on both
Livewire 3.x For real-time components
MikoPBX 2024.2+ REST API v3 and WebRTC must be enabled
A modern browser any Chrome/Firefox/Edge โ€” WebRTC requires HTTPS in production
MySQL / PostgreSQL / SQLite any For local CDR storage

Installation

Step 1 โ€” Install the package

composer require bitdreamit/laravel-mikopbx

Step 2 โ€” Run the installer

php artisan mikopbx:install

This command:

  • Publishes config/mikopbx.php
  • Runs database migrations โ€” 10 mikopbx_* tables plus two new columns on your users table (pbx_extension, pbx_sip_password) used by the web dialer
  • Publishes the self-hosted JsSIP client library to public/vendor/mikopbx/jssip.min.js
  • Writes docs/supervisor-mikopbx-ami.conf
  • Writes .env.mikopbx.example with all required variables

If installing manually instead of via the command:

php artisan vendor:publish --tag=mikopbx-config
php artisan vendor:publish --tag=mikopbx-migrations
php artisan vendor:publish --tag=mikopbx-public
php artisan migrate

Step 3 โ€” Add to .env

Copy .env.mikopbx.example and add the values to your .env file. See Environment Variables below.

Step 4 โ€” Set up MikoPBX admin panel

See MikoPBX Setup below.

Step 5 โ€” Assign extensions to your users

The web dialer needs to know which MikoPBX extension belongs to each logged-in user. This is stored directly on your users table:

// via tinker, a seeder, or your own admin UI
$user = App\Models\User::find(1);
$user->update([
    'pbx_extension'    => '121',              // the plain extension number in MikoPBX
    'pbx_sip_password' => 'sip-password-here', // set in MikoPBX Admin โ†’ Extensions โ†’ edit
]);

Add the trait to your User model for convenience helpers (callNumber(), callLogs(), pendingCallbacks()):

use BitDreamIT\MikoPBX\Traits\HasMikoPBXExtension;

class User extends Authenticatable
{
    use HasMikoPBXExtension;

    protected $fillable = [..., 'pbx_extension', 'pbx_sip_password'];
    protected $hidden   = [..., 'pbx_sip_password'];
}

Step 6 โ€” Start the AMI listener

See Start AMI Listener below.

Step 7 โ€” Sync extensions and open the dashboard

php artisan mikopbx:sync-extensions

Visit https://yourapp.com/pbx in your browser.

MikoPBX Setup

1. Enable AMI User

Go to: MikoPBX Admin Panel โ†’ System โ†’ AMI Users โ†’ Add

Field Value
Username laravelapp
Secret your-strong-ami-secret
Allowed IP Your Laravel server IP
Permissions all (or: call, originate, reporting, system)

Save and Apply Config.

2. Get REST API Key

Go to: MikoPBX Admin Panel โ†’ Settings โ†’ API Keys โ†’ Generate

Copy the JWT token and set it as MIKOPBX_API_KEY in your .env.

The REST API v3 uses Bearer token authentication in the Authorization header. The old X-Auth-Token header is not used in v3.

3. Enable WebRTC for the Web Dialer

This is required for the browser softphone to work at all:

  • MikoPBX Admin โ†’ Network โ†’ WebRTC โ†’ Enable WebRTC (this turns on the /asterisk/ws WebSocket endpoint on port 8088/8089)
  • MikoPBX Admin โ†’ Extensions โ†’ (each extension used by the web dialer) โ†’ enable "Use WebRTC" on that specific extension

MikoPBX registers WebRTC endpoints with a -WS suffix internally (e.g. extension 121 becomes 121-WS for WebRTC registrations). The package handles this automatically โ€” you only ever store the plain number (121) in users.pbx_extension.

4. Optional โ€” Create ARI User

Go to: MikoPBX Admin Panel โ†’ System โ†’ ARI Users โ†’ Add

Needed only if you use the ARIService for WebSocket channel control beyond what the web dialer needs.

Environment Variables

# โ”€โ”€โ”€ MikoPBX REST API v3 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Base URL of your MikoPBX server (no trailing slash)
MIKOPBX_URL=https://163.223.240.124

# JWT Bearer token from MikoPBX Admin โ†’ Settings โ†’ API Keys
MIKOPBX_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# HTTP timeout in seconds (default 10)
MIKOPBX_TIMEOUT=10

# Set false for self-signed SSL certificates (common in local MikoPBX installs)
MIKOPBX_VERIFY_SSL=false

# โ”€โ”€โ”€ AMI (Asterisk Manager Interface) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# AMI is used for: originate, transfer, hangup, mute, live events
# REST API v3 does NOT have call control endpoints
MIKOPBX_AMI_HOST=163.223.240.124
MIKOPBX_AMI_PORT=5038
MIKOPBX_AMI_USER=laravelapp
MIKOPBX_AMI_SECRET=your-strong-ami-secret
MIKOPBX_AMI_TIMEOUT=10

# โ”€โ”€โ”€ ARI (Asterisk REST Interface) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Optional โ€” only needed for ARIService / WebSocket channel control
MIKOPBX_ARI_URL=http://163.223.240.124:8088
MIKOPBX_ARI_USER=admin
MIKOPBX_ARI_PASSWORD=your-ari-password
MIKOPBX_ARI_APP=laravel-mikopbx

# โ”€โ”€โ”€ Web Dialer (SIP.js browser softphone) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
MIKOPBX_DIALER_ENABLED=true
MIKOPBX_SIP_SERVER=pbx.htncr.org
MIKOPBX_SIP_WS_PORT=8089
MIKOPBX_SIP_WSS=true
MIKOPBX_STUN=stun:stun.l.google.com:19302

# โ”€โ”€โ”€ TURN server (REQUIRED for two-way audio behind NAT) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Without a working TURN server, calls will connect but have NO audio on
# either side. STUN alone cannot traverse symmetric NAT. Use a real TURN
# server (e.g. self-hosted coturn) in production โ€” the openrelay defaults
# below are OK for quick testing only.
MIKOPBX_TURN_SERVER=turn:openrelay.metered.ca:80
MIKOPBX_TURN_USERNAME=openrelayproject
MIKOPBX_TURN_PASSWORD=openrelayproject

# โ”€โ”€โ”€ SMS Alerts (optional โ€” for missed call notifications) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
MIKOPBX_SMS_ENABLED=false
MIKOPBX_SMS_DRIVER=ssl_wireless
MIKOPBX_SMS_API_KEY=
MIKOPBX_SMS_FROM=YourSenderID

# โ”€โ”€โ”€ Routing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# URL prefix for all package routes (default: pbx โ†’ /pbx/*)
MIKOPBX_ROUTE_PREFIX=pbx

All config values are documented in config/mikopbx.php.

Web Dialer Setup (WebRTC / JsSIP)

The package includes a full browser-based softphone built on JsSIP โ€” no desk phone or separate SIP client required. It is self-hosted (no CDN dependency) and lives at public/vendor/mikopbx/jssip.min.js.

How it works

Browser (logged in as User with pbx_extension = "121")
      โ”‚
      โ”œโ”€ GET /pbx/dialer/config
      โ”‚     โ†’ returns { sip_uri: "sip:121-WS@pbx.htncr.org",
      โ”‚                  ws_url: "wss://pbx.htncr.org:8089/asterisk/ws", ... }
      โ”‚
      โ”œโ”€ JsSIP.UA registers as sip:121-WS@pbx.htncr.org
      โ”‚     โ†’ green "Softphone Ready" indicator appears in the header
      โ”‚
      โ”œโ”€ Outbound: click any number โ†’ JsSIP sends INVITE directly (WebRTC)
      โ”‚
      โ””โ”€ Inbound: MikoPBX sends INVITE to 121-WS โ†’ JsSIP fires 'newRTCSession'
            โ†’ Answer/Reject popup appears with ringtone

Required per-user setup

Each user who will use the web dialer needs both fields set:

$user->update([
    'pbx_extension'    => '121',
    'pbx_sip_password' => 'the-sip-password-from-mikopbx',
]);

Important: the SIP password is not the user's login password โ€” it is the extension's SIP secret, found in MikoPBX Admin โ†’ Extensions โ†’ edit โ†’ SIP password.

Critical JsSIP configuration detail

The JsSIP.UA must be created with authorization_user set to the plain extension number (not the -WS suffixed one), and must not manually override the contact option:

new JsSIP.UA({
    uri:                'sip:121-WS@pbx.htncr.org',
    authorization_user: '121',           // โœ… plain number, required for auth digest to match
    password:           cfg.password,
    register:           true,
    // Do NOT set `contact:` manually โ€” JsSIP auto-generates a valid one.
    // Overriding it breaks the Request-URI match JsSIP performs internally
    // on every incoming INVITE, causing incoming calls to silently fail
    // with no error and no 'newRTCSession' event, while outbound calls
    // continue to work normally.
});

This is already handled correctly by the package's shipped layouts/app.blade.php โ€” documented here so you don't reintroduce the bug if you customize the dialer.

Diagnosing dialer issues โ€” /pbx/dialer/debug

Visit https://yourapp.com/pbx/dialer/debug for a step-by-step diagnostic tool that checks, in order:

  1. Config API โ€” confirms your user has pbx_extension set and the backend returns valid SIP config
  2. WebSocket connection โ€” tests raw connectivity to wss://.../asterisk/ws (catches firewall/port issues)
  3. JsSIP library loaded โ€” confirms public/vendor/mikopbx/jssip.min.js is reachable
  4. Microphone permission โ€” WebRTC requires HTTPS (except on localhost) or mic access is silently blocked
  5. SIP registration โ€” attempts a real REGISTER and shows the raw SIP response
  6. Test call โ€” places a real WebRTC call to a number you specify, so you can confirm two-way audio end-to-end

Console diagnostics on the live dashboard

From any page under /pbx, open DevTools and run:

window.mikopbxDebugStatus()

This inspects the actual production JsSIP.UA instance (not a separate test one) and prints its live registration state โ€” useful for confirming the softphone is still registered at the exact moment a call comes in.

Start AMI Listener (Supervisor)

The AMI listener is the daemon that connects to MikoPBX port 5038, receives real-time events (incoming calls, hangups, agent status changes), and dispatches Laravel events for the UI.

# Copy the config generated by mikopbx:install
sudo cp docs/supervisor-mikopbx-ami.conf /etc/supervisor/conf.d/

# Load and start
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start mikopbx-ami

# Check status
sudo supervisorctl status mikopbx-ami
# Should show: mikopbx-ami   RUNNING   pid 12345, uptime 0:01:00

# View logs
tail -f storage/logs/mikopbx-ami.log

The supervisor config runs:

php artisan mikopbx:listen

This command connects to AMI, subscribes to all events, and loops forever. It auto-restarts on crash.

Pages & Routes

All routes are under the configurable prefix (default /pbx).

URL Route Name Description
/pbx mikopbx.dashboard Dashboard โ€” live calls, task manager, campaigns, follow-up list
/pbx/calls mikopbx.calls.index CDR table with real-time filters (Livewire)
/pbx/calls/{id} mikopbx.calls.show Single call detail + recording player + actions
/pbx/campaigns mikopbx.campaigns.index Campaign cards grid
/pbx/campaigns/create mikopbx.campaigns.create Create campaign with number upload
/pbx/campaigns/{id} mikopbx.campaigns.show Live campaign detail with number list
/pbx/agents mikopbx.agents.index Agent table with status change and click-to-call
/pbx/analytics mikopbx.analytics.index Chart.js analytics dashboard
/pbx/recordings mikopbx.recordings.index Recordings with sticky audio player
/pbx/blacklist mikopbx.blacklist.index Blacklist manager
/pbx/callbacks mikopbx.callbacks.index Callback scheduler
/pbx/conference mikopbx.conference.index Conference rooms
/pbx/ivr/builder mikopbx.ivr.builder Visual IVR builder
/pbx/health mikopbx.health.index System health monitor
/pbx/dialer/debug mikopbx.dialer.debug Web dialer diagnostic tool

Internal API / AJAX routes (used by the frontend)

Method URL Description
GET /pbx/calls/active/json Active calls list, polled by the dashboard header
POST /pbx/calls/originate AMI fallback originate (used when WebRTC isn't registered)
POST /pbx/calls/transfer Transfer call (AMI)
POST /pbx/calls/hangup Hangup call (AMI)
POST /pbx/calls/mute Mute/unmute (AMI)
GET /pbx/agents/statuses Agent status list, polled by the header
POST /pbx/agents/status Manual status change (from the Agents page dropdown)
POST /pbx/agents/web-dialer-status Reports browser softphone online/busy/offline โ€” see Live Agent Online Status
POST /pbx/agents/sync Pull extensions from MikoPBX
GET /pbx/dialer/config SIP/JsSIP config for the current user's browser softphone

Webhook route (no auth)

URL Description
POST /mikopbx-webhook/call Receive call events pushed from MikoPBX (closures, secured by header secret)

Facade Usage

use BitDreamIT\MikoPBX\Facades\MikoPBX;

// โ”€โ”€ Active calls (REST API v3) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
$response = MikoPBX::api()->getActiveCalls();
// $response = ['result' => true, 'data' => [...active calls...]]

// โ”€โ”€ Call control (AMI โ€” REST v3 has no call control endpoints) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
MikoPBX::originate('101', '01711000000');         // Extension 101 โ†’ customer
MikoPBX::transfer('PJSIP/101-00000001', '102');   // Transfer to ext 102
MikoPBX::hangup('PJSIP/101-00000001');            // End the call

// Or via ami() service directly:
MikoPBX::ami()->connect();
MikoPBX::ami()->originate('101', '01711000000');
MikoPBX::ami()->disconnect();

// โ”€โ”€ CDR records (REST API v3) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
$response = MikoPBX::api()->getCDR('2026-06-01 00:00:00', '2026-06-30 23:59:59', [
    'src_num'     => '01711000000',  // filter by caller
    'limit'       => 50,
    'offset'      => 0,
]);
// See "CDR Field Names & Nested Structure" below โ€” the real response is
// nested by call (linkedid), not a flat list of records.

// โ”€โ”€ Extensions (REST API v3) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
$exts = MikoPBX::api()->getExtensions(); // GET /v3/extensions:getForSelect
// Returns: [{"value": "101", "text": "101 John Smith"}, ...]

$statuses = MikoPBX::api()->getExtensionStatuses(); // GET /v3/sip:getPeersStatuses
// Each item: { id: "101", state: "OK|REGISTERED|UNREACHABLE|...", ipaddress, port }

// โ”€โ”€ SIP trunk status โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
$trunks = MikoPBX::api()->getTrunkStatus(); // GET /v3/sip-providers:getStatuses
$isUp   = collect($trunks['data'] ?? [])->contains(fn($t) => $t['state'] === 'REGISTERED');

// โ”€โ”€ Campaigns โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
$campaign = MikoPBX::campaign()->create(
    ['name' => 'June Promo', 'type' => 'voice_broadcast', 'max_channels' => 5],
    ['01711000001', '01711000002', '01711000003']
);
MikoPBX::campaign()->start($campaign);
MikoPBX::campaign()->pause($campaign);
MikoPBX::campaign()->stop($campaign);

// โ”€โ”€ Agents โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
$agents = MikoPBX::agent()->all();          // With live SIP status merged
$count  = MikoPBX::agent()->sync();         // Pull from MikoPBX โ†’ local DB

// โ”€โ”€ Blacklist โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
MikoPBX::blacklist()->add('01711999999', 'Spam caller', 'both');
$blocked = MikoPBX::blacklist()->isBlocked('01711999999');
MikoPBX::blacklist()->remove('01711999999');

// โ”€โ”€ Callbacks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
MikoPBX::callback()->schedule('01711000000', [
    'name'     => 'Customer Name',
    'priority' => 'urgent',        // low | normal | high | urgent
    'note'     => 'Called about order #1234',
]);

// โ”€โ”€ Analytics โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
$summary = MikoPBX::analytics()->summary('2026-06-01', '2026-06-30');
// Returns: total_calls, answered, missed, failed, asr%, avg_duration, inbound, outbound

// โ”€โ”€ Health โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
$health = MikoPBX::health()->check();
// Returns: ['status' => 'healthy', 'amiOk' => true, 'ariOk' => true, 'sipOk' => true, 'calls' => 3]

// โ”€โ”€ IVR โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
$menus = MikoPBX::api()->getIVRMenus();  // GET /v3/ivr-menu
MikoPBX::api()->saveIVRMenu(['name' => 'Main Menu', 'nodes' => [...]]);

// โ”€โ”€ Sound files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
MikoPBX::api()->uploadAudio('/path/to/greeting.wav');  // POST /v3/sound-files:uploadFile

// โ”€โ”€ Using the trait on your User model โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
$user->callNumber('01711000000');      // originate from this user's own extension
$user->callLogs();                     // this user's call history (requires cdr-sync)
$user->pendingCallbacks();             // callbacks assigned to this user's extension

Livewire Components

All components can be used standalone in any Blade view:

{{-- Live call board with transfer/hangup controls (polls every 5s + Echo) --}}
@livewire('mikopbx-live-call-board')

{{-- Agent status dots with click-to-call (polls every 10s + Echo) --}}
@livewire('mikopbx-agent-status-grid')

{{-- Campaign manager with start/pause/stop (polls every 8s) --}}
@livewire('mikopbx-campaign-manager')

{{-- CDR table with live search and filter (updates on Echo events) --}}
@livewire('mikopbx-call-log-table')

{{-- Blacklist add/remove (Livewire) --}}
@livewire('mikopbx-blacklist-manager')

{{-- Pending callbacks with attempt/cancel --}}
@livewire('mikopbx-pending-callbacks')

{{-- Visual IVR node builder --}}
@livewire('mikopbx-ivr-builder')

{{-- Analytics charts dashboard with date filter --}}
@livewire('mikopbx-analytics-dash')

{{-- Health monitor โ€” polls every 60s --}}
@livewire('mikopbx-health-monitor')

Note on incoming calls: the Answer/Reject popup is not a Livewire component. It's handled natively by Alpine.js directly in layouts/app.blade.php, driven by JsSIP's own newRTCSession event โ€” this avoids the latency of a server round-trip for something as time-sensitive as a ringing call. If you're looking for IncomingCallPopup.php, it exists in src/Livewire/ but is unused by the default layout; it's kept for anyone who prefers a server-driven popup instead.

Artisan Commands

Command Description
php artisan mikopbx:install Full setup wizard (publish config, run migrations, publish JsSIP, write Supervisor config)
php artisan mikopbx:listen AMI daemon โ€” run via Supervisor in production
php artisan mikopbx:cdr-sync --days=1 Pull CDR from MikoPBX REST API v3 and store locally
php artisan mikopbx:sync-extensions Pull extensions from MikoPBX and upsert local DB
php artisan mikopbx:campaign-run Start campaigns that are scheduled and due
php artisan mikopbx:campaign-run --sync Sync progress of all running campaigns
php artisan mikopbx:health Run health check (exit code 1 on critical)

Scheduler (optional)

Add to your routes/console.php or App\Console\Kernel:

Schedule::command('mikopbx:cdr-sync')->hourly();
Schedule::command('mikopbx:campaign-run --sync')->everyFiveMinutes();
Schedule::command('mikopbx:health')->everyFiveMinutes();

REST API v3 Endpoints Reference

Authentication: All requests need Authorization: Bearer {API_KEY} header.

Response envelope (every endpoint):

{
  "result": true,
  "data": [...],
  "messages": { "error": [], "info": [], "warning": [] },
  "function": "...",
  "processor": "...",
  "pid": 12345
}

CDR โ€” Call Records

Method Endpoint Description
GET /pbxcore/api/v3/cdr List CDR with filters
GET /pbxcore/api/v3/cdr/{id} Single CDR record
DELETE /pbxcore/api/v3/cdr/{id} Delete CDR record
GET /pbxcore/api/v3/cdr:playback Stream recording audio (token-based URL)
GET /pbxcore/api/v3/cdr:download Download recording file (token-based URL)
GET /pbxcore/api/v3/cdr:getMetadata CDR column metadata
GET /pbxcore/api/v3/cdr:getStatsByProvider Stats by SIP provider

GET /pbxcore/api/v3/cdr query parameters:

Parameter Required Description
limit No Max records (default 20, max 100)
offset No Skip N records for pagination
dateFrom No Start date: 2026-06-01 00:00:00
dateTo No End date: 2026-06-30 23:59:59
src_num No Filter by caller number
dst_num No Filter by called number

PBX Status

Method Endpoint Description
GET /pbxcore/api/v3/pbx-status:getActiveCalls Active calls right now (grouped display data, no channel names โ€” see below)
GET /pbxcore/api/v3/pbx-status:getActiveChannels Active Asterisk channels (has real channel names, needed for AMI hangup/transfer)

getActiveCalls() alone does not return a usable channel name field for AMI actions. The package's LiveCallBoard component cross-references it against getActiveChannels() to resolve the real channel string (e.g. PJSIP/121-00000010) before allowing Transfer/Hangup.

Extensions & Employees

Method Endpoint Description
GET /pbxcore/api/v3/extensions:getForSelect Extensions as {value, text}
GET /pbxcore/api/v3/extensions Full extension list
GET /pbxcore/api/v3/extensions/{id} Single extension
GET /pbxcore/api/v3/employees All employees
POST /pbxcore/api/v3/employees Create employee
PUT /pbxcore/api/v3/employees/{id} Update employee

MikoPBX sometimes embeds HTML (Semantic UI icon tags) inside the text field of getForSelect responses โ€” the package strips these with strip_tags() before storing display names.

SIP Peer Status

Method Endpoint Description
GET /pbxcore/api/v3/sip:getPeersStatuses All SIP peers with state
GET /pbxcore/api/v3/sip:getRegistry SIP registration status
GET /pbxcore/api/v3/sip/{id}:getStatus Single peer status
GET /pbxcore/api/v3/sip/{id}:getStats Peer call statistics

SIP peer state values: OK | REGISTERED | UNREACHABLE | LAGGED | UNKNOWN | OFF

SIP Providers (trunks)

Method Endpoint Description
GET /pbxcore/api/v3/sip-providers:getStatuses All trunk registration states
GET /pbxcore/api/v3/sip-providers Full trunk list
GET /pbxcore/api/v3/sip-providers/{id}:getStatus Single trunk status
POST /pbxcore/api/v3/sip-providers/{id}:forceCheck Force re-registration

IVR Menu

Method Endpoint Description
GET /pbxcore/api/v3/ivr-menu All IVR menus
POST /pbxcore/api/v3/ivr-menu Create IVR menu
PUT /pbxcore/api/v3/ivr-menu/{id} Update IVR menu
DELETE /pbxcore/api/v3/ivr-menu/{id} Delete IVR menu

Conference Rooms

Method Endpoint Description
GET /pbxcore/api/v3/conference-rooms All conference rooms
POST /pbxcore/api/v3/conference-rooms Create room

Sound Files

Method Endpoint Description
GET /pbxcore/api/v3/sound-files:getForSelect As {value, text} list
POST /pbxcore/api/v3/sound-files:uploadFile Upload audio (multipart)

System

Method Endpoint Description
GET /pbxcore/api/v3/sysinfo:getInfo System info, disk, CPU, version
GET /pbxcore/api/v3/system:ping Health ping (no auth needed)
GET /pbxcore/api/v3/system:checkAuth Verify API key is valid

How Call Control Works (AMI, not REST)

Critical: MikoPBX REST API v3 has no originate, transfer, hangup, or mute endpoints. All call control goes through AMI (Asterisk Manager Interface) on TCP port 5038.

Your Laravel App
      โ”‚
      โ”œโ”€ GET /v3/pbx-status:getActiveCalls  โ”€โ”€โ†’  MikoPBX REST API (port 443)
      โ”œโ”€ GET /v3/cdr                        โ”€โ”€โ†’  MikoPBX REST API (port 443)
      โ”œโ”€ GET /v3/extensions:getForSelect    โ”€โ”€โ†’  MikoPBX REST API (port 443)
      โ”‚
      โ””โ”€ Action: Originate  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’  MikoPBX AMI (TCP port 5038)
         Action: Redirect (transfer)       โ”€โ”€โ†’  MikoPBX AMI (TCP port 5038)
         Action: Hangup                    โ”€โ”€โ†’  MikoPBX AMI (TCP port 5038)
         Action: MuteAudio                 โ”€โ”€โ†’  MikoPBX AMI (TCP port 5038)
         Event: Newchannel (listen)        โ†โ”€โ”€  MikoPBX AMI (TCP port 5038)
         Event: Hangup (listen)            โ†โ”€โ”€  MikoPBX AMI (TCP port 5038)
         Event: PeerStatus (listen)        โ†โ”€โ”€  MikoPBX AMI (TCP port 5038)

The web dialer (browser softphone) is a third path entirely โ€” it doesn't go through Laravel at all for call setup. JsSIP talks directly to MikoPBX over the WebRTC WebSocket, and Laravel is only used to hand out SIP credentials (/pbx/dialer/config) and to report status changes (/pbx/agents/web-dialer-status).

AMI Event โ†’ Laravel Event

The mikopbx:listen daemon receives these AMI events and fires Laravel Events:

AMI Event Trigger Laravel Event Echo Channel
Newchannel Phone rings IncomingCallEvent mikopbx.calls .incoming
Bridge Call answered CallAnsweredEvent mikopbx.calls .answered
Hangup Call ended CallEndedEvent mikopbx.calls .ended
PeerStatus Agent login/out AgentStatusChangedEvent mikopbx.agents .status

CDR Field Names & Nested Structure

Important: MikoPBX REST API v3's CDR response is nested by call, not a flat list โ€” this trips up most first attempts at integration.

Real response shape from GET /pbxcore/api/v3/cdr:

{
  "result": true,
  "data": {
    "records": [
      {
        "linkedid": "mikopbx-1782484638.4",
        "start": "2026-06-26 20:37:18.896",
        "src_num": "121",
        "dst_num": "+8801774314856",
        "disposition": "ANSWERED",
        "totalDuration": 338,
        "totalBillsec": 314,
        "records": [
          {
            "id": 665,
            "UNIQUEID": "mikopbx-1782484638.4_Vo6697",
            "src_chan": "PJSIP/121-00000004",
            "dst_chan": "PJSIP/SIP-TRUNK-...-00000005",
            "disposition": "ANSWERED",
            "duration": 338,
            "billsec": 314,
            "recordingfile": "/storage/.../mikopbx-....webm",
            "playback_url": "/pbxcore/api/v3/cdr:playback?token=ac95731c...",
            "download_url": "/pbxcore/api/v3/cdr:download?token=ac95731c..."
          }
        ]
      }
    ],
    "pagination": { "total": 359, "limit": 100, "offset": 0, "hasMore": true, "lastId": 563 }
  }
}

Each top-level item in data.records[] is one call (grouped by linkedid); the actual per-channel-leg detail โ€” including UNIQUEID, playback_url, and recordingfile โ€” is nested one level deeper inside that item's own records[] array. php artisan mikopbx:cdr-sync flattens this correctly and merges group-level fields (src_num, dst_num) into each inner record before saving.

Our DB Column MikoPBX v3 Field Notes
caller src_num Caller phone number
callee dst_num Called phone number
uniqueid UNIQUEID Capital letters, and one level deeper than the group
channel src_chan e.g. PJSIP/121-00000004
started_at start Has microseconds (20:37:18.896) โ€” stripped before saving to MySQL
answered_at answer Empty string if not answered
ended_at endtime Call end time
status disposition ANSWERED / NOANSWER (no space) / BUSY / FAILED
duration duration Total seconds (inner record) or totalDuration (group level)
billsec billsec Answered seconds (inner record) or totalBillsec (group level)
recording_file recordingfile Full server path โ€” package stores just the basename
recording_url playback_url Relative, token-based URL โ€” proxied through Laravel with Bearer auth so the browser never needs the API key

Pagination uses data.pagination.hasMore (not a simple record-count comparison) to know when to fetch the next page.

Live Agent Online Status

The Live Call Board and CDR sync (server-side, via REST API) and the web dialer's own registration state (client-side, via JsSIP in the browser) are two entirely separate systems. A browser can be fully registered and able to make/receive WebRTC calls while MikoPBX's own sip:getPeersStatuses endpoint doesn't cleanly reflect that -WS registration โ€” which would otherwise make agents appear offline in the Agent Status Grid even while actively on a call.

To fix this, the web dialer actively reports its own state to the server:

JsSIP UA registers          โ†’ POST /pbx/agents/web-dialer-status {status: "online"}
Call starts (in or out)     โ†’ POST /pbx/agents/web-dialer-status {status: "busy"}
Call ends                   โ†’ POST /pbx/agents/web-dialer-status {status: "online"}
Tab closed (beforeunload)   โ†’ sendBeacon โ†’ {status: "offline"}
Every 60s while registered  โ†’ heartbeat re-reports current state

AgentService::all() trusts a browser-reported status for 90 seconds before letting the next AMI/REST poll overwrite it โ€” this prevents the status flickering between "online" and "offline" on every 10-second poll cycle just because MikoPBX's peer-status endpoint doesn't recognize the WebRTC contact the same way it does a desk phone.

Testing with MikoPBXFake

MikoPBXFake replaces the MikoPBX service container binding. No real API or AMI calls during tests.

use BitDreamIT\MikoPBX\Testing\MikoPBXFake;

class CallTest extends TestCase
{
    private MikoPBXFake $fake;

    protected function setUp(): void
    {
        parent::setUp();
        $this->fake = MikoPBXFake::make($this->app);
    }

    protected function tearDown(): void
    {
        $this->fake->reset();
        parent::tearDown();
    }

    public function test_placing_order_triggers_call(): void
    {
        $this->post('/orders', ['phone' => '01711000000']);

        $this->fake->assertOriginated('101', '01711000000');
    }

    public function test_no_call_when_blacklisted(): void
    {
        Blacklist::create(['number' => '01711999999', 'direction' => 'both']);

        $this->post('/orders', ['phone' => '01711999999']);

        $this->fake->assertNothingOriginated();
    }
}

Available assertions

Method Description
assertOriginated($from, $to) Assert a call was originated
assertNotOriginated($from, $to) Assert call was NOT made
assertOriginateCount($n) Assert exactly N originate calls
assertNothingOriginated() Assert zero calls were originated
assertTransferred($channel, $to) Assert a transfer was performed
assertHungUp($channel) Assert a channel was hung up
assertCampaignStarted() Assert a campaign was started
failOnNextCall() Make next originate throw exception
reset() Clear all recorded calls between tests

Database Tables

All tables use the prefix mikopbx_ (configurable in config/mikopbx.php), plus two new columns added directly to your existing users table.

Table / Column Description
mikopbx_extensions Agents / SIP extensions synced from MikoPBX
mikopbx_call_logs CDR โ€” local copy of call records
mikopbx_campaigns Auto dialer campaigns
mikopbx_campaign_numbers Numbers in each campaign with per-number status
mikopbx_blacklist Blocked numbers with direction and expiry
mikopbx_callbacks Scheduled callback tasks
mikopbx_ivr_trees IVR node definitions
mikopbx_conference_rooms Conference room config
mikopbx_agent_status_log Agent status change history
mikopbx_health_logs Health check results over time
users.pbx_extension The MikoPBX extension number for this user's web dialer
users.pbx_sip_password The SIP password for that extension (hidden from JSON output)

Real-time Events (Laravel Echo)

Set up Laravel Echo in your resources/js/bootstrap.js:

import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Echo = new Echo({
    broadcaster: 'reverb',        // or 'pusher'
    key: import.meta.env.VITE_REVERB_APP_KEY,
    wsHost: import.meta.env.VITE_REVERB_HOST,
    wsPort: import.meta.env.VITE_REVERB_PORT,
    wssPort: import.meta.env.VITE_REVERB_PORT,
    forceTLS: false,
    enabledTransports: ['ws', 'wss'],
});

If Echo is not configured, you will see Laravel Echo cannot be found logged by Livewire in the browser console โ€” this is harmless. Livewire.on() listeners (used for the ringtone and toast events) work over Livewire's own local event bus and do not require Echo/broadcasting to function.

Channel Event Trigger
mikopbx.calls .incoming Incoming call detected via AMI (server-side CDR/board updates)
mikopbx.calls .answered Call was answered
mikopbx.calls .ended Call ended/missed
mikopbx.agents .status Agent went online/offline/busy

Note: the browser's own incoming-call popup does not depend on these Echo events โ€” it's driven directly by JsSIP's newRTCSession event for near-instant response. Echo/AMI events update the server-side dashboard widgets (Live Call Board, Agent Status Grid) in parallel.

Package Structure

bitdreamit/laravel-mikopbx/
โ”œโ”€โ”€ composer.json
โ”œโ”€โ”€ README.md
โ”œโ”€โ”€ CHANGELOG.md
โ”œโ”€โ”€ config/
โ”‚   โ””โ”€โ”€ mikopbx.php                    All configuration keys
โ”œโ”€โ”€ database/
โ”‚   โ””โ”€โ”€ migrations/
โ”‚       โ”œโ”€โ”€ ..._create_mikopbx_tables.php       All 10 mikopbx_* tables
โ”‚       โ””โ”€โ”€ ..._add_pbx_fields_to_users_table.php  pbx_extension + pbx_sip_password
โ”œโ”€โ”€ public/
โ”‚   โ””โ”€โ”€ vendor/mikopbx/
โ”‚       โ””โ”€โ”€ jssip.min.js               Self-hosted JsSIP client (no CDN dependency)
โ”œโ”€โ”€ routes/
โ”‚   โ”œโ”€โ”€ web.php                        Named routes /pbx/*
โ”‚   โ”œโ”€โ”€ api.php                        JSON API routes /api/pbx/*
โ”‚   โ””โ”€โ”€ webhook.php                    /mikopbx-webhook/* (no auth)
โ”œโ”€โ”€ resources/
โ”‚   โ”œโ”€โ”€ views/mikopbx/
โ”‚   โ”‚   โ”œโ”€โ”€ layouts/app.blade.php      Master layout: sidebar, web dialer, incoming-call
โ”‚   โ”‚   โ”‚                              popup, header status pill โ€” all Alpine + JsSIP
โ”‚   โ”‚   โ”œโ”€โ”€ dashboard/index.blade.php  Dashboard with task manager & follow-up list
โ”‚   โ”‚   โ”œโ”€โ”€ calls/                     index.blade.php, show.blade.php
โ”‚   โ”‚   โ”œโ”€โ”€ campaigns/                 index, create, show
โ”‚   โ”‚   โ”œโ”€โ”€ agents/index.blade.php
โ”‚   โ”‚   โ”œโ”€โ”€ analytics/index.blade.php
โ”‚   โ”‚   โ”œโ”€โ”€ recordings/index.blade.php
โ”‚   โ”‚   โ”œโ”€โ”€ blacklist/index.blade.php
โ”‚   โ”‚   โ”œโ”€โ”€ callbacks/index.blade.php
โ”‚   โ”‚   โ”œโ”€โ”€ conference/index.blade.php
โ”‚   โ”‚   โ”œโ”€โ”€ ivr/                       index, builder
โ”‚   โ”‚   โ”œโ”€โ”€ health/index.blade.php
โ”‚   โ”‚   โ”œโ”€โ”€ partials/dialer-debug.blade.php   Web dialer diagnostic page
โ”‚   โ”‚   โ””โ”€โ”€ livewire/                  Livewire blade views
โ”‚   โ”œโ”€โ”€ js/mikopbx/
โ”‚   โ”‚   โ”œโ”€โ”€ app.js                     Optional standalone entry (layout is self-contained)
โ”‚   โ”‚   โ”œโ”€โ”€ echo-listeners.js          Reverb/Pusher channel subscriptions
โ”‚   โ”‚   โ””โ”€โ”€ click-to-call.js           Alpine.js component + [data-pbx-call] wiring
โ”‚   โ””โ”€โ”€ css/
โ”‚       โ””โ”€โ”€ mikopbx.css                Animations, waveform, status dots
โ””โ”€โ”€ src/
    โ”œโ”€โ”€ MikoPBXServiceProvider.php     Package bootstrap
    โ”œโ”€โ”€ MikoPBXManager.php             Facade target (14 services)
    โ”œโ”€โ”€ Facades/MikoPBX.php
    โ”œโ”€โ”€ Services/
    โ”‚   โ”œโ”€โ”€ RestApiService.php         MikoPBX REST API v3 (correct v3 endpoints)
    โ”‚   โ”œโ”€โ”€ AMIService.php             TCP socket AMI โ€” call control & events
    โ”‚   โ”œโ”€โ”€ ARIService.php             ARI REST + WebSocket URL
    โ”‚   โ”œโ”€โ”€ CampaignService.php        Campaign CRUD + start/pause/stop
    โ”‚   โ”œโ”€โ”€ AgentService.php           Agent list + sync + status (browser-trust window)
    โ”‚   โ”œโ”€โ”€ AnalyticsService.php       Summary, trend, peak hours, agents
    โ”‚   โ”œโ”€โ”€ BlacklistService.php       Add/remove/check blacklist
    โ”‚   โ”œโ”€โ”€ CallbackService.php        Schedule + attempt callbacks
    โ”‚   โ”œโ”€โ”€ RecordingService.php       List + proxy stream recordings (Bearer auth)
    โ”‚   โ”œโ”€โ”€ ConferenceService.php      Rooms + kick/mute
    โ”‚   โ”œโ”€โ”€ IVRService.php             IVR menu CRUD
    โ”‚   โ”œโ”€โ”€ HealthCheckService.php     AMI + SIP + REST health check
    โ”‚   โ”œโ”€โ”€ SmsService.php             SSL Wireless + Twilio SMS
    โ”‚   โ””โ”€โ”€ WebDialerService.php       JsSIP config builder for the browser softphone
    โ”œโ”€โ”€ Models/                        7 models (all with dynamic getTable())
    โ”œโ”€โ”€ Http/Controllers/              12 controllers, incl. WebDialerController
    โ”œโ”€โ”€ Livewire/                      10 Livewire v3 components
    โ”œโ”€โ”€ Commands/                      6 Artisan commands
    โ”œโ”€โ”€ Events/                        4 ShouldBroadcast events
    โ”œโ”€โ”€ Jobs/ProcessCallbackJob.php
    โ”œโ”€โ”€ Listeners/MissedCallListener.php
    โ”œโ”€โ”€ Enums/                         CallStatus, CampaignStatus, AgentStatus
    โ”œโ”€โ”€ Traits/HasMikoPBXExtension.php Add to User model
    โ”œโ”€โ”€ Exceptions/MikoPBXException.php
    โ””โ”€โ”€ Testing/MikoPBXFake.php        Full test double

Anyone installs it

composer require bitdreamit/laravel-mikopbx
php artisan mikopbx:install

Troubleshooting

Outbound call connects but has NO audio on either side

This is the single most common WebRTC failure mode and almost always points to one cause: missing or unreachable TURN server.

Symptom: you click Call, the other side picks up, the call timer is counting, but neither party can hear the other. The SIP signaling completed (the call is "connected" because the INVITE/200 OK/ACK handshake travels through the WebSocket), but the RTP media path was never established because ICE could not traverse NAT using STUN alone.

Diagnostics (in this order):

  1. Open browser DevTools โ†’ Console. Look for [ICE] iceConnectionState: lines.
    • connected or completed = good.
    • failed = ICE failed โ†’ TURN issue.
    • If you never see this log line at all, the layout is stale โ€” republish views (php artisan vendor:publish --tag=mikopbx-views --force).
  2. Look for [ICE] โš  No TURN server configured warning. If present, no TURN env vars are set.
  3. Look at [MikoPBX] ICE servers in use: โ€” confirm it includes a turn: entry.

Fix:

# .env  โ€” use a real TURN server. The defaults point at the public
# openrelay.metered.ca service which is OK for testing but unreliable
# for production. Run your own coturn instance for production.
MIKOPBX_TURN_SERVER=turn:your-turn-host.example.com:3478?transport=udp
MIKOPBX_TURN_USERNAME=youruser
MIKOPBX_TURN_PASSWORD=yourpass

Also confirm:

Other causes (less common):

  • Browser autoplay policy blocked audio.play() โ€” a red "๐Ÿ”‡ Browser blocked call audio" banner now appears at the bottom of the page; click Enable Audio to resume.
  • HTTPS not enforced โ€” getUserMedia is blocked on http:// (except localhost), so the local mic track is never captured. Force HTTPS.
  • MikoPBX WebRTC endpoint codec mismatch โ€” MikoPBX's PJSIP WebRTC module defaults to Opus; ensure the browser also offers Opus (Chrome/Firefox/Edge all do).

Incoming calls don't ring or show a popup, but outbound calls work fine

Step 1 โ€” Confirm JsSIP actually received the INVITE.

Open DevTools โ†’ Console on a /pbx page (any page that uses the softphone layout). Place a real inbound call to the user's extension. You should see:

๐Ÿ“ž [MikoPBX] newRTCSession EVENT!
  • If you DO see this log but the popup doesn't appear โ†’ it's a code bug; report it. (The fixed layout in this version handles this case correctly, so a republish should resolve it: php artisan vendor:publish --tag=mikopbx-views --force.)

  • If you do NOT see this log โ†’ JsSIP never received the INVITE. The problem is on the MikoPBX side, not in this code:

    1. MikoPBX Admin โ†’ Extensions โ†’ โ†’ "Use WebRTC" toggle is OFF. Turn it ON, save, then refresh the browser page so JsSIP re-registers.
    2. The same extension is also registered on a desk phone (Zoiper, Linphone, hardware SIP phone). MikoPBX may deliver the call to the desk-phone contact only and never fork it to the -WS WebRTC contact. Either unbind the desk phone, or configure MikoPBX to ring all contacts simultaneously (MikoPBX Admin โ†’ Extensions โ†’ "Call settings" โ†’ "Ring simultaneously").
    3. The inbound route / IVR in MikoPBX points to a single extension number, not a ring-group that includes the -WS variant. Edit the inbound route to ring the extension (which MikoPBX will fork to all its registered contacts).
    4. The WebSocket (wss://...:8089/asterisk/ws) dropped silently. Check the "Softphone Ready" pill in the header โ€” if it's red, refresh the page. If it stays red, see Web dialer shows "Softphone Offline" below.

Step 2 โ€” Confirm authorization_user is the plain extension.

The JsSIP.UA config must use the plain extension number (e.g. "121") for authorization_user, and the SIP URI uses the -WS suffix (sip:121-WS@pbx.htncr.org). The shipped layout does this correctly โ€” do not override contact: manually, or every incoming INVITE will be silently rejected.

Step 3 โ€” Confirm ringtone is not autoplay-blocked.

If the popup appears but you hear no ringtone, the browser's autoplay policy blocked audio.play() because the user hasn't interacted with the page yet. Click anywhere on the page (or just the Answer button) โ€” the ringtone will start, and the call will be answered in one motion.

Web dialer shows "Softphone Offline" and never registers

Open /pbx/dialer/debug and run all checks in order. The most common causes, in order of likelihood:

  1. No pbx_extension set for the logged-in user โ€” Step 1 (Config API) will show extension: null. Fix: $user->update(['pbx_extension' => '121', 'pbx_sip_password' => '...'])
  2. WebSocket port blocked by firewall โ€” Step 2 will time out or error; open port 8089 (or 8088 for non-SSL)
  3. Site is served over plain HTTP โ€” Step 4 (Microphone) will fail; WebRTC requires HTTPS except on localhost
  4. Wrong SIP password โ€” Step 5 will show a 401/403 in the raw SIP response
  5. Multiple UAs registered to the same extension โ€” if you have the dashboard open in two browser tabs, or you've previously visited /pbx/dialer/debug and started a test UA, MikoPBX may be forking the INVITE to the wrong contact. Close all tabs, refresh, and re-test.

AMI connection failed

MikoPBX AMI: Cannot connect to 163.223.240.124:5038

Checks:

  • Port 5038 is open on MikoPBX VPS firewall
  • MIKOPBX_AMI_HOST points to the correct IP
  • AMI user created in MikoPBX Admin โ†’ System โ†’ AMI Users
  • Laravel server IP is in the allowed IP list
# Test from your Laravel server:
telnet 163.223.240.124 5038
# Should show: Asterisk Call Manager/...

API key 401 Unauthorized

MikoPBX API error [401] GET /pbxcore/api/v3/cdr
# Test with curl:
curl -k -H "Authorization: Bearer YOUR_KEY" https://163.223.240.124/pbxcore/api/v3/system:ping
# Should return: {"result":true,"data":{"PONG":"..."},...}

SSL certificate error

Set MIKOPBX_VERIFY_SSL=false โ€” MikoPBX uses a self-signed certificate by default.

CDR sync inserting only 1 row, or wrong field names

MikoPBX v3's CDR response is nested (data.records[].records[]) โ€” see CDR Field Names & Nested Structure above. If you're on an older copy of this package that expected a flat array, update CdrSyncCommand.php to the current version, then re-run:

php artisan mikopbx:cdr-sync --days=7

Live Call Board shows calls but Transfer/Hangup do nothing

getActiveCalls() alone does not include a channel name field, so Transfer/Hangup have nothing to act on unless the channel is resolved via getActiveChannels() first. Confirm you're on the current LiveCallBoard.php, which cross-references both endpoints automatically.

Extensions not syncing

php artisan mikopbx:sync-extensions
# If 0 extensions: verify the API key has extensions read permission
curl -k -H "Authorization: Bearer YOUR_KEY" https://163.223.240.124/pbxcore/api/v3/extensions:getForSelect

License

MIT โ€” free for commercial use.

Built by BitDream IT, Bangladesh.

GitHub: github.com/bitdreamit/laravel-mikopbx