PearlPBX2

PearlPBX2 REST API

All endpoints are available under the /api/v1/ prefix.

Access Control

Authentication uses DRF token-based authentication. Send the header Authorization: Token <key> with every request.

Requests without a valid token receive HTTP 401 Unauthorized.

Create a token:

python manage.py drf_create_token <username>

Or via Django admin / shell (rest_framework.authtoken.models.Token).

There is no session-based or IP-based restriction. CSRF protection is not enforced (token auth only).

Common Response Format

Success: JSON object or array with resource fields. GET list endpoints return paginated responses:

{
  "count": 42,
  "next": "http://host/api/v1/blacklist/?page=2",
  "previous": null,
  "results": [...]
}

Validation errors use DRF’s standard format:

{"field_name": ["message"]}

HTTP status codes:


Blacklist

Manages a list of caller IDs to block in Asterisk dialplan.

POST uses upsert logic: if a record with the same callerid + destination already exists, it is updated; otherwise a new record is created.

GET /api/v1/blacklist/

Returns paginated blacklist entries.

Response:

{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "id": "uuid",
      "callerid": "+380501234567",
      "destination": "101",
      "reason": "Spam",
      "expiration_date": "2026-12-31T23:59:59"
    }
  ]
}

POST /api/v1/blacklist/

Add or update a blacklist entry.

Request body:

{
  "callerid": "+380501234567",
  "destination": "101",
  "reason": "Spam",
  "expiration_date": "2026-12-31T23:59:59"
}

Uniqueness is enforced on the callerid + destination pair (a common value is destination = "" for system-wide blocking).

Response: 201 on create, 200 on update. Updating a record’s callerid/destination (via PUT/PATCH on /api/v1/blacklist/<uuid>/) so that it collides with another existing pair returns 409 Conflict.

DELETE /api/v1/blacklist/<uuid>/

Delete a blacklist entry by ID.

Response: 204 No Content (empty body).


Whitelist

Manages a list of caller IDs to allow or route in Asterisk dialplan. Identical interface to Blacklist.

GET /api/v1/whitelist/

POST /api/v1/whitelist/

DELETE /api/v1/whitelist/<uuid>/

Same request/response format as Blacklist.


Contacts

A directory of caller IDs with display names. Used for caller ID name resolution.

POST uses upsert logic keyed on callerid.

GET /api/v1/contacts/

Returns paginated contacts.

Response:

{
  "count": 1,
  "results": [
    {
      "id": "uuid",
      "callerid": "+380501234567",
      "name": "Ivan Petrenko"
    }
  ]
}

POST /api/v1/contacts/

Add or update a contact.

Request body:

{
  "callerid": "+380501234567",
  "name": "Ivan Petrenko"
}

Both fields are required.

Response: 201 on create, 200 on update.

DELETE /api/v1/contacts/<uuid>/

Delete a contact by ID. Returns 204 No Content.


Custom Lists

Named lists with entries. Useful for dynamic dialplan lookups (e.g. VIP callers, routing groups).

Each list has a name and contains entries with callerid, optional destination, reason, and expiration_date.

GET /api/v1/lists/

Returns all custom lists.

Response:

{
  "count": 1,
  "results": [
    {"id": "uuid", "name": "VIP"}
  ]
}

POST /api/v1/lists/

Create a new list.

Request body:

{"name": "VIP"}

Response: HTTP 201 with the created list object.

PATCH /api/v1/lists/<uuid>/

Rename a list.

Request body:

{"name": "New Name"}

name must be a non-empty string. An empty or missing name returns 400 with body {"error": "Missing \"name\""}.

DELETE /api/v1/lists/<uuid>/

Delete a list and all its entries. Returns 204 No Content.


GET /api/v1/lists/<uuid>/entries/

Returns paginated entries of a specific list.

Response:

{
  "count": 1,
  "results": [
    {
      "id": "uuid",
      "callerid": "+380501234567",
      "destination": "101",
      "reason": "VIP caller",
      "expiration_date": null
    }
  ]
}

POST /api/v1/lists/<uuid>/entries/

Add an entry to a list.

Request body:

{
  "callerid": "+380501234567",
  "destination": "101",
  "reason": "VIP caller",
  "expiration_date": "2026-12-31T23:59:59"
}

Response: HTTP 201 with the created entry object.

DELETE /api/v1/lists/<uuid>/entries/<entry_uuid>/

Delete a specific entry from a list. Returns 204 No Content.


Originate a call

POST /api/v1/calls/originate/

Originate a call via Asterisk AMI. Replaces direct HTTP calls to Asterisk rawman — the AMI secret is never sent by the client.

