Connect your CRM to your Cloud PBX. Pull every call and its recording automatically, and get a webhook within seconds of each call ending.
https://pbx.cloudtools.co.za/api/v1Authorization: Bearer <key>JSON · UTF-8PBX API Access · R100/mocurl https://pbx.cloudtools.co.za/api/v1/calls \
-H "Authorization: Bearer YOUR_API_KEY"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.
| Method | Path | Purpose |
|---|---|---|
| GET | /calls | List calls by date, or fetch new ones since your last check |
| GET | /calls/{id} | One call |
| GET | /calls/{id}/recording | Download the recording audio |
| GET | /webhooks | List your webhooks and their delivery health |
| POST | /webhooks | Register a webhook |
| PATCH | /webhooks/{id} | Change the URL, pause or resume |
| DELETE | /webhooks/{id} | Remove a webhook |
| POST | /webhooks/{id}/test | Send a test delivery now |
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"
}
| Field | Meaning |
|---|---|
id | Unique call ID. IDs only ever increase, so the last one you've seen works as a bookmark. |
direction | inbound, outbound or local (extension to extension). |
extension | The extension on your PBX that made or took the call. |
caller_numbercalled_number | International format without the plus sign (27…). |
started_atanswered_atended_at | ISO 8601, South African time. answered_at is null for unanswered calls. |
duration_seconds | Talk time from answer to hang-up. 0 if unanswered. |
answered | true if the call was answered. |
hangup_cause | Why it ended: NORMAL_CLEARING, NO_ANSWER, USER_BUSY, CALL_CANCELLED… |
recording_url | Where to download the recording, or null if the call wasn't recorded. |
Lists calls newest first, 100 per page by default.
| Parameter | Description |
|---|---|
start_date end_date | Date range, YYYY-MM-DD. Defaults to the last 7 days. |
direction | inbound, outbound or local. |
number | Calls to or from this number (partial match). |
since_id | Polling mode: every call after this ID, oldest first. See below. |
per_page page | Paging. per_page up to 500. |
GET /calls?start_date=2026-10-01&end_date=2026-10-12&direction=inbound
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 }
}
Returns a single call object in data.
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
Recordings need call recording on your PBX. has_recording tells you up front whether there is audio to fetch.
Register an HTTPS address and we'll POST every completed call on your account to it, usually within a few seconds of hang-up.
{ "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.
Send url and/or enabled. {"enabled": false} pauses deliveries. {"enabled": true} resumes from that moment.
Sends your most recent call to the webhook immediately as event webhook.test, and returns the status your endpoint replied with. Handy while building.
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.
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.
$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;
}
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
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
If your endpoint is down or doesn't reply 2xx, we try again on this schedule, then give up on that call:
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.
| Status | Meaning |
|---|---|
| 200 201 | Success. |
| 401 | Missing or invalid API key. |
| 404 | Not found, or not on your account. |
| 422 | Invalid input. The response says what's wrong. Webhook URLs must be HTTPS and publicly reachable. |
| 5xx | Something went wrong on our side. Retry shortly. |
Fair use: poll /calls no more than once a minute. Use webhooks if you need calls sooner.
Add PBX API Access to your account and we'll set up your API key.
Contact support