# FreePBX API — Findings & Integration Reference

> Compiled: April 10, 2026
> Scope: FreePBX 15+ "API" module (OAuth2 / GraphQL) as integrated in this codebase

---

## 1. Two Distinct APIs

FreePBX ships with two separate API systems. Only the modern one is active here.

|                          | Legacy REST Module              | Modern API Module                   |
|--------------------------|---------------------------------|-------------------------------------|
| **FreePBX version**      | 13+ (largely abandoned)         | 15+ (actively maintained)           |
| **Auth**                 | HMAC token + tokenkey in headers | OAuth 2.0 client_credentials       |
| **Transport**            | REST (JSON over HTTP)           | GraphQL over HTTP POST              |
| **Extension CRUD**       | ❌ No create/update/delete      | ✅ Full CRUD                        |
| **WebRTC / PJSIP params**| ❌ None                         | ❌ None (abstracted away)           |
| **Base path**            | `/restapi/`                     | `/admin/api/api/`                   |
| **Status in this project**| Not used                       | Active — see `app/Services/PBX/`   |

---

## 2. Modern API — Authentication

**Grant type**: `client_credentials`
**Token endpoint**: `POST /admin/api/api/token`

```http
POST /admin/api/api/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id={client_id}
&client_secret={client_secret}
&scope=gql rest
```

Credentials are stored encrypted in the Laravel `pbx_api_credentials` table and in `asterisk.api_applications`
(FreePBX side). Managed by `App\Services\PBX\PBXCredentialManager` and provisioned with
`php artisan pbx:generate-credentials`.

---

## 3. Modern API — Endpoints

| Purpose                   | Method | Path                                         | Auth scope        |
|---------------------------|--------|----------------------------------------------|-------------------|
| Obtain token              | POST   | `/admin/api/api/token`                       | (form body creds) |
| GraphQL                   | POST   | `/admin/api/api/gql`                         | `gql` Bearer      |
| REST module               | ANY    | `/admin/api/api/rest/{module}/{route}`       | `rest` Bearer     |
| Admin — list REST users   | GET    | `/admin/api/api/rest_users`                  | HTTP Basic (admin)|
| Admin — create REST user  | POST   | `/admin/api/api/rest_users`                  | HTTP Basic (admin)|
| Admin — grant scopes      | POST   | `/admin/api/api/rest_users/{username}/scopes`| HTTP Basic (admin)|
| Framework version (health)| GET    | `/admin/api/api/rest/framework/version`      | `rest` Bearer     |

All calls go to `http://127.0.0.1` (configurable via `FREEPBX_API_URL` / `config('pbx.api_url')`).

---

## 4. GraphQL Operations (Extension Management)

### 4.1 Create Extension

```graphql
mutation AddExtension($input: addExtensionInput!) {
    addExtension(input: $input) { status message }
}
```

**Input fields** (`addExtensionInput`):

| Field         | Type    | Notes                              |
|---------------|---------|------------------------------------|
| `extensionId` | String! | SIP extension number (e.g. `"1095"`) |
| `name`        | String  | Display name                       |
| `email`       | String  | User email                         |
| `tech`        | String  | Always `"pjsip"`                   |
| `vmEnable`    | Boolean | Voicemail on/off                   |

Internally calls FreePBX `Core->processQuickCreate()`. Does **not** set the SIP password — that
requires a subsequent `updateExtension` call.

**Idempotent**: if the extension already exists FreePBX returns `"already in use"`.
`ExtensionManager` catches this and skips silently.

---

### 4.2 Update Extension (including password)

```graphql
mutation UpdateExtension($input: updateExtensionInput!) {
    updateExtension(input: $input) { status message }
}
```

**Input fields** (`updateExtensionInput`):

| Field         | Type    | Notes                        |
|---------------|---------|------------------------------|
| `extensionId` | String! | Extension to update          |
| `extPassword` | String  | SIP auth password            |
| `name`        | String  | Display name                 |
| `vmEnable`    | Boolean | Voicemail on/off             |
| `vmPassword`  | String  | Voicemail PIN                |
| `outboundCid` | String  | Outbound caller ID override  |

---

### 4.3 Delete Extension

```graphql
mutation DeleteExtension($input: deleteExtensionInput!) {
    deleteExtension(input: $input) { status message }
}
```

---

### 4.4 Read Extension

```graphql
query FetchExtension($id: ID!) {
    fetchExtension(extensionId: $id) {
        extensionId tech status
        user { extension name extPassword outboundCid }
        coreDevice { deviceId tech dial }
    }
}
```

---

### 4.5 Apply Config / Reload

