Webhooks
Webhooks send phone system events to your systems with a signed HTTPS request: no connection to keep open. Scopes webhooks:read / webhooks:write; at most 10 webhooks per tenant.
Creating a webhook
curl -s -X POST https://centralino.exovoip.it/api/v1/webhooks \
-H "X-API-Key: $EXOPBX_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"CRM","url":"https://crm.example.com/exopbx","events":["channel.hangup","calendar.override"]}'
curl -s -X POST https://centralino.exovoip.it/api/v1/webhooks/2/test -H "X-API-Key: $EXOPBX_API_KEY"const { webhook, secret } = await pbx.webhooks.create({
name: 'CRM',
url: 'https://crm.example.com/exopbx',
events: ['channel.hangup', 'calendar.override'], // oppure ['*']
});
// secret (whsec_...) è mostrato solo ora: serve a verificare la firma
const { delivery } = await pbx.webhooks.test(webhook.id);$out = $pbx->webhooks->create([
'name' => 'CRM',
'url' => 'https://crm.example.com/exopbx',
'events' => ['channel.hangup', 'calendar.override'],
]);
$secret = $out['secret'];
$pbx->webhooks->test($out['webhook']['id']);out = pbx.webhooks.create({
"name": "CRM",
"url": "https://crm.example.com/exopbx",
"events": ["channel.hangup", "calendar.override"],
})
secret = out["secret"]
pbx.webhooks.test(out["webhook"]["id"])events: event types (the same as real-time) or["*"]for all. The list of available types is atGET /v1/webhooks/event-types.- The
whsec_…secret is shown only on creation and rotation (POST /v1/webhooks/:id/rotate-secret). POST /v1/webhooks/:id/testimmediately sends awebhook.testevent and returns the result.
Request sent
POST to the webhook URL, JSON body:
{
"id": "7f1c1f4e-2c1b-4d4e-9d61-0b8a2b8e6c55",
"type": "channel.hangup",
"tenantId": 3,
"createdAt": "2026-11-04T10:00:00.000Z",
"data": { "type": "channel.hangup", "uniqueid": "1762250400.42", "durationS": 95 }
}| Header | Content |
|---|---|
X-ExoPBX-Signature | t=<unix>,v1=<hmac>: signature to verify |
X-ExoPBX-Timestamp | Time of sending (Unix seconds) |
X-ExoPBX-Event | Event type |
X-ExoPBX-Delivery | Event id, the same across all attempts: use it to discard duplicates |
User-Agent | ExoPBX-Webhook/1.0 |
Verifying the signature
The signature is the hex-encoded HMAC-SHA256 of the string <t>.<raw body>, computed with the webhook secret. Reject requests with a timestamp older than 5 minutes. Use the body exactly as received: re-serializing the JSON changes the signature.
# Verifica manuale con openssl: firma = HMAC-SHA256("<t>.<corpo>")
printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$EXOPBX_WEBHOOK_SECRET" -heximport { constructWebhookEvent } from '@exobyte/exopbx-sdk';
// rawBody: corpo grezzo della richiesta (stringa o Uint8Array), non il JSON ri-serializzato
const event = await constructWebhookEvent({
payload: rawBody,
signature: request.headers['x-exopbx-signature'],
secret: process.env.EXOPBX_WEBHOOK_SECRET!,
});
console.log(event.id, event.type, event.data);use Exobyte\ExoPBX\Webhook;
$event = Webhook::constructEvent(
file_get_contents('php://input'),
$_SERVER['HTTP_X_EXOPBX_SIGNATURE'] ?? null,
getenv('EXOPBX_WEBHOOK_SECRET'),
); // WebhookSignatureException se la firma non è validafrom exopbx import construct_event
event = construct_event(raw_body, headers.get("X-ExoPBX-Signature"), os.environ["EXOPBX_WEBHOOK_SECRET"])Complete endpoint examples (Node, Flask, PHP) are included in the SDK archives, in the examples folder.
Delivery and retries
- Success: any 2xx response within 10 seconds. Respond immediately and process the event afterwards.
- Otherwise, retries after 30 s, 2 min, 8 min, 32 min, about 2 h, 6 h and 6 h (8 attempts in total); after that the delivery stays
failedand can be retried from the log. - Redirects are not followed; the URL must be public (
httporhttps,httpsrecommended). - Re-enabling a webhook that was disabled after consecutive errors resets the counter.
curl -s "https://centralino.exovoip.it/api/v1/webhooks/2/deliveries?status=failed" -H "X-API-Key: $EXOPBX_API_KEY"
curl -s -X POST https://centralino.exovoip.it/api/v1/webhooks/2/deliveries/918/retry -H "X-API-Key: $EXOPBX_API_KEY"
curl -s -X POST https://centralino.exovoip.it/api/v1/webhooks/2/rotate-secret -H "X-API-Key: $EXOPBX_API_KEY"const failed = await pbx.webhooks.deliveries(2, { status: 'failed' });
await pbx.webhooks.redeliver(2, 918);
const { secret } = await pbx.webhooks.rotateSecret(2);$failed = $pbx->webhooks->deliveries(2, status: 'failed');
$pbx->webhooks->redeliver(2, 918);
$secret = $pbx->webhooks->rotateSecret(2)['secret'];failed = pbx.webhooks.deliveries(2, status="failed")
pbx.webhooks.redeliver(2, 918)
secret = pbx.webhooks.rotate_secret(2)["secret"]The delivery log is kept for 30 days.
Endpoints
| Request | Scope | SDK method |
|---|---|---|
GET /v1/webhooks/event-types | webhooks:read | webhooks.eventTypes() · py: webhooks.event_types() |
GET /v1/webhooks | webhooks:read | webhooks.list() |
POST /v1/webhooks | webhooks:write | webhooks.create() |
GET /v1/webhooks/:id | webhooks:read | webhooks.get() |
PATCH /v1/webhooks/:id | webhooks:write | webhooks.update() |
DELETE /v1/webhooks/:id | webhooks:write | webhooks.delete() |
POST /v1/webhooks/:id/rotate-secret | webhooks:write | webhooks.rotateSecret() · py: webhooks.rotate_secret() |
POST /v1/webhooks/:id/test | webhooks:write | webhooks.test() |
GET /v1/webhooks/:id/deliveries | webhooks:read | webhooks.deliveries() |
POST /v1/webhooks/:id/deliveries/:deliveryId/retry | webhooks:write | webhooks.redeliver() |
