php-ksip-telnet

A PHP library for managing FreePBX/Asterisk PJSIP extensions remotely via SSH.

PHP >= 7.4 MIT License Composer Laravel Ready

Requirements

Installation

composer require codego/php-ksip-telnet

Usage

1. Connect to SSH

<?php

require 'vendor/autoload.php';

use KsipTelnet\SSHClient;

$client = new SSHClient();

// Default port 22
$client->connect('your-server-ip', 'root', 'your-password');

// Custom port
$client->connect('your-server-ip', 'root', 'your-password', 2222);

2. Create a PJSIP Extension

$result = $client->createExtensionKsip(
    '1001',          // extension number
    'secret123',     // extension password
    'freepbxuser',   // MySQL username
    'dbpassword'     // MySQL password
);

echo $result['sql_output'];
echo $result['reload_output'];

3. Get All Extensions

$extensions = $client->getExtKsipList('freepbxuser', 'dbpassword');
// returns: ['1001', '1002', '1003', ...]

4. Generate Extension if Not Exists

$result = $client->genExtKsip(
    [
        'extName'  => '1005',
        'password' => 'secret123'  // optional, defaults to extName
    ],
    'freepbxuser',
    'dbpassword'
);

// If extension already exists:
// ['status' => 'exists', 'extension' => '1005', 'message' => 'Extension 1005 already exists']

// If extension was created:
// ['status' => 'created', 'extension' => '1005', 'result' => [...]]

5. Run a Custom SSH Command

$output = $client->exec('asterisk -rx "pjsip show endpoints"');
echo $output;

Environment Variables (Recommended)

Instead of hardcoding credentials, use environment variables:

export SSH_HOST=your-server-ip
export SSH_USER=root
export SSH_PASS=your-password
export SSH_PORT=22
$client->connect(
    getenv('SSH_HOST'),
    getenv('SSH_USER'),
    getenv('SSH_PASS'),
    getenv('SSH_PORT') ?: 22
);

Laravel Integration

1

Install the package

composer require codego/php-ksip-telnet
2

Add credentials to .env

SSH_HOST=your-server-ip
SSH_USER=root
SSH_PASS=your-password
SSH_PORT=22
SSH_DB_USER=freepbxuser
SSH_DB_PASS=dbpassword
3

Create a Service class

php artisan make:service FreePBXService
<?php

namespace App\Services;

use KsipTelnet\SSHClient;

class FreePBXService
{
    protected SSHClient $client;

    public function __construct()
    {
        $this->client = new SSHClient();
        $this->client->connect(
            config('services.freepbx.host'),
            config('services.freepbx.user'),
            config('services.freepbx.pass'),
            config('services.freepbx.port', 22)
        );
    }

    public function createExtensionKsip(string $ext, string $password): array
    {
        return $this->client->createExtensionKsip(
            $ext, $password,
            config('services.freepbx.db_user'),
            config('services.freepbx.db_pass')
        );
    }

    public function getExtKsipList(): array
    {
        return $this->client->getExtKsipList(
            config('services.freepbx.db_user'),
            config('services.freepbx.db_pass')
        );
    }

    public function genExtKsip(array $data): array
    {
        return $this->client->genExtKsip(
            $data,
            config('services.freepbx.db_user'),
            config('services.freepbx.db_pass')
        );
    }

    public function exec(string $command): string
    {
        return $this->client->exec($command);
    }
}
4

Register in config/services.php

'freepbx' => [
    'host'    => env('SSH_HOST'),
    'user'    => env('SSH_USER'),
    'pass'    => env('SSH_PASS'),
    'port'    => env('SSH_PORT', 22),
    'db_user' => env('SSH_DB_USER'),
    'db_pass' => env('SSH_DB_PASS'),
],
5

Bind in AppServiceProvider

use App\Services\FreePBXService;

public function register(): void
{
    $this->app->singleton(FreePBXService::class);
}
6

Use in a Controller

php artisan make:controller ExtensionController
<?php

namespace App\Http\Controllers;

use App\Services\FreePBXService;
use Illuminate\Http\Request;

class ExtensionController extends Controller
{
    public function __construct(protected FreePBXService $freepbx) {}

    public function store(Request $request)
    {
        $request->validate([
            'extension' => 'required|numeric',
            'password'  => 'required|string',
        ]);

        return response()->json(
            $this->freepbx->createExtensionKsip($request->extension, $request->password)
        );
    }

    public function index()
    {
        return response()->json($this->freepbx->getExtKsipList());
    }

