← Back to the API documentation

Webhooks

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.

1. Available events

EventWhen it is sent
call.createdA new inbound or outbound call is recorded, at the start of the call.
call.endedThe call ends, regardless of the outcome. The final status is available in data.status.
call.missedSent in addition to call.ended, but only when an inbound call ends with the MISSED status.
recording.readyThe call recording has been saved and is available. The payload contains the call ID, not a file URL.

2. Common payload structure

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.

3. Signature — X-Isahar-Signature header

Every 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.

Replay protection

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.

Verification — Node.js

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');
  },
);

Verification — PHP

<?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';

4. What your endpoint must return

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.

5. Retries and automatic deactivation

After a failure, isahar Voice retries automatically at progressively longer intervals, for up to five attempts in total (the initial delivery plus four retries):

AttemptDelay before the next attempt, if this one fails
1 (initial delivery)1 minute
25 minutes
315 minutes
430 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.

6. Managing webhooks in the admin panel

Under Integrations → Webhooks, using admin-panel authentication rather than a public API key, you can:

These are admin-panel features, not public API endpoints. Authenticate with your isahar Voice account, not an API key.