4phone Client Calls API
This guide describes the production API for exporting calls and call recordings, configuring API keys, handling webhook events, and following the recommended integration workflow for your CRM, ERP, BI, or internal system.
1. Capabilities
The integration supports:
- REST API for initiating outbound calls from a CRM.
- REST API for querying the call list, single call details, and call recording files.
- Webhooks for real-time notifications when an inbound call starts ringing, when it completes and when its recording file becomes ready.
Recommended integration workflow:
- Receive new events via webhooks.
- Store each call in your database keyed by
id. - Download the recording after receiving
CALL_RECORDING_READY. - Periodically reconcile data via the REST API to recover any events missed while your webhook endpoint was unavailable.
2. Endpoints and Dashboard URLs
REST API base URL:
https://api.4phone.uz/api/v1/external
API Keys management page:
https://4phone.uz/dashboard/settings/integrations/api-keys
Webhooks management page:
https://4phone.uz/dashboard/settings/integrations/webhooks
Webhook delivery history page:
https://4phone.uz/dashboard/settings/integrations/deliveries
3. Creating an API Key
Users with the OWNER or ADMIN role can create and revoke API keys.
Setup steps:
- Open
Settings. - Navigate to
Integrations. - Open
API Keys. - Click
Create key. - Enter a descriptive name, such as
Bitrix exportorBI production. - Select the
calls:readscope. - Select the
calls:createscope if your CRM needs to initiate click-to-call outbound calls. - Select the
recordings:readscope if your integration needs to download recording files. - Optionally restrict the key by department, IP addresses, and expiration date.
- Create the key and immediately save the displayed secret.
The full API key value is displayed only once. A lost secret cannot be recovered—you must revoke the old key and create a new one.
We recommend creating a separate API key for each external system so you can independently manage scopes, IP restrictions, and expiration dates.
3.1. Scopes
Three permission scopes are used for the Calls API:
calls:readgrants access to the list and details of completed calls.calls:creategrants permission to initiate outbound calls from a CRM.recordings:readgrants permission to check and download call recording files.
A key with only recordings:read can download a recording using a known call
id, but cannot query the call list. Initiating a call from a CRM requires the
separate calls:create scope; calls:read alone is not sufficient to start a
call.
3.2. Department Restriction
If the Department field is left empty, the API key has access to all eligible
calls across your tenant.
If a department is selected, the API returns only calls involving extensions in
that department. Filtering by an extension from another department returns an
empty list, and requesting details for a call from another department returns
404.
Department restrictions also apply to downloading recordings and initiating
outbound calls: attempting to start a call from an extension in another
department returns 404.
3.3. IP Whitelist
The IP whitelist field accepts:
- a single IPv4 address, for example
45.147.253.34; - an IPv4 CIDR subnet, for example
45.147.253.32/27; - a single IPv6 address;
- an IPv6 subnet in CIDR notation.
Multiple entries can be separated by commas, spaces, or newlines. If the list is empty, no IP restriction is applied.
Specify the public outbound IP address of the server making requests to the API.
If the request originates from an unlisted IP, the API returns 403 with the
error code api_key.ip_not_allowed.
3.4. Expiration and Revocation
Setting an expiration date is optional, but recommended for temporary integrations and staging environments.
A revoked or expired key is immediately rejected. Revocation is permanent and irreversible. Both key creation and revocation are recorded in the platform audit log.
4. Request Authentication
Recommended HTTP header:
X-Api-Key: cpbx_XXXXXXXX_SECRET
Bearer authentication is also supported:
Authorization: Bearer cpbx_XXXXXXXX_SECRET
Never pass the API key:
- in URL query parameters;
- in file names;
- in client-side browser JavaScript;
- in error messages or unredacted logs.
Store the API key in a secret manager or environment variable:
export FOURPHONE_API_KEY='cpbx_XXXXXXXX_SECRET'
Verify access:
curl -sS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
"https://api.4phone.uz/api/v1/external/calls?limit=1"
5. Listing Calls
GET /calls
Full URL:
https://api.4phone.uz/api/v1/external/calls
Required scope: calls:read.
The API returns only completed inbound and outbound calls. Internal extension-to-extension calls are excluded from the client export.
Available call statuses:
ANSWEREDBUSYNO_ANSWERFAILEDCANCELLED
5.1. Query Filters
Supported query parameters:
directionwith valueINBOUNDorOUTBOUND;statuswith a comma-separated list of statuses;startedFromwith the start timestamp in RFC 3339 format (inclusive);startedTowith the end timestamp in RFC 3339 format (exclusive);phonewith a full or partial phone number;extensionNumberwith an internal extension number;hasRecordingwith valuetrueorfalse;limitfrom 1 to 100 (default is 50);cursortaken frommeta.nextCursorof the previous response.
In the phone filter, all non-digit characters are stripped automatically. The
search matches against both the caller number and the destination number.
hasRecording=true means a recording record is linked to the call, though the
audio file may still be processing. Always verify recording.available=true
before attempting to download the file.
Example request for answered inbound calls:
curl -sS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
"https://api.4phone.uz/api/v1/external/calls?direction=INBOUND&status=ANSWERED&startedFrom=2026-07-01T00%3A00%3A00%2B05%3A00&limit=50"
Example search by phone number:
curl -sS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
"https://api.4phone.uz/api/v1/external/calls?phone=998901234567&limit=50"
5.2. Response Format
{
"data": [
{
"id": "847cc73c-6aaf-4a5e-aea8-d106fae10c45",
"callId": "17ed2d85-a64a-4ce0-9151-a4dc57505dc6",
"direction": "INBOUND",
"status": "ANSWERED",
"date": "23.07.2026 10:40:59",
"dateIso": "2026-07-23T05:40:59Z",
"timeZone": "Asia/Tashkent",
"from": "998901234567",
"to": "781139788",
"durationSeconds": 83,
"extensionNumber": "101",
"recording": {
"status": "AVAILABLE",
"available": true,
"fileName": "17ed2d85-a64a-4ce0-9151-a4dc57505dc6.wav",
"contentType": "audio/wav",
"sizeBytes": 1382400,
"downloadUrl": "/api/v1/external/calls/847cc73c-6aaf-4a5e-aea8-d106fae10c45/recording"
}
}
],
"meta": {
"limit": 50,
"hasMore": false,
"nextCursor": null
}
}
5.3. Field Descriptions
idis the call record ID. Use this ID to fetch call details or download the call recording.callIdis the technical telephony session ID.directionis eitherINBOUNDorOUTBOUND.statusis the final call disposition status.dateis the call timestamp formatted in the tenant's local time zone down to the second.dateIsois the same timestamp in UTC (RFC 3339 format).timeZonespecifies the IANA time zone used to formatdate.durationSecondsis the billable talk time in seconds. For unanswered calls, this value is0.extensionNumberis the employee's internal extension number, ornull.
For an inbound call:
fromcontains the external caller's phone number without a leading+;tocontains the original DID number dialed by the caller.
For an outbound call:
fromcontainsLast Name First Name Middle Nameif all three name fields are populated on the extension's assigned user;- if the full name is incomplete,
fromcontains the phone number; tocontains the external destination number without a leading+.
5.4. Recording Object
If no recording was created for the call, recording is null.
Possible values for recording.status:
CAPTUREDACCEPTEDUPLOADEDAVAILABLEFAILEDARCHIVED
Downloading is permitted only when:
{
"available": true
}
available=true is set for the AVAILABLE and ARCHIVED statuses. For all
other statuses, downloadUrl is null.
5.5. Initiating an Outbound Call from CRM
POST /calls
Required scope: calls:create.
Use this endpoint to power a click-to-call button in your CRM. First, 4phone rings the employee's internal extension. Once the employee answers, 4phone dials the customer's external phone number using the tenant's configured outbound routes.
curl -sS \
-X POST \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"extensionNumber": "101",
"phoneNumber": "+998901234567"
}' \
"https://api.4phone.uz/api/v1/external/calls"
Request body fields:
extensionNumber: the employee's extension number (2 to 6 digits).phoneNumber: the customer's external phone number. A leading+, spaces, parentheses, and hyphens are allowed. After normalization, the number must contain between 7 and 15 digits.
A successful request returns HTTP 202:
{
"callId": "17ed2d85-a64a-4ce0-9151-a4dc57505dc6",
"jobId": "8f4a3b2c-1234-4567-89ab-1234567890ab",
"status": "INITIATED",
"extensionNumber": "101",
"phoneNumber": "998901234567"
}
callId is the technical telephony session ID. Once the call finishes, you can
locate it in the call list by callId or receive its final outcome via the
CALL_COMPLETED webhook.
The INITIATED status indicates only that the platform has accepted the request
to start the call—it does not mean the employee or customer has answered yet. If
the employee does not answer their extension, the customer is not dialed, and
the completed call record will appear in history with a status of NO_ANSWER,
BUSY, CANCELLED, or FAILED.
This endpoint has a dedicated rate limit of 30 call initiations per minute per API key. Your CRM UI should disable the call button until the API responds to prevent accidentally placing duplicate calls to the same customer.
6. Pagination
Results are sorted from newest to oldest by call start time.
When meta.hasMore=true, pass meta.nextCursor unchanged in the next request:
curl -sS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
"https://api.4phone.uz/api/v1/external/calls?limit=50&cursor=VALUE_FROM_NEXT_CURSOR"
The cursor is cryptographically bound to:
- the tenant;
- the API key's department restriction;
direction;status;- date range (
startedFrom/startedTo); phone;extensionNumber;hasRecording.
You cannot change query filters between pages. If you change any filter, start a
fresh request without cursor. Passing a modified or corrupted cursor returns
400.
For scheduled synchronization jobs, query a time window with a small overlap and
deduplicate records by id. This prevents missed calls in the event of a
transient network error.
7. Call Details
GET /calls/{id}
Required scope: calls:read.
curl -sS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
"https://api.4phone.uz/api/v1/external/calls/$CALL_ID"
The response returns the same call object structure as an item in the data
array of GET /calls.
The API returns 404 if:
- the call does not exist;
- the call belongs to another tenant;
- the call belongs to a department outside the API key's scope;
- the call is an internal extension-to-extension call;
- the call has not yet reached a completed status.
Returning an identical 404 response across all these cases prevents leaking
the existence of records belonging to other tenants or departments.
8. Downloading Call Recordings
HEAD /calls/{id}/recording
Checks recording availability and response headers without downloading the audio file body.
curl -sS -I \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
"https://api.4phone.uz/api/v1/external/calls/$CALL_ID/recording"
GET /calls/{id}/recording
Downloads the recording audio file.
curl -fS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
-o call-recording.wav \
"https://api.4phone.uz/api/v1/external/calls/$CALL_ID/recording"
Required scope for both methods: recordings:read.
8.1. Dual-Channel (Stereo) WAV Files
When stereo recording is enabled:
- the recording file remains in
WAVformat; - the first channel contains the inbound audio stream from the originating leg;
- the second channel contains the outbound audio stream toward the originating leg;
- for an inbound call, channel 1 is typically the customer and channel 2 is the operator;
- for an outbound call, channel 1 is typically the operator and channel 2 is the customer;
- stereo recording continues to work across call queues and call transfers.
Older recordings may remain single-channel (mono). Your integration should read the channel count directly from the WAV file header rather than inferring it from the call date or file name.
GET streams the original file without transcoding or downmixing channels, and
HEAD returns its exact metadata. The endpoint URL, recordings:read scope,
HTTP Range requests, and all other API rules remain identical for dual-channel
recordings. The recording JSON object does not include a channel count field;
inspect the downloaded WAV header to determine the number of channels.
Example command to inspect a WAV file with ffprobe:
ffprobe -v error \
-select_streams a:0 \
-show_entries stream=codec_name,sample_rate,channels,channel_layout \
-of default=noprint_wrappers=1 \
call-recording.wav
A dual-channel WAV file is roughly twice the size of a single-channel recording of the same duration.
The response includes the following headers:
Content-Typewith the audio MIME type;Content-Dispositionwith a sanitized file name;Content-Length, when the file size is known;Accept-Ranges: bytes;Cache-Control: private, no-store.
Partial byte-range requests are supported:
curl -fS \
-H "X-Api-Key: $FOURPHONE_API_KEY" \
-H "Range: bytes=0-1048575" \
-o first-megabyte.bin \
"https://api.4phone.uz/api/v1/external/calls/$CALL_ID/recording"
Partial responses return HTTP 206 Partial Content along with a Content-Range
header.
9. Call Webhooks
While the REST API is ideal for batch exports and reconciliation, configure a webhook to receive call notifications in real time.
9.1. Creating a Webhook
- Open
Settings. - Navigate to
Integrations. - Open
Webhooks. - Click
Add webhook. - Enter a descriptive name.
- Enter the public HTTPS URL of your webhook receiver endpoint.
- Select
CALL_STARTif you want to show a contact card the moment an inbound call arrives. - Select
CALL_COMPLETED. - Select
CALL_RECORDING_READYif you also need recording readiness events. - Leave the secret field blank to let the platform generate a strong secret automatically.
- Save the displayed signing secret in your system's secret manager.
Your webhook URL must:
- use
HTTPS; - not contain embedded credentials (
username:password); - use a public DNS hostname or public IP address;
- resolve exclusively to public IP addresses;
- be reachable from the public internet.
Localhost, loopback, link-local, and private network ranges are blocked to protect against SSRF. The URL policy is validated both when saving the webhook and before every delivery attempt.
A custom signing secret must be between 16 and 256 characters long. Once saved, the secret is never shown again.
If a signing secret is lost or compromised, click Rotate secret. After
rotating the secret, update your webhook receiver immediately with the new
value.
9.2. CALL_START
This event is created at the moment an inbound call starts ringing on an
employee's phone. It is the earliest point at which your system can show a
contact card for the number in data.from, without waiting for the call to be
answered or to finish.
{
"id": "5d0c6a52-8d2b-4a39-9f0e-0a6b7b0f4c11",
"event": "CALL_START",
"data": {
"schemaVersion": 1,
"callId": "17ed2d85-a64a-4ce0-9151-a4dc57505dc6",
"direction": "INBOUND",
"status": "RINGING",
"date": "23.07.2026 10:40:59",
"dateIso": "2026-07-23T05:40:59Z",
"timeZone": "Asia/Tashkent",
"from": "998901234567",
"to": "781139788",
"extensionNumber": "101"
},
"sentAt": "2026-07-23T05:40:59.000Z",
"timestamp": 1784770859
}
Things to know:
- The event is created only for inbound calls from customers. Outbound and internal calls do not create it.
data.fromis the customer's number,data.tois the number the customer dialed, anddata.extensionNumberis the internal extension that is ringing. Use it to show the card to the right operator.- One event is sent for every ringing extension. If a call reaches a queue or
group that rings several employees at once, you receive several events with
the same
data.callIdand differentdata.extensionNumbervalues. - If the call first goes through a voice menu or waits in a queue, the event is sent when the call is offered to a specific employee.
- The event is sent once per
data.callIdanddata.extensionNumberpair. - The call record does not exist in the history yet, so the event has no
data.id,durationSecondsorrecording. To correlate it withCALL_COMPLETEDandCALL_RECORDING_READY, usedata.callId: it is the same in all three events. - Delivery uses automatic retries, so event order is not guaranteed. If
CALL_STARTarrives afterCALL_COMPLETEDwith the samedata.callId, do not show the card. - Your handler should respond quickly (within 10 seconds), otherwise the card appears with a delay. Perform heavy work after responding.
The event requires a webhook with a secret: the signature and delivery format are the same as for the other call events.
9.3. CALL_COMPLETED
This event is triggered as soon as CDR processing finishes for an inbound or outbound call. Internal calls do not trigger webhooks.
If the call recording is still being processed when the call ends, the
CALL_COMPLETED payload includes the call record with:
{
"recording": {
"available": false,
"downloadUrl": null
}
}
9.4. CALL_RECORDING_READY
This event is sent separately once the recording file reaches the AVAILABLE or
ARCHIVED status. Pending recordings are checked every 30 seconds.
The event carries the same data.id as the corresponding CALL_COMPLETED
event, along with:
{
"recording": {
"available": true,
"downloadUrl": "/api/v1/external/calls/847cc73c-6aaf-4a5e-aea8-d106fae10c45/recording"
}
}
If recording processing ends with the FAILED status, CALL_RECORDING_READY is
not sent.
9.5. Webhook Request Format
POST /your/4phone/webhook
Content-Type: application/json
X-Webhook-Id: 8f4a3b2c-1234-4567-89ab-1234567890ab
X-Webhook-Timestamp: 1784770859
X-Webhook-Event: CALL_COMPLETED
X-Webhook-Signature: v1,abcdef1234567890
{
"id": "8f4a3b2c-1234-4567-89ab-1234567890ab",
"event": "CALL_COMPLETED",
"data": {
"schemaVersion": 1,
"id": "847cc73c-6aaf-4a5e-aea8-d106fae10c45",
"callId": "17ed2d85-a64a-4ce0-9151-a4dc57505dc6",
"direction": "INBOUND",
"status": "ANSWERED",
"date": "23.07.2026 10:40:59",
"dateIso": "2026-07-23T05:40:59Z",
"timeZone": "Asia/Tashkent",
"from": "998901234567",
"to": "781139788",
"durationSeconds": 83,
"extensionNumber": "101",
"recording": {
"status": "CAPTURED",
"available": false,
"fileName": "17ed2d85-a64a-4ce0-9151-a4dc57505dc6.wav",
"contentType": "audio/wav",
"sizeBytes": null,
"downloadUrl": null
}
},
"sentAt": "2026-07-23T05:40:59.000Z",
"timestamp": 1784770859
}
The root id is the unique webhook event ID. data.id is the call record ID.
9.6. Verifying the HMAC Signature
The signature is computed over the exact raw bytes of the HTTP request body:
signedContent = X-Webhook-Id + "." + X-Webhook-Timestamp + "." + rawBody
signature = "v1," + HMAC_SHA256_HEX(secret, signedContent)
Always verify the signature using the raw request body before parsing JSON. Re-serializing a parsed JSON object may alter whitespace or key ordering and cause signature verification to fail.
Node.js and Express example:
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.FOURPHONE_WEBHOOK_SECRET;
app.post(
"/4phone/webhook",
express.raw({ type: "application/json" }),
(request, response) => {
const messageId = String(request.header("X-Webhook-Id") || "");
const timestamp = String(request.header("X-Webhook-Timestamp") || "");
const received = String(request.header("X-Webhook-Signature") || "");
const rawBody = request.body;
const signedContent = Buffer.concat([
Buffer.from(`${messageId}.${timestamp}.`),
rawBody,
]);
const expected =
"v1," +
crypto.createHmac("sha256", secret).update(signedContent).digest("hex");
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!valid || !Number.isFinite(age) || age > 300) {
return response.status(401).json({ accepted: false });
}
const payload = JSON.parse(rawBody.toString("utf8"));
// First check whether payload.id has already been processed in your database.
// Then enqueue the event for asynchronous processing.
return response.status(202).json({ accepted: true });
},
);
The exact same verification formula applies in Python, PHP, Go, or any other language: pass the raw HTTP body bytes directly into HMAC-SHA256 without re-serializing JSON.
9.7. Idempotency
4phone uses at-least-once webhook delivery. Your receiver must handle duplicate deliveries gracefully.
Store the root id (or X-Webhook-Id header) in a processed-events table. If
an incoming event ID has already been processed, return a 2xx HTTP response
immediately without repeating the business operation.
Similarly, use data.id as the unique key for call records in your database.
Note that CALL_COMPLETED and CALL_RECORDING_READY for the same call have
different webhook id values, but share the same data.id. CALL_START is
sent before the call record exists and has no data.id: correlate it with the
other events by data.callId.
9.8. Responses and Automatic Retries
A delivery is considered successful when your endpoint responds with an HTTP
status code between 200 and 299.
Your webhook handler should:
- verify the HMAC signature;
- validate the timestamp freshness;
- check idempotency;
- persist the event or push it to an internal queue quickly;
- respond within 10 seconds.
If a network error, timeout, or non-2xx HTTP status occurs, the platform
attempts up to 8 deliveries. Retry backoff intervals after failed attempts are:
30, 60, 120, 240, 480, 960, and 1920 seconds.
On each retry attempt, timestamp, sentAt, and X-Webhook-Signature are
regenerated with the current retry time, while X-Webhook-Id (and the root
id) remains unchanged.
10. Delivery History
Under Settings -> Integrations -> Delivery History, you can inspect:
- event type;
- target webhook;
- delivery status;
- last HTTP status code;
- number of delivery attempts;
- last error message;
- timestamps of recent attempts.
Delivery statuses:
PENDING— queued and waiting to be sent;PROCESSING— delivery attempt currently in progress;DELIVERED— accepted by your endpoint (2xx);FAILED— attempt failed, waiting for the next scheduled retry;DLQ— all automatic retry attempts exhausted.
Manual Replay is available for deliveries in FAILED or DLQ status.
11. Rate Limits
API key rate limits are tracked per endpoint category within a 60-second window:
- up to 300 requests per minute for standard read endpoints;
- up to 120 requests per minute for
GETorHEADrecording endpoints; - up to 30 call initiations per minute for
POST /calls.
Responses include rate-limit headers:
X-RateLimit-Limit
X-RateLimit-Remaining
X-RateLimit-Reset
When a limit is exceeded, the API returns HTTP 429 along with
Retry-After: 60.
Your client should handle 429 responses, respect the Retry-After header, and
apply exponential backoff before retrying.
12. Error Reference
Errors are returned in JSON format:
{
"statusCode": 403,
"timestamp": "2026-07-30T12:41:59.676Z",
"path": "/api/v1/external/calls?limit=1",
"code": "api_key.scope_missing",
"message": "Missing API key scopes: calls:read"
}
Common HTTP status codes and error codes:
400 calls.invalid_date_range— invalid date range (startedFrom/startedTo);400 calls.invalid_phone—phonefilter contains no digits;400 calls.invalid_cursor— malformed or corrupted pagination cursor;400 calls.cursor_filter_mismatch— query filters changed between paginated requests;401 api_key.required— missing API key header;401 api_key.invalid— invalid or revoked API key;401 api_key.expired— expired API key;403 api_key.ip_not_allowed— client IP is not in the key's IP whitelist;403 api_key.scope_missing— API key lacks the required scope for this endpoint;400 calls.invalid_phone(onPOST /calls) — invalid destination phone number;404 calls.not_found— call does not exist or is not accessible;404 calls.extension_not_found— extension was not found, is disabled, or belongs to a department outside the key's scope;404 recordings.not_found— recording does not exist or failed processing;409 calls.concurrent_limit_exceeded— tenant concurrent call limit reached;409 recordings.not_ready— recording file is still being processed;422 calls.route_not_found— no outbound route matches the destination number;503 calls.telephony_unavailable— telephony service is temporarily unavailable;416 recordings.invalid_range— requested byteRangeis invalid;429 rate_limit.exceeded— API key rate limit exceeded.
13. Scheduled Synchronization Example
Node.js example:
const baseUrl = "https://api.4phone.uz/api/v1/external";
const apiKey = process.env.FOURPHONE_API_KEY;
async function request(path) {
const response = await fetch(`${baseUrl}${path}`, {
headers: {
"X-Api-Key": apiKey,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(
`4phone API ${response.status}: ${error.code || error.message || "unknown"}`,
);
}
return response.json();
}
async function loadCalls(startedFrom) {
const calls = [];
let cursor = null;
do {
const query = new URLSearchParams({
startedFrom,
limit: "100",
});
if (cursor) {
query.set("cursor", cursor);
}
const page = await request(`/calls?${query.toString()}`);
calls.push(...page.data);
cursor = page.meta.nextCursor;
} while (cursor);
return calls;
}
Store id as a unique key in your database. After each successful sync run,
advance your checkpoint to the latest dateIso while keeping a small time
overlap on the next query.
14. Troubleshooting
API returns 401
Check that:
- the key is passed in the
X-Api-KeyorAuthorization: Bearerheader; - the secret value was copied in full without extra whitespace;
- the key has not been revoked;
- the key has not passed its expiration date.
API returns 403
Check that:
- the
calls:readscope is enabled on the key; - the
calls:createscope is enabled when initiating calls; - the
recordings:readscope is enabled when accessing recordings; - the IP whitelist includes the public egress IP of your server.
Call list is empty
Check that:
- the date range and time zone offset in
startedFromandstartedToare correct; - the
directionandstatusfilters match the expected calls; - the key's department restriction includes the target extensions;
- the
extensionNumberfilter is accurate; - the calls you are looking for are external (internal calls are not exported).
Recording endpoint returns 409
The audio file is still being processed. Wait for the CALL_RECORDING_READY
webhook or retry the request with exponential backoff.
Initiating a call returns 404
Check that:
- the internal extension exists and is active;
- the API key is not restricted to a different department;
- the internal extension was passed in
extensionNumberrather than the customer's phone number.
Initiating a call returns 202, but the customer never receives a call
HTTP 202 confirms only that the call job was queued. Check the call outcome in
history by callId: if the employee did not answer their extension, the
external customer number is never dialed. If the employee did answer, inspect
the final call status, outbound routing rules, and SIP trunk availability.
Webhook cannot be created
Check that:
- the webhook is marked active;
- at least one event type is selected;
- the endpoint URL uses
https://; - the hostname resolves only to public IP addresses;
- the signing secret meets length requirements (16–256 characters).
Webhook delivery is in FAILED or DLQ status
Open Delivery History and inspect:
- the HTTP response status code;
- the last error message;
- the endpoint response time;
- your server's TLS certificate chain validity;
- whether your endpoint responds within 10 seconds.
Once the issue is resolved, click Replay to redeliver the event.
HMAC signature verification fails
Check that:
- you are hashing the exact raw request body bytes before JSON parsing;
- you are using the signing secret belonging to that specific webhook;
- the signed string concatenates
X-Webhook-Id,X-Webhook-Timestamp, and the body separated by dots (.); - the expected signature includes the
v1,prefix before the lowercase hex digest; - your server clock is synchronized via NTP;
- your receiver has been updated if the webhook secret was recently rotated.
15. Security Best Practices
- Create a separate API key for each integrated system.
- Grant only the minimum required scopes.
- Restrict production API keys to your server's public IP addresses.
- Set expiration dates on temporary or staging keys.
- Never commit API keys or webhook secrets to Git.
- Never pass secrets in URL query parameters.
- Redact authorization headers from application and proxy logs.
- Verify webhook HMAC signatures before parsing JSON payloads.
- Enforce a maximum timestamp tolerance window of 5 minutes.
- Implement idempotency using the webhook
id. - Immediately revoke compromised API keys or rotate compromised webhook secrets.
16. Versioning and Compatibility
Current payload schema version:
{
"schemaVersion": 1
}
New optional fields may be added to responses and webhook payloads without changing the API URL version. Your client must ignore unknown fields rather than rejecting the payload. Breaking changes will only be introduced under a new API version path.