    public function generate(Request $request)
    {
        $request->validate([
            'extName'  => 'required|numeric',
            'password' => 'nullable|string',
        ]);

        return response()->json(
            $this->freepbx->genExtKsip($request->only('extName', 'password'))
        );
    }
}
7

Add routes in routes/api.php

use App\Http\Controllers\ExtensionController;

Route::get('/extensions', [ExtensionController::class, 'index']);
Route::post('/extensions', [ExtensionController::class, 'store']);
Route::post('/extensions/generate', [ExtensionController::class, 'generate']);
8

Test via API

curl -X POST http://your-app.com/api/extensions \
  -H "Content-Type: application/json" \
  -d '{"extension": "1001", "password": "secret123"}'

Auto Extension Registration

Automatically generates a unique 12-digit extension number from a user's name acronym + birthdate, then registers it in FreePBX. Can run as a cron job or be called directly from a controller.

Extension Number Formula

Each initial letter is converted to its alphabet position (A=01 … Z=26, zero-padded to 2 digits), then the birthdate is appended as mmddyy. If the total is less than 12 digits, random digits fill the remainder.

PartExampleResult
Last name initial — L (12th letter)Luna12
First name initial — J (10th letter)Juan10
Middle name initial — M (13th letter)Mercado13
Birthdate (mmddyy)12/16/1996121696
Final Extension121013121696
If the generated number is shorter than 12 digits (e.g. short names), random digits are appended automatically until it reaches exactly 12 digits.
1

Scaffold the cron job

php artisan make:ksip-register-user

Generates app/Console/Commands/AssignExtensionToUsers.php and appends the schedule entry to routes/console.php.

2

Add to .env

SSH_HOST=your-server-ip
SSH_USER=root
SSH_PASS=your-password
SSH_PORT=22
SSH_DB_USER=freepbxuser
SSH_DB_PASS=dbpassword
3

Run the scheduler

# Run once manually
php artisan ksip:assign-extensions

# Or let the scheduler handle it every minute
php artisan schedule:run

Production: Auto-run via Supervisor

In production, use Supervisor to keep the scheduler running automatically — no need to manually trigger it every minute.

1. Install Supervisor

sudo apt install supervisor

2. Create config file

sudo nano /etc/supervisor/conf.d/laravel-scheduler.conf
[program:laravel-scheduler]
process_name=%(program_name)s
command=bash -c "while true; do php /var/www/your-project/artisan schedule:run; sleep 60; done"
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/www/your-project/storage/logs/scheduler.log
Replace /var/www/your-project with your actual project path and www-data with your server user (e.g. ubuntu, forge, deployer).

3. Start Supervisor

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-scheduler

4. Check status

sudo supervisorctl status laravel-scheduler

Supervisor will automatically restart the scheduler if the server reboots or the process crashes. Logs are written to storage/logs/scheduler.log.

4

Call directly from a Registration Controller

Use SSHClient::ksipRegisterUser() to assign an extension immediately on user registration — no need to wait for the cron job.

<?php

namespace App\Http\Controllers\Auth;

use App\Models\User;
use Illuminate\Http\Request;
use KsipTelnet\SSHClient;

class RegisterController extends Controller
{
    public function register(Request $request)
    {
        $request->validate([
            'first_name'  => 'required|string',
            'last_name'   => 'required|string',
            'middle_name' => 'nullable|string',
            'birth_date'  => 'required|date',
            'email'       => 'required|email|unique:users',
            'password'    => 'required|min:8|confirmed',
        ]);

        $user = User::create([
            'first_name'  => $request->first_name,
            'last_name'   => $request->last_name,
            'middle_name' => $request->middle_name,
            'birth_date'  => $request->birth_date,
            'email'       => $request->email,
            'password'    => bcrypt($request->password),
        ]);

        $ssh = new SSHClient();
        $ssh->connect(
            config('services.freepbx.host'),
            config('services.freepbx.user'),
            config('services.freepbx.pass'),
            config('services.freepbx.port', 22)
        );

        $result = SSHClient::ksipRegisterUser(
            $user,
            $ssh,
            config('services.freepbx.db_user'),
            config('services.freepbx.db_pass')
        );

        return response()->json([
            'user'      => $user,
            'extension' => $result['extension'],
            'status'    => $result['status'],
        ], 201);
    }
}

Generate Extension Number Only (no SSH)

use KsipTelnet\SSHClient;

