isahar Voice can proactively send an HTTP POST notification to one of your URLs when an event occurs in your account. Configure webhooks under Integrations → Webhooks in the admin panel.
| Event | When it is sent |
|---|---|
call.created | A new inbound or outbound call is recorded, at the start of the call. |
call.ended | The call ends, regardless of the outcome. The final status is available in data.status. |
call.missed | Sent in addition to call.ended, but only when an inbound call ends with the MISSED status. |
recording.ready | The call recording has been saved and is available. The payload contains the call ID, not a file URL. |
Every delivery contains exactly these fields, plus event-specific data:
{
"id": "evt_9f1c2a3b4d5e6f708192a3b4",
"type": "call.created",
"version": "1",
"created_at": "2026-01-15T10:00:00.000Z",
"data": { "callId": "c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8", "direction": "INBOUND", "from": "+40711111111", "to": "+40788800000" }
}
If you have multiple active webhook URLs for the same event, each one receives the same id. You can therefore use it to deduplicate the event. Each delivery attempt also has its own separate internal identifier.
For complete examples of every event, see /docs/en/openapi.json, under the OpenAPI 3.1 webhooks object. The exact schemas are components.schemas.CallCreatedEvent, CallEndedEvent, CallMissedEvent, and RecordingReadyEvent.
X-Isahar-Signature headerEvery delivery includes this header:
X-Isahar-Signature: t=1700000000,v1=5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d1
t is the Unix timestamp, in seconds, at signing time. v1 is the hexadecimal HMAC-SHA256 signature, calculated exactly as follows:
hmac_sha256(secret, `${t}.${raw_request_body}`)
Important: the signature is calculated over the raw request body—the exact bytes that were sent—not JSON that you re-serialize after parsing. Field order or whitespace may differ, causing signature verification to fail. Capture the raw body before parsing the JSON.
The webhook secret is displayed only once, when the webhook is created under Integrations → Webhooks.
The timestamp (t) is part of the signed value, allowing you to reject stale deliveries. A request captured and resent later may have a valid signature but an expired timestamp. isahar Voice signs every delivery, including retries, with the current timestamp. Your application is responsible for checking the age of t. We recommend rejecting signatures whose timestamp is more than five minutes old.
const crypto = require('crypto');
function verifyIsaharSignature(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(signatureHeader.split(',').map((p) => p.split('=')));
const timestamp = parts.t;
const provided = parts.v1;
if (!timestamp || !provided) return false;
// Replay protection: reject signatures older than five minutes.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(provided, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: use express.raw() on this route so req.body is the RAW body (a Buffer),
// not a body that has already been parsed by a global JSON middleware.
app.post(
'/webhooks/isahar',
express.raw({ type: 'application/json' }),
(req, res) => {
const rawBody = req.body.toString('utf8');
const signature = req.headers['x-isahar-signature'];
if (!signature || !verifyIsaharSignature(rawBody, signature, process.env.ISAHAR_WEBHOOK_SECRET)) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(rawBody);
// ... process event.type / event.data ...
res.status(200).send('ok');
},
);
<?php
function verifyIsaharSignature(string $rawBody, string $signatureHeader, string $secret): bool {
$parts = [];
foreach (explode(',', $signatureHeader) as $pair) {
$kv = explode('=', $pair, 2);
if (count($kv) === 2) {
$parts[$kv[0]] = $kv[1];
}
}
$timestamp = $parts['t'] ?? null;
$provided = $parts['v1'] ?? null;
if ($timestamp === null || $provided === null) {
return false;
}
// Replay protection: reject signatures older than five minutes.
if (abs(time() - (int) $timestamp) > 300) {
return false;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $provided);
}
$rawBody = file_get_contents('php://input');
$signatureHeader = $_SERVER['HTTP_X_ISAHAR_SIGNATURE'] ?? '';
$secret = getenv('ISAHAR_WEBHOOK_SECRET');
if (!verifyIsaharSignature($rawBody, $signatureHeader, $secret)) {
http_response_code(401);
exit('invalid signature');
}
$event = json_decode($rawBody, true);
// ... process $event['type'] / $event['data'] ...
http_response_code(200);
echo 'ok';
Any 2xx HTTP status confirms delivery. Any other status, or no response within 10 seconds, counts as a failure—even if your application processed the event correctly but returned a different status code.
After a failure, isahar Voice retries automatically at progressively longer intervals, for up to five attempts in total (the initial delivery plus four retries):
| Attempt | Delay before the next attempt, if this one fails |
|---|---|
| 1 (initial delivery) | 1 minute |
| 2 | 5 minutes |
| 3 | 15 minutes |
| 4 | 30 minutes |
| 5 | — |
If the fifth attempt also fails, the webhook is automatically disabled. It remains configured but receives no further deliveries, and every account administrator receives an email notification. After checking the URL, you can manually reactivate the webhook from the admin panel.
Any successful 2xx response resets the failure counter completely. Failures do not carry over between different events.
Under Integrations → Webhooks, using admin-panel authentication rather than a public API key, you can:
type: "test") through the same delivery mechanism, including the signature and failure counter.