isahar Voice îți poate trimite, proactiv, o notificare HTTP POST către un URL al tău, la evenimente din cont. Configurare: Integrări → Webhooks, din panoul de administrare.
| Eveniment | Când se trimite |
|---|---|
call.created | Un apel nou (intrat sau ieșit) a fost înregistrat — la începutul apelului. |
call.ended | Apelul s-a terminat, indiferent de rezultat (status-ul final e în data.status). |
call.missed | În plus față de call.ended, doar când un apel intrat s-a terminat MISSED. |
recording.ready | Înregistrarea apelului a fost salvată și e disponibilă (nu se trimite un URL de fișier — doar id-ul apelului). |
Fiecare livrare are exact aceste câmpuri, plus data, specific fiecărui eveniment:
{
"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" }
}
Același id ajunge pe toate URL-urile tale active pentru acel eveniment, dacă ai mai multe webhook-uri configurate — util pentru deduplicare la nivel de eveniment (fiecare încercare de livrare are propriul ei identificator intern, separat).
Exemple complete pentru fiecare eveniment: vezi /docs/openapi.json, obiectul webhooks (standard OpenAPI 3.1) — schemele exacte sunt în components.schemas.CallCreatedEvent, CallEndedEvent, CallMissedEvent, RecordingReadyEvent.
X-Isahar-SignatureFiecare livrare are header-ul:
X-Isahar-Signature: t=1700000000,v1=5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d1
t = timestamp Unix (secunde) la momentul semnării. v1 = HMAC-SHA256 în hex, calculat exact așa:
hmac_sha256(secret, `${t}.${corpul_brut_al_cererii}`)
Important: semnătura se calculează peste corpul brut al cererii (bytes exacți, așa cum au fost trimiși), nu peste un JSON re-serializat de tine după parsare — cele două pot diferi (ordinea câmpurilor, spații) și semnătura nu se mai potrivește. Capturează corpul brut înainte de orice parsare JSON.
Secretul webhook-ului se arată o singură dată, la creare, în panoul Integrări → Webhooks.
Timestamp-ul (t) este parte din ce se semnează — asta îți permite să respingi livrări cu un t prea vechi (o cerere capturată și retrimisă mai târziu ar avea o semnătură validă, dar un timestamp expirat). isahar Voice semnează cu timestamp-ul curent la fiecare livrare (inclusiv reîncercări); verificarea vechimii lui t este responsabilitatea aplicației tale — recomandăm respingerea semnăturilor cu t mai vechi de 5 minute.
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;
// Anti-replay -- respinge semnaturi mai vechi de 5 minute.
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: foloseste express.raw() pe ruta asta, ca sa ai corpul BRUT (Buffer), nu unul
// deja parsat de un middleware JSON global.
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);
// ... proceseaza 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;
}
// Anti-replay -- respinge semnaturi mai vechi de 5 minute.
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);
// ... proceseaza $event['type'] / $event['data'] ...
http_response_code(200);
echo 'ok';
Orice status HTTP 2xx înseamnă livrare confirmată. Orice alt status, sau lipsa unui răspuns în 10 secunde, contează ca eșec — indiferent dacă tu ai procesat corect evenimentul intern, dar ai răspuns cu alt cod.
La eșec, isahar Voice reîncearcă automat, cu intervale progresive, până la 5 încercări în total (livrarea inițială + 4 reîncercări):
| Încercare | Așteptare până la următoarea (dacă eșuează) |
|---|---|
| 1 (livrarea inițială) | 1 minut |
| 2 | 5 minute |
| 3 | 15 minute |
| 4 | 30 minute |
| 5 | — |
Dacă și a 5-a încercare eșuează, webhook-ul este dezactivat automat (rămâne configurat, dar nu mai primește livrări) și toți administratorii tenantului primesc un email de notificare. Un webhook dezactivat se reactivează manual din panou, după ce verifici URL-ul.
Un succes (orice 2xx) resetează complet contorul de eșecuri — nu se acumulează între evenimente diferite.
Din Integrări → Webhooks (panoul de administrare, autentificare separată de cheile API — nu prin API-ul public):
type: "test") prin exact același mecanism ca livrarea reală (semnătură, contor de eșecuri inclus).