```graphql
mutation DoReload($input: doreloadInput!) {
    doreload(input: $input) { status message }
}
```

Input must be an empty object `{}`. Equivalent to clicking "Apply Config" in the FreePBX GUI.

---

## 5. What the API Cannot Do

The API abstracts extensions at the FreePBX "device" level. It has **no fields** for raw PJSIP
endpoint parameters. The following cannot be set via the API:

| Parameter                         | Required for                    | Status           |
|-----------------------------------|---------------------------------|------------------|
| `webrtc`                          | WebRTC/WSS browser clients      | ❌ Not in API    |
| `use_avpf`                        | DTLS-SRTP                       | ❌ Not in API    |
| `media_encryption`                | DTLS-SRTP (`dtls` value)        | ❌ Not in API    |
| `media_use_received_transport`    | WSS transport auto-selection    | ❌ Not in API    |
| `dtls_cert_file`/`dtls_private_key` | DTLS certificate              | ❌ Not in API    |
| `dtls_setup` / `dtls_verify`      | DTLS negotiation                | ❌ Not in API    |
| `bundle`                          | SDP bundle (WebRTC)             | ❌ Not in API    |
| `timers=no`                       | WebRTC browser compat           | ❌ Not in API    |
| `user_eq_phone=no`                | WebRTC UA identification        | ❌ Not in API    |
| `transport=` (pin)                | Force a specific transport      | ❌ Not in API    |

Any advanced PJSIP tuning requires writing directly to the FreePBX MySQL DB (`asterisk.pjsip` table)
or editing `_custom_post.conf` files.

---

## 6. The WSS / WebRTC Mirror Pattern (`99`-prefix)

FreePBX auto-generates a `99`-prefixed WebRTC mirror for each extension when WebRTC is enabled via
the UCP/WebRTC module. Example: extension `1089` → mirror `991089`.

| Parameter                      | Base (`1089`) | WebRTC mirror (`991089`)             |
|--------------------------------|---------------|--------------------------------------|
| `webrtc`                       | `no`          | `yes`                                |
| `use_avpf`                     | `no`          | `yes`                                |
| `media_encryption`             | `no`          | `dtls`                               |
| `media_use_received_transport` | `no`          | `yes`                                |
| `aggregate_mwi`                | `yes`         | `no`                                 |
| `timers`                       | `yes`         | `no`                                 |
| `user_eq_phone`                | `yes`         | `no`                                 |
| `dtls_verify`                  | —             | `fingerprint`                        |
| `dtls_setup`                   | —             | `actpass`                            |
| `dtls_cert_file`               | —             | `/etc/asterisk/keys/default.crt`     |
| `dtls_private_key`             | —             | `/etc/asterisk/keys/default.key`     |
| auth password                  | plaintext     | MD5 hashed                           |

No `transport=` is set on either endpoint — Asterisk auto-selects based on the incoming connection
(UDP phone → UDP transport, WebRTC browser → WSS transport).

**Trigger**: `FreePBX::Webrtc()->ucpUpdateUser($usermanID, 'userman', 'enabled', [])` — this is
what `public/pbxCore.php` calls after `bulkhandlerImport`.

---

## 7. WebRTC via API — Not Possible

The FreePBX GraphQL API has **no `enableWebRTC` mutation or field**. The WebRTC module's UCP
settings are not exposed via API at all.

| Task                                        | Via GraphQL API?  |
|---------------------------------------------|-------------------|
| Create base extension                       | ✅ `addExtension`  |
| Set SIP password                            | ✅ `updateExtension` with `extPassword` |
| Enable WebRTC / generate `99`-prefix mirror | ❌ Not possible   |
| Set individual PJSIP advanced params        | ❌ Not possible   |

---

## 8. How This Codebase Bridges the Gap

| Task                      | Mechanism                                                                          |
|---------------------------|------------------------------------------------------------------------------------|
| Create base extension     | `ExtensionManager::createExtension()` → GraphQL `addExtension` + `updateExtension` |
| Set SIP password          | Step 2 of above → GraphQL `updateExtension` with `extPassword`                     |
| Disable call waiting      | `ExtensionManager::disableCallWaiting()` → Asterisk AMI rawman (`http://127.0.0.1:8088/rawman`) |
| Enable WebRTC (`99`-mirror) | `public/pbxCore.php` → native FreePBX PHP bootstrap → `bulkhandlerImport` + `ucpUpdateUser` |
| Apply config              | `ExtensionManager::applyChanges()` → GraphQL `doreload`                            |

`pbxCore.php` exists because FreePBX **cannot be bootstrapped inside a Laravel process** — FreePBX
and Laravel define conflicting PHP function names, causing fatal errors. The workaround is an
isolated PHP process reached over HTTP on `127.0.0.1`.

