API Documentation

● REST API · v1

Connect your CRM to your Cloud PBX. Pull every call and its recording automatically, and get a webhook within seconds of each call ending.

Call historyCaller, number dialled, extension, times, talk time and outcome for every call.
RecordingsDownload the audio for any recorded call straight into your system.
WebhooksA signed notification to your CRM the moment a call ends, with retries built in.
Base URLhttps://pbx.cloudtools.co.za/api/v1
AuthAuthorization: Bearer <key>
FormatJSON · UTF-8
PlanPBX API Access · R100/mo

Quick start

  1. Get your API keyAdd PBX API Access to your account and our support team will issue your key. A key only ever sees your own account's calls.
  2. Make your first request
    curl https://pbx.cloudtools.co.za/api/v1/calls \
      -H "Authorization: Bearer YOUR_API_KEY"
  3. Register a webhookTell us where to send calls as they end:
    curl -X POST https://pbx.cloudtools.co.za/api/v1/webhooks \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"url": "https://crm.example.com/hooks/calls"}'
!

Keep your key secret. Use it only from your server, never in a browser or mobile app. If you think it has been exposed, contact support and we'll replace it immediately.

Endpoints

MethodPathPurpose
GET/callsList calls by date, or fetch new ones since your last check
GET/calls/{id}One call
GET/calls/{id}/recordingDownload the recording audio
GET/webhooksList your webhooks and their delivery health
POST/webhooksRegister a webhook
PATCH/webhooks/{id}Change the URL, pause or resume
DELETE/webhooks/{id}Remove a webhook
POST/webhooks/{id}/testSend a test delivery now

The call object

Every endpoint and every webhook describes a call the same way.

{
  "id": 411971,
  "domain": "yourcompany.cloudtools.co.za",
  "direction": "inbound",
  "extension": "101",
  "caller_number": "27821234567",
  "called_number": "27101011200",
  "started_at": "2026-10-12T17:30:50+02:00",
  "answered_at": "2026-10-12T17:30:56+02:00",
  "ended_at": "2026-10-12T17:34:12+02:00",
  "duration_seconds": 196,
  "answered": true,
  "hangup_cause": "NORMAL_CLEARING",
  "has_recording": true,
  "recording_url": "https://pbx.cloudtools.co.za/api/v1/calls/411971/recording"
}
FieldMeaning
idUnique call ID. IDs only ever increase, so the last one you've seen works as a bookmark.
directioninbound, outbound or local (extension to extension).
extensionThe extension on your PBX that made or took the call.
caller_number
called_number
International format without the plus sign (27…).
started_at
answered_at
ended_at
ISO 8601, South African time. answered_at is null for unanswered calls.
duration_secondsTalk time from answer to hang-up. 0 if unanswered.
answeredtrue if the call was answered.
hangup_causeWhy it ended: NORMAL_CLEARING, NO_ANSWER, USER_BUSY, CALL_CANCELLED…
recording_urlWhere to download the recording, or null if the call wasn't recorded.

Calls & recordings

GET /calls

Lists calls newest first, 100 per page by default.

ParameterDescription
start_date end_dateDate range, YYYY-MM-DD. Defaults to the last 7 days.
directioninbound, outbound or local.
numberCalls to or from this number (partial match).
since_idPolling mode: every call after this ID, oldest first. See below.
per_page pagePaging. per_page up to 500.
GET /calls?start_date=2026-10-01&end_date=2026-10-12&direction=inbound

GET /calls?since_id={id}

The simplest way to keep a CRM in sync without webhooks. Save meta.last_id and send it back next time.

{
  "success": true,
  "data": [ /* call objects, oldest first */ ],
  "meta": { "count": 3, "last_id": 411974, "has_more": false }
}

GET /calls/{id}

Returns a single call object in data.

GET /calls/{id}/recording

Streams the audio file (normally MP3) as a download. Returns 404 if the call has no recording.

curl -H "Authorization: Bearer YOUR_API_KEY" \
  -o call-411971.mp3 \
  https://pbx.cloudtools.co.za/api/v1/calls/411971/recording
i

Recordings need call recording on your PBX. has_recording tells you up front whether there is audio to fetch.

Webhooks

Register an HTTPS address and we'll POST every completed call on your account to it, usually within a few seconds of hang-up.

POST /webhooks

{ "url": "https://crm.example.com/hooks/calls" }
{
  "success": true,
  "data": {
    "id": "306f3bad-8fee-440c-a1e1-b4dea1da24dc",
    "url": "https://crm.example.com/hooks/calls",
    "events": ["call.ended"],
    "enabled": true,
    "secret": "whsec_…"
  }
}
!

The secret is shown only once. Store it. You'll need it to verify deliveries. Up to 5 webhooks per account. A new webhook only receives calls that end after it's registered. Use GET /calls for history.

PATCH /webhooks/{id}

Send url and/or enabled. {"enabled": false} pauses deliveries. {"enabled": true} resumes from that moment.

POST /webhooks/{id}/test

Sends your most recent call to the webhook immediately as event webhook.test, and returns the status your endpoint replied with. Handy while building.

What we send

POST /hooks/calls
Content-Type: application/json
X-Webhook-Event: call.ended
X-Webhook-Id: 18734
X-Webhook-Timestamp: 1791820217
X-Webhook-Signature: sha256=5f1c…

{
  "event": "call.ended",
  "sent_at": "2026-10-12T17:34:15+02:00",
  "call": { /* the call object */ }
}
✓

Reply with any 2xx within 10 seconds. Do slow work, like downloading the recording, after you've responded.

Verify the signature

Compute an HMAC-SHA256 of timestamp + "." + raw body with your webhook secret and compare it with X-Webhook-Signature. Reject timestamps more than 5 minutes old.

PHP PHP
$body = file_get_contents('php://input');
$ts   = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'];
$sig  = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $secret);

if (!hash_equals($sig, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE']) || abs(time() - $ts) > 300) {
    http_response_code(401); exit;
}
PY Python
import hmac, hashlib, time

expected = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
valid = hmac.compare_digest(expected, signature) and abs(time.time() - int(ts)) < 300
JS Node.js
const crypto = require('crypto')

const expected = 'sha256=' + crypto.createHmac('sha256', secret)
  .update(`${ts}.` + rawBody).digest('hex')
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
  && Math.abs(Date.now() / 1000 - Number(ts)) < 300

Retries

If your endpoint is down or doesn't reply 2xx, we try again on this schedule, then give up on that call:

now→30 s→2 min→10 min→30 min→2 h→6 h

X-Webhook-Id stays the same across retries, so you can safely ignore duplicates. A webhook that fails 50 times in a row is paused. Fix the endpoint and re-enable it with PATCH.

Errors

StatusMeaning
200 201Success.
401Missing or invalid API key.
404Not found, or not on your account.
422Invalid input. The response says what's wrong. Webhook URLs must be HTTPS and publicly reachable.
5xxSomething went wrong on our side. Retry shortly.
i

Fair use: poll /calls no more than once a minute. Use webhooks if you need calls sooner.

Ready to connect your CRM?

Add PBX API Access to your account and we'll set up your API key.

Contact support