$ext = SSHClient::generateExtensionFromUser(
    'Luna',      // last_name
    'Juan',      // first_name
    'Mercado',   // middle_name
    '12/16/1996' // birth_date
);

// $ext → '121013121696'

Return Values of ksipRegisterUser

statusMeaning
assignedExtension generated and registered in FreePBX
skippedUser already has an extensionName
errorMissing name or birthdate fields

Artisan Scaffold Generator

The package includes a make:ksipgen command that auto-generates call recording scaffold files in your Laravel project connects to KSIP (juv-ksip-softphone) just install reacr ksip juv-ksip-softphone.

Install React KSIP

npm i juv-ksip-softphone@latest

What it generates

Usage

php artisan make:ksipgen
php artisan migrate

Generated Routes

Route::prefix('recordings')->group(function () {
    Route::post('/upload', [CallRecordingController::class, 'upload']);
    Route::get('/', [CallRecordingController::class, 'index']);
    Route::get('/{id}', [CallRecordingController::class, 'show']);
    Route::get('/{id}/download', [CallRecordingController::class, 'download']);
    Route::delete('/{id}', [CallRecordingController::class, 'delete']);
});

Upload Example

curl -X POST http://your-app.com/api/recordings/upload \
  -F "file=@/path/to/recording.wav" \
  -F "caller=1001" \
  -F "callee=1002" \
  -F "duration=60"

Migration Schema

Schema::create('call_recordings', function (Blueprint $table) {
    $table->id();
    $table->string('filename');
    $table->string('path');
    $table->string('caller')->nullable();
    $table->string('callee')->nullable();
    $table->integer('duration')->nullable();
    $table->timestamps();
});

Available API Endpoints

Method Endpoint Description
GET /api/recordings List all recordings
POST /api/recordings/upload Upload a recording (mp3/wav/ogg)
GET /api/recordings/{id} Get a single recording
GET /api/recordings/{id}/download Download a recording
DELETE /api/recordings/{id} Delete a recording

License

This package is open-sourced software licensed under the MIT license.

juv-ksip-softphone

A professional, draggable, resizable, transparent React WebRTC SIP softphone component for FreePBX / Asterisk. Supports audio and video calls, incoming call handling, codec selection, and a global ksipcall API for triggering calls from anywhere in your app.

React WebRTC SIP MIT License npm

How does it work?