Request body:

Field Type Required Default Description
channel string yes First leg to dial, e.g. "Local/0503856087@default" or "PJSIP/101"
exten string yes Extension/number the first leg connects to, e.g. "0675653380"
context string no "default" Dialplan context
priority integer no 1 Dialplan priority (min: 1)
callerid string no Caller ID in format name<number>, e.g. "380443333333<0675653380>"
variable object no Channel variables as key-value pairs, e.g. {"userId": "0"}
timeout_ms integer no 30000 Asterisk-side Originate timeout in ms (1000–120000). This is also the server-side wait budget: the API worker will not block waiting for the AMI response longer than timeout_ms + 5s.

Responses:

Status Meaning
200 Call originated successfully. Body: {"status": "originated", "message": "..."}
400 Invalid request body (missing required fields, validation errors)
401 Authentication credentials were not provided
502 AMI error or Asterisk unreachable
503 Asterisk is disabled in this DEVMODE

Example:

curl -X POST http://127.0.0.1:8000/api/v1/calls/originate/ \
  -H "Authorization: Token <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "channel": "Local/0503856087@default",
        "exten": "0675653380",
        "context": "default",
        "callerid": "380443333333<0675653380>",
        "variable": {"userId": "0"}
      }'

Mapping from old rawman parameters:

rawman param API field
channel channel
exten exten
context context
priority priority
Variable variable (dict)
callerId callerid

Queue Members

Pause/unpause a queue member and read live queue member status via Asterisk AMI. There is no persistent “queue member” resource here — these endpoints talk to Asterisk directly, so they reflect (and change) live runtime state, not the QueueMember records managed in Django admin.

POST /api/v1/queues/members/pause/

Pause or unpause a queue member.

Request body:

Field Type Required Default Description
interface string yes Queue member interface, e.g. "PJSIP/101"
paused boolean yes true to pause, false to unpause
queue string no Limit the change to one queue. Omitted, it applies to the member in every queue it belongs to

Responses:

Status Meaning
200 Pause state updated. Body: {"status": "paused"} or {"status": "unpaused"}
400 Invalid request body
401 Authentication credentials were not provided
404 The interface is not a member of the given queue(s)
502 AMI error or Asterisk unreachable
503 Asterisk is disabled in this DEVMODE

Example:

curl -X POST http://127.0.0.1:8000/api/v1/queues/members/pause/ \
  -H "Authorization: Token <your-token>" \
  -H "Content-Type: application/json" \
  -d '{"interface": "PJSIP/101", "paused": true}'

GET /api/v1/queues/members/

List queue members and their current status. Optional ?queue=<name> query parameter limits results to one queue; omit it to list members of every queue.

Response:

{
  "members": [
    {
      "queue": "support",
      "name": "PJSIP/101",
      "location": "PJSIP/101",
      "state_interface": "PJSIP/101",
      "membership": "static",
      "penalty": 0,
      "calls_taken": 3,
      "last_call": 0,
      "in_call": false,
      "status": "1",
      "paused": true
    }
  ]
}

status is the raw Asterisk device-state code (AST_DEVICE_*), passed through without translation.

Responses:

Status Meaning
200 List of members (possibly empty)
401 Authentication credentials were not provided
502 AMI error or Asterisk unreachable
503 Asterisk is disabled in this DEVMODE

Call Recordings

GET /api/v1/recordings/<uniqueid>/

Fetch a recorded call’s audio by Asterisk uniqueid. This is the endpoint referenced by recording_url in CRM webhook payloads — see CRM integration guide (Ukrainian) for the full webhook reference. Supports HTTP Range requests for streaming/seeking, and a ?download=1 query parameter to force a Content-Disposition: attachment response.

Responses:

Status Meaning
200 / 206 The audio file (audio/wav or audio/mpeg), full or partial (Range)
401 Authentication credentials were not provided
404 No recording exists for this uniqueid (not recorded, or not yet written to disk)

Example:

curl -H "Authorization: Token <your-token>" \
  http://127.0.0.1:8000/api/v1/recordings/1753000000.42/ \
  -o call.wav

Access is not scoped further — any valid API token can fetch any recording, the same as the rest of this API.


Known Limitations


API Documentation

Interactive, auto-generated OpenAPI 3.0 documentation is available (powered by drf-spectacular). All three endpoints require authentication (a valid token or an authenticated session), the same as the rest of the API.

Note: These three endpoints require authentication (Authorization: Token <key>), just like the rest of the API. Opening them directly in a browser without a token returns 401. This is intentional — the documentation is not publicly exposed.

The schema is generated directly from the DRF ViewSets and serializers, so it always matches the running code.