PHPackages                             bitdreamit/laravel-mikopbx - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. bitdreamit/laravel-mikopbx

ActiveLibrary[API Development](/categories/api)

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 &amp; more.

1.1.9(3w ago)228MITBladePHP ^8.2

Since Jun 19Pushed 3w agoCompare

[ Source](https://github.com/bitdreamit/laravel-mikopbx)[ Packagist](https://packagist.org/packages/bitdreamit/laravel-mikopbx)[ Docs](https://github.com/bitdreamit/laravel-mikopbx)[ RSS](/packages/bitdreamit-laravel-mikopbx/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (10)Dependencies (11)Versions (15)Used By (0)

bitdreamit/laravel-mikopbx
==========================

[](#bitdreamitlaravel-mikopbx)

> 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](https://camo.githubusercontent.com/826ec2cf257f81755a0746fae6d5ff21155bffcaa4ee9a186b55ed2b49967b30/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d696e6469676f2e737667)](LICENSE)[![PHP](https://camo.githubusercontent.com/0f16581d1180dbfd4c0e13166ec1267d4ad2f2fab8281ea6d6b284cf5c65d921/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e322532422d626c75652e737667)](https://php.net)[![Laravel](https://camo.githubusercontent.com/83de96409118ea195839aace0a5acaa6558feffcd47ffed462dda8933c38885a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d313125324631322d7265642e737667)](https://laravel.com)[![MikoPBX](https://camo.githubusercontent.com/397af2e62f414fa06c0cd342b3691551f8f9f97bc2871fec20956ba7af8877cb/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d696b6f5042582d5245535425323041504925323076332d6f72616e67652e737667)](https://mikopbx.com)

---

Table of Contents
-----------------

[](#table-of-contents)

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [MikoPBX Setup](#mikopbx-setup)
- [Environment Variables](#environment-variables)
- [Web Dialer Setup (WebRTC / JsSIP)](#web-dialer-setup-webrtc--jssip)
- [Start AMI Listener](#start-ami-listener-supervisor)
- [Pages &amp; Routes](#pages--routes)
- [Facade Usage](#facade-usage)
- [Livewire Components](#livewire-components)
- [Artisan Commands](#artisan-commands)
- [REST API v3 Endpoints Reference](#rest-api-v3-endpoints-reference)
- [How Call Control Works (AMI, not REST)](#how-call-control-works-ami-not-rest)
- [CDR Field Names &amp; Nested Structure](#cdr-field-names--nested-structure)
- [Live Agent Online Status](#live-agent-online-status)
- [Testing with MikoPBXFake](#testing-with-mikopbxfake)
- [Database Tables](#database-tables)
- [Real-time Events (Laravel Echo)](#real-time-events-laravel-echo)
- [Package Structure](#package-structure)
- [Publishing to GitHub &amp; Packagist](#publishing-to-github--packagist)
- [Troubleshooting](#troubleshooting)

---

Features
--------

[](#features)

FeatureDescription📞 Live Call BoardReal-time active calls with transfer, mute, hangup via AMI📱 Web DialerReal browser softphone — WebRTC calling via JsSIP, no desk phone required🔔 Incoming Call PopupNative Answer/Reject alert with ringtone, driven directly by JsSIP's `newRTCSession` event👥 Agent ManagementStatus grid, click-to-call, sync from MikoPBX, DND/away support📢 Auto DialerCreate campaigns, upload number lists, voice broadcast, IVR survey🌿 IVR BuilderVisual node editor — Press 1 for Sales, Press 2 for Support📊 AnalyticsDaily trend, peak hours, ASR %, agent performance, Chart.js🎙️ RecordingsAudio player, proxy stream with Bearer auth, download, search by number/date🚫 BlacklistBlock inbound/outbound numbers with expiry📅 CallbacksSchedule, prioritise, attempt, assign to agent🎙️ ConferenceRoom list, kick/mute participants❤️ Health MonitorAMI + SIP + REST API status check with 60-second auto-poll🧪 Dialer Debug PageStep-by-step diagnostic tool for WebSocket, mic, and SIP registration issues🧪 MikoPBXFakeFull test double — `assertOriginated`, `assertTransferred` etc.---

Requirements
------------

[](#requirements)

RequirementVersionNotesPHP8.2+Required for enumsLaravel11 or 12Tested on bothLivewire3.xFor real-time componentsMikoPBX2024.2+REST API v3 and WebRTC must be enabledA modern browseranyChrome/Firefox/Edge — WebRTC requires HTTPS in productionMySQL / PostgreSQL / SQLiteanyFor local CDR storage---

Installation
------------

[](#installation)

### Step 1 — Install the package

[](#step-1--install-the-package)

```
composer require bitdreamit/laravel-mikopbx
```

### Step 2 — Run the installer

[](#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

[](#step-3--add-to-env)

Copy `.env.mikopbx.example` and add the values to your `.env` file. See [Environment Variables](#environment-variables) below.

### Step 4 — Set up MikoPBX admin panel

[](#step-4--set-up-mikopbx-admin-panel)

See [MikoPBX Setup](#mikopbx-setup) below.

### Step 5 — Assign extensions to your users

[](#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

[](#step-6--start-the-ami-listener)

See [Start AMI Listener](#start-ami-listener-supervisor) below.

### Step 7 — Sync extensions and open the dashboard

[](#step-7--sync-extensions-and-open-the-dashboard)

```
php artisan mikopbx:sync-extensions
```

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

---

MikoPBX Setup
-------------

[](#mikopbx-setup)

### 1. Enable AMI User

[](#1-enable-ami-user)

Go to: **MikoPBX Admin Panel → System → AMI Users → Add**

FieldValueUsername`laravelapp`Secret`your-strong-ami-secret`Allowed IPYour Laravel server IPPermissionsall (or: call, originate, reporting, system)Save and Apply Config.

### 2. Get REST API Key

[](#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

[](#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

[](#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
---------------------

[](#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)
---------------------------------

[](#web-dialer-setup-webrtc--jssip)

The package includes a full browser-based softphone built on [JsSIP](https://jssip.net) — 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

[](#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

[](#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

[](#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`

[](#diagnosing-dialer-issues--pbxdialerdebug)

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

[](#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)
-------------------------------

[](#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 &amp; Routes
------------------

[](#pages--routes)

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

URLRoute NameDescription`/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)

[](#internal-api--ajax-routes-used-by-the-frontend)

MethodURLDescriptionGET`/pbx/calls/active/json`Active calls list, polled by the dashboard headerPOST`/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 headerPOST`/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](#live-agent-online-status)POST`/pbx/agents/sync`Pull extensions from MikoPBXGET`/pbx/dialer/config`SIP/JsSIP config for the current user's browser softphone### Webhook route (no auth)

[](#webhook-route-no-auth)

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

Facade Usage
------------

[](#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
-------------------

[](#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
----------------

[](#artisan-commands)

CommandDescription`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)

[](#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
-------------------------------

[](#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

[](#cdr--call-records)

MethodEndpointDescriptionGET`/pbxcore/api/v3/cdr`List CDR with filtersGET`/pbxcore/api/v3/cdr/{id}`Single CDR recordDELETE`/pbxcore/api/v3/cdr/{id}`Delete CDR recordGET`/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 metadataGET`/pbxcore/api/v3/cdr:getStatsByProvider`Stats by SIP providerGET `/pbxcore/api/v3/cdr` query parameters:

ParameterRequiredDescription`limit`NoMax records (default 20, max 100)`offset`NoSkip N records for pagination`dateFrom`NoStart date: `2026-06-01 00:00:00``dateTo`NoEnd date: `2026-06-30 23:59:59``src_num`NoFilter by caller number`dst_num`NoFilter by called number### PBX Status

[](#pbx-status)

MethodEndpointDescriptionGET`/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 &amp; Employees

[](#extensions--employees)

MethodEndpointDescriptionGET`/pbxcore/api/v3/extensions:getForSelect`Extensions as `{value, text}`GET`/pbxcore/api/v3/extensions`Full extension listGET`/pbxcore/api/v3/extensions/{id}`Single extensionGET`/pbxcore/api/v3/employees`All employeesPOST`/pbxcore/api/v3/employees`Create employeePUT`/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

[](#sip-peer-status)

MethodEndpointDescriptionGET`/pbxcore/api/v3/sip:getPeersStatuses`All SIP peers with stateGET`/pbxcore/api/v3/sip:getRegistry`SIP registration statusGET`/pbxcore/api/v3/sip/{id}:getStatus`Single peer statusGET`/pbxcore/api/v3/sip/{id}:getStats`Peer call statisticsSIP peer state values: `OK` | `REGISTERED` | `UNREACHABLE` | `LAGGED` | `UNKNOWN` | `OFF`

### SIP Providers (trunks)

[](#sip-providers-trunks)

MethodEndpointDescriptionGET`/pbxcore/api/v3/sip-providers:getStatuses`All trunk registration statesGET`/pbxcore/api/v3/sip-providers`Full trunk listGET`/pbxcore/api/v3/sip-providers/{id}:getStatus`Single trunk statusPOST`/pbxcore/api/v3/sip-providers/{id}:forceCheck`Force re-registration### IVR Menu

[](#ivr-menu)

MethodEndpointDescriptionGET`/pbxcore/api/v3/ivr-menu`All IVR menusPOST`/pbxcore/api/v3/ivr-menu`Create IVR menuPUT`/pbxcore/api/v3/ivr-menu/{id}`Update IVR menuDELETE`/pbxcore/api/v3/ivr-menu/{id}`Delete IVR menu### Conference Rooms

[](#conference-rooms)

MethodEndpointDescriptionGET`/pbxcore/api/v3/conference-rooms`All conference roomsPOST`/pbxcore/api/v3/conference-rooms`Create room### Sound Files

[](#sound-files)

MethodEndpointDescriptionGET`/pbxcore/api/v3/sound-files:getForSelect`As `{value, text}` listPOST`/pbxcore/api/v3/sound-files:uploadFile`Upload audio (multipart)### System

[](#system)

MethodEndpointDescriptionGET`/pbxcore/api/v3/sysinfo:getInfo`System info, disk, CPU, versionGET`/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)
--------------------------------------

[](#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

[](#ami-event--laravel-event)

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

AMI EventTriggerLaravel EventEcho 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 &amp; Nested Structure
--------------------------------------

[](#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 ColumnMikoPBX v3 FieldNotes`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 keyPagination uses `data.pagination.hasMore` (not a simple record-count comparison) to know when to fetch the next page.

---

Live Agent Online Status
------------------------

[](#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
------------------------

[](#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

[](#available-assertions)

MethodDescription`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
---------------

[](#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 / ColumnDescription`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)
-------------------------------

[](#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.

ChannelEventTrigger`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
-----------------

[](#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

[](#anyone-installs-it)

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

---

Troubleshooting
---------------

[](#troubleshooting)

### Outbound call connects but has NO audio on either side

[](#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:

- 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 )
- `MIKOPBX_STUN` is 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 — `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

[](#incoming-calls-dont-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

[](#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

[](#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

[](#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

[](#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

[](#cdr-sync-inserting-only-1-row-or-wrong-field-names)

MikoPBX v3's CDR response is nested (`data.records[].records[]`) — see [CDR Field Names &amp; Nested Structure](#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

[](#live-call-board-shows-calls-but-transferhangup-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

[](#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
-------

[](#license)

MIT — free for commercial use.

Built by [BitDream IT](https://bitdreamit.com), Bangladesh.

GitHub: [github.com/bitdreamit/laravel-mikopbx](https://github.com/bitdreamit/laravel-mikopbx)

###  Health Score

45

—

FairBetter than 91% of packages

Maintenance95

Actively maintained with recent releases

Popularity13

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity54

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

14

Last Release

24d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/13940491?v=4)[MD. SIRAJ UD DOULLA](/maintainers/SirajCse)[@SirajCse](https://github.com/SirajCse)

---

Top Contributors

[![SirajCse](https://avatars.githubusercontent.com/u/13940491?v=4)](https://github.com/SirajCse "SirajCse (44 commits)")

---

Tags

laravelcrmasteriskamivoippbxmikopbxcall centerARIauto-dialer

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/bitdreamit-laravel-mikopbx/health.svg)

```
[![Health](https://phpackages.com/badges/bitdreamit-laravel-mikopbx/health.svg)](https://phpackages.com/packages/bitdreamit-laravel-mikopbx)
```

###  Alternatives

[statamic/cms

The Statamic CMS Core Package

4.9k3.8M1.1k](/packages/statamic-cms)[backpack/crud

Quickly build admin interfaces using Laravel, Bootstrap and JavaScript.

3.4k3.8M227](/packages/backpack-crud)[unopim/unopim

UnoPim Laravel PIM

10.8k2.5k](/packages/unopim-unopim)[bagisto/bagisto

Bagisto Laravel E-Commerce

27.9k175.2k9](/packages/bagisto-bagisto)[leantime/leantime

Open source project management system for non-project managers. Simple like Trello, powerful like Jira. Built with neurodiversity in mind.

10.2k4.0k](/packages/leantime-leantime)[tencentcloud/tencentcloud-sdk-php

TencentCloudApi php sdk

3661.3M49](/packages/tencentcloud-tencentcloud-sdk-php)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