The component connects to your FreePBX/Asterisk server via WebSocket SIP (ws:// port 8088 or wss:// port 8089). Once registered, WebRTC handles audio/video streams natively in the browser — no plugins needed.

Browser  ──WebSocket──▶  FreePBX/Asterisk  ──SIP──▶  Other Phone
           (SIP/WS)           (PBX)
Browser  ◀──WebRTC──▶   FreePBX/Asterisk  ◀──RTP──▶  Other Phone
           (Audio/Video)      (Media)

Screenshots

Settings Panel Incoming Call Video Call Dialer Fullscreen

Features

Installation

npm install juv-ksip-softphone

Quick Start

import { Softphone } from 'juv-ksip-softphone';
import 'juv-ksip-softphone/styles';

function App() {
  return (
    <>
      <YourExistingApp />
      <Softphone />
    </>
  );
}

The softphone renders as a transparent overlay with a floating phone button (top-right). Click it to open the nav menu.

Props

SIP Connection Props

PropTypeDefaultDescription
serverstring""FreePBX server IP or hostname
wsProtocolws | wss"ws"WebSocket protocol
wsPortstring"8088"WebSocket port
extensionstring""SIP extension number
passwordstring""SIP extension password
displayNamestring""Caller ID display name

Recording Props

PropTypeDefaultDescription
autoRecordbooleanfalseEnable automatic recording of all calls
recordingDirstring"video/recordings/Ksip"Directory path for saved recordings
uploadApiUrlstring""API endpoint URL for uploading recordings

Settings Configuration Props

PropTypeDefaultDescription
settingConfigTogglesobjectall trueControls which toggles are visible in settings UI
settingConfigTogglesActiveStateobjectvariesSets initial active state for toggles
settingConfigCodecsobjectall visibleControls codec visibility and available options

UI Props

PropTypeDefaultDescription
enabledBubblebooleantrueShow or hide the entire softphone bubble
showDialerbooleantrueShow the dialer button in the FAB nav
showSettingbooleantrueShow the settings button in the FAB nav
showOpacitybooleantrueShow the opacity button in the FAB nav
answerwithVideoCallbooleanfalseAuto-answer incoming calls with video
ShowIncomingCallVideoBtnbooleantrueShow the video answer button on incoming calls
ShowIncomingCallAudiobooleantrueShow the audio answer button on incoming calls

Examples

Basic — manual config via settings panel

<Softphone />

Pre-configured — auto-connect on load

<Softphone
  server="192.168.1.100"
  wsProtocol="ws"
  wsPort="8088"
  extension="1001"
  password="mypassword"
  displayName="John Doe"
/>

Full configuration

<Softphone
  server="192.168.1.100"
  wsProtocol="ws"
  wsPort="8088"
  extension="1001"
  password="mypassword"
  displayName="John Doe"
  enabledBubble={true}
  showDialer={true}
  showSetting={true}
  showOpacity={true}
  answerwithVideoCall={false}
  ShowIncomingCallVideoBtn={true}
  ShowIncomingCallAudio={true}
/>

Audio-only mode

<Softphone
  ShowIncomingCallVideoBtn={false}
  ShowIncomingCallAudio={true}
/>

wss:// for HTTPS pages

<Softphone
  server="pbx.yourdomain.com"
  wsProtocol="wss"
  wsPort="8089"
  extension="1001"
  password="mypassword"
/>

Settings Configuration

<Softphone
  settingConfigToggles={{
    bubble: true,
    dialer: true,
    settings: false,  // Hide settings toggle
    opacity: true,
    autoAnswerVideo: true,
    answerButtonVideo: false,  // Hide video answer button toggle
    answerButtonAudio: true,
    fullscreen: true,
    autoRecording: true,
  }}
  settingConfigTogglesActiveState={{
    bubble: true,
    dialer: true,
    settings: true,
    opacity: true,
    autoAnswerVideo: true,  // Start with auto-answer video ON
    answerButtonVideo: true,
    answerButtonAudio: false,
    fullscreen: false,
    autoRecording: true,  // Start with recording ON
  }}
  settingConfigCodecs={{
    audio: {
        visible: true, 
    },  
    video: {
        visible: false, 
    },  
  }}
/>

Auto Recording with API Upload

<Softphone
  autoRecord={true}
  recordingDir="video/recordings/Ksip"
  uploadApiUrl="https://your-domain.com/api/recordings/upload"
/>

Complete Example with All Features

<Softphone
  // SIP Connection
  server="192.168.1.100"
  wsProtocol="ws"
  wsPort="8088"
  extension="1001"
  password="mypassword"
  displayName="John Doe"
  
  // UI Controls
  enabledBubble={true}
  showDialer={true}
  showSetting={true}
  showOpacity={true}
  answerwithVideoCall={false}
  ShowIncomingCallVideoBtn={true}
  ShowIncomingCallAudio={true}
  fullscreen={false}
  
  // Recording
  autoRecord={true}
  recordingDir="video/recordings/Ksip"
  uploadApiUrl="https://your-domain.com/api/recordings/upload"
  
  // Settings Configuration
  settingConfigToggles={{
    bubble: true,
    dialer: true,
    settings: true,
    opacity: true,
    autoAnswerVideo: true,
    answerButtonVideo: true,
    answerButtonAudio: true,
    fullscreen: true,
    autoRecording: true,
  }}
  settingConfigTogglesActiveState={{
    bubble: true,
    dialer: true,
    settings: true,
    opacity: true,
    autoAnswerVideo: false,
    answerButtonVideo: true,
    answerButtonAudio: true,
    fullscreen: false,
    autoRecording: true,
  }}
  settingConfigCodecs={{
    audio: { visible: true, codecs: ["PCMU", "PCMA", "opus"] },
    video: { visible: true, codecs: ["VP8", "H264"] },
  }}
/>

ksipcall API

The ksipcall global lets you trigger calls from anywhere — plain JavaScript, Vue, Angular, or any other framework.

Audio Call

ksipcall.audio("123");

Video Call

ksipcall.video("123");

Via window (plain HTML)

<button onclick="ksipcall.audio('123')">Call Support</button>
<button onclick="ksipcall.video('456')">Video Call</button>

Import in React

import { ksipcall } from 'juv-ksip-softphone';

ksipcall.audio("123");
ksipcall.video("123");

Settings Panel

The in-app settings panel (accessible via the ⚙ button) has a 3-column layout.

Column 1 — SIP Configuration

FieldDescriptionExample
FreePBX Server IPIP or hostname192.168.1.100
ExtensionSIP extension number1001
PasswordExtension SIP passwordmypassword
Display NameCaller ID name (optional)John Doe
Protocolws:// or wss://ws://
PortWebSocket port8088

Column 2 — Codecs

Audio: PCMU, PCMA, G722, G729, opus

Video: VP8, VP9, H264, H265, AV1

Column 3 — UI Preferences

CDN Usage (Browser)

Use directly in the browser via CDN — no build tools required.

1. Include scripts

<!-- Styles -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/juv-ksip-softphone@1.0.35/dist/juv-ksip-softphone.css">

<!-- Softphone bundle -->
<script src="https://cdn.jsdelivr.net/npm/juv-ksip-softphone@1.0.35/dist/juv-ksip-softphone.cdn.js"></script>

2. Mount the softphone

<div id="softphone"></div>

<script>
  const { Softphone, createRoot, createElement } = window.JuvKsipSoftphone;
  createRoot(document.getElementById('softphone')).render(
    createElement(Softphone, {
      server: "192.168.1.100",
      wsProtocol: "ws",
      wsPort: "8088",
      extension: "1001",
      password: "mypassword",
      displayName: "John Doe"
    })
  );
</script>

3. Trigger calls via ksipcall (CDN)

<button onclick="window.ksipcall.audio('1002')">Audio Call</button>
<button onclick="window.ksipcall.video('1002')">Video Call</button>

WebSocket Protocol

ProtocolPortUse Case
ws://8088Local network, HTTP pages
wss://8089Production, HTTPS pages (requires SSL cert on FreePBX)
Browsers block ws:// when the page is served over https://. Use wss:// with a valid SSL certificate for production.

FreePBX / Asterisk Requirements

For WebRTC to work, each SIP extension must have these settings:

SettingValue
webrtcyes
use_avpfyes
media_encryptiondtls
ice_supportyes
bundleyes
rtcp_muxyes
dtls_setupactpass

Enable via CLI

[1001](+)
webrtc=yes
asterisk -rx "pjsip reload"

Auto Recording

The softphone supports automatic recording of audio and video calls with configurable save directory and API upload.

Basic Usage

<Softphone autoRecord={true} />

Custom Directory

<Softphone
  autoRecord={true}
  recordingDir="video/recordings/Ksip"
/>

With API Upload

<Softphone
  autoRecord={true}
  recordingDir="video/recordings/Ksip"
  uploadApiUrl="https://your-domain.com/api/recordings/upload"
/>

How It Works

  1. When auto-record is enabled, a modal prompts user to select a directory
  2. Browser's File System Access API creates video/recordings/Ksip/ folder structure
  3. Recordings save directly to selected folder without download prompts
  4. If API URL is configured, uploads to server (once per day to prevent duplicates)
  5. Browser remembers directory permission for future recordings

Convert to MP3

ffmpeg -i recording.webm -vn -ar 44100 -ac 2 -b:a 192k output.mp3

localStorage

Config is automatically saved under the key sip_softphone_config and restored on next page load, including SIP credentials, WebSocket settings, codecs, recording settings, and UI preferences.

showSetting is never saved to localStorage — it is always controlled by props.
{
  "server": "192.168.1.100",
  "extension": "1001",
  "password": "mypassword",
  "displayName": "John Doe",
  "wsProtocol": "ws",
  "wsPort": "8088",
  "audioCodecs": ["PCMU", "PCMA"],
  "videoCodecs": ["VP8", "H264"],
  "enabledBubble": true,
  "showDialer": true,
  "showOpacity": true,
  "answerwithVideoCall": false,
  "ShowIncomingCallVideoBtn": true,
  "ShowIncomingCallAudio": true,
  "fullscreen": false,
  "autoRecord": true,
  "recordingDir": "video/recordings/Ksip",
  "uploadApiUrl": "https://api.example.com/upload",
  "hasDirectoryAccess": true
}

Status Toast Notification

A floating status indicator appears at the top-center of the screen showing the current connection state:

StatusColorBehavior
Connected🟢 GreenAuto-hides after 5 seconds
Reconnecting🟡 YellowStays visible until connected
Not Connected🔴 RedStays visible until connected

Connection Monitoring

The softphone includes robust connection monitoring to detect server failures:

Example Scenario:
• App connected at 9:00 AM
• Server goes down at 9:30 AM (30 minutes later)
• Status toast immediately shows "Reconnecting..." (yellow)
• Auto-retry every 3 seconds
• When server comes back online → auto-reconnects → "Connected" (green)

Keyboard Shortcuts

ShortcutAction
Ctrl + Shift + KToggle settings panel