An HTTP API for FusionPBX.
To install it, clone into fusionpbx's app/ folder. Make sure this repo clones into a folder called rest_api.
Then log into the FusionPBX web interface, select Advanced -> Upgrade, check Schema, Menu Defaults and Permission Defaults, press Execute. (Permission Defaults is what gives the superadmin group the rest_api_key_* permissions.)
This version targets FusionPBX 5.6.5.
Every request is made with an API key, sent with HTTP Basic auth as <key id>:<secret>. Each key is bound to a FusionPBX user, and the request runs as that user. It has the permissions of the user's groups, acts on the user's domain, and records it creates show that user as insert_user.
Keys are managed under the REST API app in the FusionPBX menu. Managing keys needs the rest_api_key_view, rest_api_key_add, rest_api_key_edit and rest_api_key_delete permissions, which superadmins have by default. Anyone who can add or edit keys can bind a key to any user, including a superadmin, and use the API as that user. Only give these permissions to superadmins.
A key authenticates only when all of these hold:
- the key is enabled
- the key hasn't expired (an empty expiry means never)
- its user exists and is enabled
- the user's domain is enabled
Otherwise the request gets 401 {"error": "unauthorized"}.
The secret is shown once, when the key is created. If you lose it, create a new key and delete the old one.
Recommended setup: create one user for each integration, in a custom group that only has the permissions of the actions the integration uses (see the table below). Bind the integration's key to that user.
The API endpoint is shown on the API key page, usually https://<your fusionpbx>/app/rest_api/rest.php. Every request is an HTTP POST with a JSON body. The body's action parameter names the action (see below). For example, the domain-details action with domain_name=fusionpbx.example.net:
$ curl -s --user "5bc14e83-fc4e-4578-99b8-c7151eb2ec54:jM2GQuYgQTkIGE6nJ2SP" -d '{"action": "domain-details", "domain_name": "fusionpbx.example.net"}' https://fusionpbx.example.net/app/rest_api/rest.php | jq
{
"domain_uuid": "3a644e67-de8f-4798-b07e-6f22c33a656e",
"domain_parent_uuid": null,
"domain_name": "fusionpbx.example.net",
"domain_enabled": true,
"domain_description": ""
}
Each action needs these FusionPBX permissions in the key user's groups:
| Action | Permissions |
|---|---|
cdr-list |
xml_cdr_view |
destination-create |
destination_add, dialplan_add, dialplan_detail_add |
destination-details |
destination_view |
domain-details |
none |
extension-create |
extension_add, voicemail_add (extension_password to also get the SIP password back) |
extension-details |
extension_view |
extension-list |
extension_view |
originate |
click_to_call_call |
ringgroup-create |
ring_group_add, ring_group_destination_add, dialplan_add |
A missing permission returns 403 {"error": "forbidden", "missing_permissions": [...]}.
domain_uuid is optional on every action. Without it, the action acts on the key user's own domain. Acting on another domain needs the FusionPBX domain_select permission. Without it, any other domain_uuid returns 403 {"error": "forbidden"}. With it, a domain that doesn't exist or is disabled returns 404 {"error": "domain not found"}.
Lookups never return records of domains the user can't act on: they answer 404 as if the record didn't exist. One exception: destination-details without domain_uuid, by a user with domain_select, searches every domain. Use it to find the domain an inbound number belongs to.
| Status | Meaning |
|---|---|
| 400 | Invalid body, unknown action, missing or invalid parameter (missing_parameters lists missing ones) |
| 401 | Unusable key (see above) |
| 403 | The user lacks a permission (missing_permissions), or the request is for another domain without domain_select |
| 404 | Record or domain not found |
| 500 | Server error, or an action that doesn't declare its permissions |
Other FusionPBX apps can expose actions through an app_api.php file (call them with "app": "<app name>"). Each such action file must declare the FusionPBX permissions it needs, for example $required_permissions = array("my_app_view");, or array() for none. Actions that don't declare them aren't run. do_action() receives the request body and, as an optional second argument, the context: domain_explicit, cross_domain and user_domain_uuid. Do all the work inside do_action(): top-level code in the action file runs before rest.php checks $required_permissions.
This version changes how keys work:
- After upgrading, run Advanced → Upgrade → Schema, Menu Defaults and Permission Defaults, then log out and back in (permissions are cached in the session) before editing keys. Existing keys stop working until a superadmin edits each one, picks a user and enables it.
- Each integration's user needs the permissions listed above.
- Responses only contain the documented columns.
extension-createno longer returns the SIP password unless the user hasextension_password. Anything that read other columns fromextension-create,destination-create,destination-details,ringgroup-createordomain-detailsmust be updated. domain_uuidis now optional. It defaults to the key user's domain.
All actions are defined in the actions/ directory of this repo. What follows is a best effort attempt to document them.
| Parameter | Required | Description |
|---|---|---|
domain_uuid |
no | Domain to act on. Defaults to the key user's domain |
number |
yes | Phone number to add |
extension |
yes | Extension to transfer calls for this number to |
Creates a new destination in FusionPBX.
| Parameter | Required | Description |
|---|---|---|
number |
yes | Inbound number to look up |
domain_uuid |
no | Domain to search. Defaults to the key user's domain; users with domain_select who leave it out search every domain |
looks up details for a particular destination
| Parameter | Required | Description |
|---|---|---|
domain_uuid |
no | UUID of the domain to look up. |
domain_name |
no | Name of the domain to look up, used when domain_uuid is not given. |
looks up details of a domain. Mostly useful for converting between domain uuid and domain name. With neither parameter it returns the key user's domain.
| Parameter | Required | Description |
|---|---|---|
domain_uuid |
no | Domain to act on. Defaults to the key user's domain |
extension |
yes | Extension (number) to create |
caller_id_name |
no | Caller ID name to set for outbound calls from the extension |
caller_id_number |
no | Caller ID number to set for outbound calls from the extension |
create an extension
Returns the extension's details, plus its SIP password when the key user has extension_password.
| Parameter | Required | Description |
|---|---|---|
domain_uuid |
no | Domain to act on. Defaults to the key user's domain |
extension_uuid |
yes | Extension (by UUID) to look up |
get all details of an extension
| Parameter | Required | Description |
|---|---|---|
domain_uuid |
no | Domain to act on. Defaults to the key user's domain |
List number, UUID and a few other details of all extensions on a given domain.
| Parameter | Required | Description |
|---|---|---|
domain_uuid |
no | Domain to act on. Defaults to the key user's domain |
name |
yes | name for the ring group |
extension |
yes | Extension to route TO the ring group |
destinations |
yes | JSON array of extensions to send calls from the ring group. Example: [{"number": "100"}, {"number": "101"}, {"number": "102"}] |
strategy |
yes | one of: simultaneous, sequence, enterprise, rollover or random |
Create a ring group
| Parameter | Required | Description |
|---|---|---|
domain_uuid |
no | Domain to act on. Defaults to the key user's domain |
caller_id_number |
yes | caller ID number to display for both legs of the call |
caller_id_name |
no | an optional caller ID name to request. typically will be delivered for internal calls and stripped by the upstream provider for external calls |
destination_a |
yes | the number to call first |
destination_b |
yes | the number to call second |
Call one number (destination_a) and connect the call to another number (destination_b) when it's picked up. The selected domain's internal dialplan is used, so internal extensions may be dialed.
Note that the call is ended when destination_a ends the call, so if one leg isn't expected to hang up, make it destination_b.
Use destination_b=*9664 to indefinitely play hold music to destination_a.
| Parameter | Required | Description |
|---|---|---|
domain_uuid |
no | Domain to act on. Defaults to the key user's domain |
returns the the last 100 call detail records.
The tests need PHP 8.3 or newer and Composer. They don't need FusionPBX: tests/Support/fusionpbx/ provides stand-ins for the FusionPBX functions and classes the plugin uses, including a small in-memory database.
composer install
composer test
tests/Unit: thelib/helpers and every action, each test in its own PHP process.tests/Http:rest.phpand the key management pages, served by PHP's built-in web server from a temporary FusionPBX-like document root.
To check that the tests catch a regression, run them against another checkout of the plugin, for example an older commit:
git worktree add /tmp/rest_api_old <commit>
PLUGIN_DIR=/tmp/rest_api_old composer test
composer coverage runs the suite with Xdebug in coverage mode and writes an HTML report to build/coverage/html/index.html. It merges the unit tests' coverage with the lines run by requests to the HTTP test server, so rest.php, index.php and key_edit.php are included.
Xdebug must be installed (e.g. sudo apt install php8.5-xdebug). The script turns on coverage mode itself. Without Xdebug locally, use a Docker image that has it, e.g. a ddev web image:
docker run --rm -u $(id -u):$(id -g) -e HOME=/tmp -v $PWD:/app -w /app --entrypoint bash ddev/ddev-webserver:<tag> -c '
mkdir -p /tmp/ini && printf "zend_extension=xdebug\nopcache.enable_cli=0\n" > /tmp/ini/xdebug.ini
PHP_INI_SCAN_DIR=":/tmp/ini" XDEBUG_MODE=coverage composer coverage'
The test stand-ins refuse to run when requested through a web server outside the test suite, but vendor/ and tests/ don't need to be deployed to FusionPBX.