### SSL Considerations for `pbxCore.php`

When `BAP_FORCE_SSL=true`, Apache redirects all `http://127.0.0.1` requests to `https://127.0.0.1`.
The old `file_get_contents` implementation silently received the 301 and never executed the script.
The fixed implementation uses Laravel's `Http::` facade with `withoutVerifying()`, which:

1. Follows the redirect automatically.
2. Accepts the self-signed localhost certificate (`withoutVerifying()` — safe for loopback).
3. Reads the base URL from `config('pbx.core_url')` (`PBX_CORE_URL` env variable, default
   `http://127.0.0.1`) so it can be overridden without a code change.

---

## 9. Persistent Custom Config Files

FreePBX **completely regenerates** its `pjsip.*.conf` files on every `fwconsole reload`. The
`_custom` variants survive all reloads:

| File                                          | Included at              | Use for                    |
|-----------------------------------------------|--------------------------|----------------------------|
| `/etc/asterisk/pjsip.endpoint_custom_post.conf` | Bottom of endpoint file | Custom endpoint stanzas    |
| `/etc/asterisk/pjsip.aor_custom_post.conf`    | Bottom of AOR file       | Custom AOR stanzas         |
| `/etc/asterisk/pjsip.auth_custom_post.conf`   | Bottom of auth file      | Custom auth stanzas        |
| `/etc/asterisk/pjsip.transports_custom.conf`  | Top of transports file   | Extra transports           |
| `/etc/asterisk/extensions_custom.conf`        | FreePBX dialplan         | Custom dialplan contexts   |
| `/etc/asterisk/pjsip_custom.conf`             | Top of `pjsip.conf [0]`  | Global PJSIP overrides     |

The FreePBX DB route (via `pbxCore.php`) is preferred — it keeps the extension visible and manageable
in the FreePBX GUI, whereas `_custom_post.conf` entries are invisible to FreePBX.

---

## 10. Artisan Commands (PBX-related)

| Command                  | File                                                   | Purpose                                                  |
|--------------------------|--------------------------------------------------------|----------------------------------------------------------|
| `pbx:generate-credentials` | `app/Console/Commands/PBX/GenerateCredentialsCommand.php` | Generates OAuth2 credentials, writes to `asterisk.api_applications` |
| `pbx:test-connection`    | `app/Console/Commands/PBX/TestConnectionCommand.php`   | Connectivity check via `ExtensionManager::checkConnection()` |
| `pbx:set-wss`            | `app/Console/Commands/PBX/SetWssSettingsCommand.php`   | Sets WebSocket URL + port in company settings            |
| `pbx:export`             | `app/Console/Commands/ExportPbxComponents.php`         | Exports IVR, queues, trunks, routes to zip               |
| `pbx:import`             | `app/Console/Commands/ImportPbxComponents.php`         | Imports from `pbx:export` zip                            |
| `egy:dialplan`           | `app/Console/Commands/ApplyDialplan.php`               | Copies `dialplan/` → `/etc/asterisk/`, runs `fwconsole reload` |
| `trunk:inject`           | `app/Console/Commands/TrunkInjectCommand.php`          | Direct DB insert of a trunk                              |
| `trunk:remove`           | `app/Console/Commands/TrunkRemoveCommand.php`          | Removes trunk from DB                                    |

---

## 11. Key Service Classes

| Class                  | Path                                        | Role                                                        |
|------------------------|---------------------------------------------|-------------------------------------------------------------|
| `PBXApiClient`         | `app/Services/PBX/PBXApiClient.php`         | OAuth2 token management + GraphQL + REST HTTP client        |
| `ExtensionManager`     | `app/Services/PBX/ExtensionManager.php`     | High-level extension CRUD (implements `PBXServiceInterface`)|
| `PBXCredentialManager` | `app/Services/PBX/PBXCredentialManager.php` | Credential generation and lifecycle                         |
| `PBXAdminApiClient`    | `app/Services/PBX/PBXAdminApiClient.php`    | Admin HTTP Basic Auth client (managing REST users)          |
| `CreateExtensionJob`   | `app/Jobs/PBX/CreateExtensionJob.php`       | Queued job (`pbx` queue) wrapping `createExtension`         |
| `UpdateExtensionJob`   | `app/Jobs/PBX/UpdateExtensionJob.php`       | Queued job wrapping `updateExtension`                       |
| `DeleteExtensionJob`   | `app/Jobs/PBX/DeleteExtensionJob.php`       | Queued job wrapping `deleteExtension`                       |
