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.
Package info
github.com/bitdreamit/laravel-mikopbx
Language:Blade
pkg:composer/bitdreamit/laravel-mikopbx
Requires
- php: ^8.2
- laravel/framework: ^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^9.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
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.
Table of Contents
- Features
- Requirements
- Installation
- MikoPBX Setup
- Environment Variables
- Web Dialer Setup (WebRTC / JsSIP)
- Start AMI Listener
- Pages & Routes
- Facade Usage
- Livewire Components
- Artisan Commands
- REST API v3 Endpoints Reference
- How Call Control Works (AMI, not REST)
- CDR Field Names & Nested Structure
- Live Agent Online Status
- Testing with MikoPBXFake
- Database Tables
- Real-time Events (Laravel Echo)
- Package Structure
- Publishing to GitHub & Packagist
- Troubleshooting
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 youruserstable (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.examplewith 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
Authorizationheader. The oldX-Auth-Tokenheader 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/wsWebSocket 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:
- Config API โ confirms your user has
pbx_extensionset and the backend returns valid SIP config - WebSocket connection โ tests raw connectivity to
wss://.../asterisk/ws(catches firewall/port issues) - JsSIP library loaded โ confirms
public/vendor/mikopbx/jssip.min.jsis reachable - Microphone permission โ WebRTC requires HTTPS (except on
localhost) or mic access is silently blocked - SIP registration โ attempts a real REGISTER and shows the raw SIP response
- 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 ownnewRTCSessionevent โ this avoids the latency of a server round-trip for something as time-sensitive as a ringing call. If you're looking forIncomingCallPopup.php, it exists insrc/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'sLiveCallBoardcomponent cross-references it againstgetActiveChannels()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
textfield ofgetForSelectresponses โ the package strips these withstrip_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
newRTCSessionevent 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):
- Open browser DevTools โ Console. Look for
[ICE] iceConnectionState:lines.connectedorcompleted= 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).
- Look for
[ICE] โ No TURN server configuredwarning. If present, no TURN env vars are set. - Look at
[MikoPBX] ICE servers in use:โ confirm it includes aturn: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:
- UDP egress from the browser is not blocked (corporate firewalls often block UDP โ only TURN-over-TCP/443 will work in that case)
- The TURN server is reachable:
curl -v https://your-turn-host:3478(or use https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/) MIKOPBX_STUNis set to a reachable STUN server
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 โ
getUserMediais blocked onhttp://(exceptlocalhost), 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:
- MikoPBX Admin โ Extensions โ โ "Use WebRTC" toggle is OFF. Turn it ON, save, then refresh the browser page so JsSIP re-registers.
- 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
-WSWebRTC contact. Either unbind the desk phone, or configure MikoPBX to ring all contacts simultaneously (MikoPBX Admin โ Extensions โ "Call settings" โ "Ring simultaneously"). - The inbound route / IVR in MikoPBX points to a single extension number, not a ring-group that includes the
-WSvariant. Edit the inbound route to ring the extension (which MikoPBX will fork to all its registered contacts). - 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:
- No
pbx_extensionset for the logged-in user โ Step 1 (Config API) will showextension: null. Fix:$user->update(['pbx_extension' => '121', 'pbx_sip_password' => '...']) - WebSocket port blocked by firewall โ Step 2 will time out or error; open port 8089 (or 8088 for non-SSL)
- Site is served over plain HTTP โ Step 4 (Microphone) will fail; WebRTC requires HTTPS except on
localhost - Wrong SIP password โ Step 5 will show a
401/403in the raw SIP response - Multiple UAs registered to the same extension โ if you have the dashboard open in two browser tabs, or you've previously visited
/pbx/dialer/debugand 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_HOSTpoints 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.