Conventions
Rules common to all endpoints.
Format
- Requests and responses in JSON, fields in
camelCase, UTF-8 encoding. - Dates and times in ISO 8601 UTC (
2026-11-04T10:00:00.000Z); plain dates asYYYY-MM-DD. - Numeric identifiers, unless stated otherwise (e.g. call
linkedid, voicemail messageid). - PATCH updates only the fields you send; a field set to
nullis cleared, an omitted field is left unchanged. PUT replaces the specified object or setting. - Secrets (trunk passwords, PINs) are never returned: responses include
hasSecret/hasPininstead. - Values shown only once: the SIP password when an extension is created, the API key on creation and rotation, the webhook secret, the initial administrator password of a new tenant.
Base URL
The default address is https://centralino.exovoip.it/api. In the SDKs it is a constructor option: if the address changes, just update it.
export EXOPBX_BASE_URL="https://centralino.exovoip.it/api"
curl -s "$EXOPBX_BASE_URL/health"const pbx = new ExoPBX({
apiKey: process.env.EXOPBX_API_KEY,
baseUrl: 'https://centralino.exovoip.it/api', // predefinito: DEFAULT_BASE_URL
});$pbx = new Client([
'apiKey' => getenv('EXOPBX_API_KEY'),
'baseUrl' => 'https://centralino.exovoip.it/api', // predefinito: Defaults::BASE_URL
]);pbx = ExoPBX(base_url="https://centralino.exovoip.it/api") # predefinito: exopbx.DEFAULT_BASE_URLPagination
Lists use a cursor:
limit(1–200, default 50) andcursorquery parameters;- response
{ "items": [...], "nextCursor": "..." };nextCursorisnullon the last page; - the cursor is opaque: pass it as is, without interpreting it.
# Prima pagina
curl -s "https://centralino.exovoip.it/api/v1/cdr?limit=100" -H "X-API-Key: $EXOPBX_API_KEY"
# Pagina successiva: valore nextCursor della risposta precedente
curl -s "https://centralino.exovoip.it/api/v1/cdr?limit=100&cursor=eyJ2IjoxfQ" -H "X-API-Key: $EXOPBX_API_KEY"// Tutte le pagine, una dopo l'altra
for await (const call of pbx.paginate(pbx.cdr.list, { limit: 100 })) {
console.log(call.linkedid);
}
// Una pagina alla volta
const page = await pbx.cdr.list({ limit: 100 });
const next = page.nextCursor ? await pbx.cdr.list({ limit: 100, cursor: page.nextCursor }) : null;foreach ($pbx->paginate([$pbx->cdr, 'list'], ['limit' => 100]) as $call) {
echo $call['linkedid'], PHP_EOL;
}for call in pbx.paginate(pbx.cdr.list, limit=100):
print(call["linkedid"])Idempotency
POST requests accept the Idempotency-Key header (up to 255 characters). Repeating the same request with the same key within 24 hours returns the same response without creating duplicates; with a different body you get 409 IDEMPOTENCY_CONFLICT. The SDKs send a random key with every POST, so automatic retries are safe.
curl -s -X POST https://centralino.exovoip.it/api/v1/extensions \
-H "X-API-Key: $EXOPBX_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: crm-utente-4821" \
-d '{"number":"105","name":"Giulia Verdi"}'// L'SDK genera una chiave casuale per ogni POST; per legarla a un dato del proprio sistema:
await pbx.extensions.create(
{ number: '105', name: 'Giulia Verdi' },
{ idempotencyKey: 'crm-utente-4821' },
);$pbx->extensions->create(['number' => '105', 'name' => 'Giulia Verdi'], ['idempotencyKey' => 'crm-utente-4821']);pbx.extensions.create({"number": "105", "name": "Giulia Verdi"}, idempotency_key="crm-utente-4821")Errors
Every error has the same format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Dati non validi",
"details": [{ "path": ["number"], "message": "Obbligatorio" }]
}
}code is stable and is what your code should check; message is human-readable text that may change.
| Status | Main codes | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR, BAD_REQUEST, TENANT_REQUIRED | Invalid data or missing tenant |
| 401 | UNAUTHORIZED, API_KEY_INVALID, API_KEY_EXPIRED, API_KEY_REVOKED | Missing or invalid key |
| 403 | FORBIDDEN, SCOPE_MISSING, IP_NOT_ALLOWED, TENANT_SUSPENDED | Insufficient permissions |
| 404 | NOT_FOUND | Resource does not exist or belongs to another tenant |
| 409 | CONFLICT, IN_USE, LIMIT_REACHED, IDEMPOTENCY_CONFLICT | Duplicate, resource in use, plan limit |
| 413 / 415 | PAYLOAD_TOO_LARGE, UNSUPPORTED_MEDIA_TYPE | File too large or unsupported format |
| 429 | RATE_LIMITED | Too many requests |
| 501 | NOT_IMPLEMENTED | Feature not yet available |
| 503 | SERVICE_UNAVAILABLE | Service temporarily unavailable |
curl -s -X POST https://centralino.exovoip.it/api/v1/extensions \
-H "X-API-Key: $EXOPBX_API_KEY" -H "Content-Type: application/json" -d '{"number":""}'
# {"error":{"code":"VALIDATION_ERROR","message":"Dati non validi","details":[...]}}import { isExoPBXError } from '@exobyte/exopbx-sdk';
try {
await pbx.extensions.create({ number: '', name: '' });
} catch (err) {
if (isExoPBXError(err, 'VALIDATION_ERROR')) console.log(err.details);
else if (isExoPBXError(err)) console.log(err.status, err.code, err.message, err.requestId);
else throw err;
}use Exobyte\ExoPBX\ApiException;
try {
$pbx->extensions->create(['number' => '', 'name' => '']);
} catch (ApiException $e) {
echo $e->getStatus(), ' ', $e->getErrorCode(), ' ', $e->getMessage(), PHP_EOL;
print_r($e->getDetails());
}from exopbx import ExoPBXError
try:
pbx.extensions.create({"number": "", "name": ""})
except ExoPBXError as err:
print(err.status, err.code, err.message, err.details, err.request_id)Rate limits
Each key can send up to 600 requests per minute (platform default). Beyond the limit you get 429 RATE_LIMITED with the Retry-After header (seconds to wait) and details.retryAfter. The SDKs wait and retry automatically.
# HTTP/1.1 429 Too Many Requests
# Retry-After: 12
# {"error":{"code":"RATE_LIMITED","message":"Limite di richieste superato per questa chiave","details":{"retryAfter":12}}}// Nuovi tentativi automatici su 429 (rispettando Retry-After) e su errori temporanei
const pbx = new ExoPBX({ apiKey: process.env.EXOPBX_API_KEY, maxRetries: 3, timeoutMs: 20_000 });$pbx = new Client(['apiKey' => getenv('EXOPBX_API_KEY'), 'maxRetries' => 3, 'timeout' => 20]);pbx = ExoPBX(max_retries=3, timeout=20)Other limits: JSON body up to 1 MB; audio files within the limits listed in the Voice prompts guide; at most 10 webhooks per tenant.
Caching and useful headers
- API responses are not cached (
Cache-Control: no-store). - Always send
Accept: application/json(the SDKs do this for you). - When contacting support, include the date and time of the request, the endpoint and the error code.
Endpoints not yet in the SDK
If the API adds an endpoint before you update the SDK, you can call it with the generic request method:
curl -s "https://centralino.exovoip.it/api/v1/extensions?limit=10" -H "X-API-Key: $EXOPBX_API_KEY"// Per un endpoint appena aggiunto all'API e non ancora nella versione dell'SDK in uso
const data = await pbx.request({ method: 'GET', path: '/v1/extensions', query: { limit: 10 } });$data = $pbx->request('GET', '/v1/extensions', ['limit' => 10]);data = pbx.request("GET", "/v1/extensions", query={"limit": 10})