Evenimente (webhook-uri)
Când se întâmplă ceva cu un apel, îți trimitem noi. Tu doar asculți.
Un webhook e o adresă HTTPS a ta la care CrioX trimite câte o cerere pentru fiecare apel preluat, încheiat, pierdut sau dat. Nu întrebi nimic, nu aștepți nimic; serverul tău primește un JSON și face ce are de făcut cu el: scrie apelul în CRM, deschide o sarcină pentru un apel pierdut, trimite un SMS. Adresa poate fi serverul tău, sau un scenariu din Make, Zapier ori n8n.
Cum funcționează
- În CrioX, pe pagina Integrări, în panoul Webhook-uri, adaugi adresa și bifezi ce evenimente vrei să primească. Primești un secret de semnătură, pe care îl poți copia oricând de acolo.
- Trimiți un eveniment de probă, cu toate câmpurile completate, ca sistemul tău să le vadă pe toate de la început.
- De atunci încolo, fiecare apel care se potrivește ajunge la adresa ta, în câteva secunde. Apelurile de dinaintea pornirii nu se trimit.
Formatul, antetele și momentul trimiterii sunt ale noastre, fixe și versionate. Tu alegi doar adresa. Așa, un scenariu făcut pe un apel merge pe toate, iar o schimbare la noi nu îți rupe nimic.
Evenimentele
| Eveniment | Când | Întârziere |
|---|---|---|
call.answered | Un apel primit, în clipa în care răspunde cineva. Convorbirea e încă în curs, deci nu are durată și nu are line_number. | câteva secunde |
call.completed | Un apel primit și preluat, la închidere, cu durata și cine a vorbit. | 15–75 s după închidere |
call.missed | Un apel primit la care nu a răspuns nimeni, la închidere, cu motivul. | 15–75 s după închidere |
call.outbound | Un apel dat, la închidere, cu rezultatul și durata. | 15–75 s după închidere |
Apelurile pe care le-ai blocat tu din agendă nu sunt „pierdute” și nu se trimit. Nici apelurile interne.
Formatul
Un singur obiect JSON, plat, cu aceleași chei pe toate cele patru evenimente, null acolo unde nu se aplică. Make și Zapier învață structura din primul eveniment primit; cu chei care apar și dispar după tip, o mapare făcută pe un apel pierdut nu ar găsi „cine a răspuns” pe unul preluat.
Primele opt chei sunt ale evenimentului; restul sunt obiectul „apel”, cu id sub numele call_id.
| Câmp | Tip | Ce înseamnă |
|---|---|---|
id | uuid | Id-ul evenimentului. Același și în antetul X-CrioX-Event-Id; după el elimini dublurile. |
event | string | Care dintre cele patru. |
version | integer | Versiunea formatului. Azi, 1. |
test | boolean | true pe evenimentul de probă. Nu-l scrie în CRM. |
occurred_at | moment | Când s-a întâmplat: ridicarea, la call.answered; închiderea, la celelalte. UTC. |
account_id | uuid | Contul tău. Același ca la GET /v1/account. |
account_name | string | Numele contului. |
call_id | uuid | Id-ul apelului. Cu el ceri apelul mai târziu, prin GET /v1/calls/{id}. |
…urmate de câmpurile apelului: direction, started_at, ended_at, contact_number, contact_number_national, contact_name, line_number, line_number_national, agent_extension, agent_name, queue_number, queue_name, result, missed_reason, after_hours, wait_seconds, talk_seconds, duration_seconds, recorded, crm_provider, crm_contact_id, crm_contact_url, destination_type, destination_number, destination_name, path. Fiecare e explicat în referința apelurilor. Pe call.answered, destinația și path sunt null: se știu la închidere. | ||
Un call.completed, așa cum ajunge la tine:
{
"id": "9b1e6c4a-2f3d-4e5a-9b7c-8d9e0f1a2b3c",
"event": "call.completed",
"version": 1,
"test": false,
"occurred_at": "2026-10-17T08:15:27.000Z",
"account_id": "3f0c1d2e-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"account_name": "Clinica Dorobanți",
"call_id": "c0ffee00-1b2c-4d3e-8f4a-5b6c7d8e9f01",
"direction": "inbound",
"started_at": "2026-10-17T08:12:41.000Z",
"ended_at": "2026-10-17T08:15:27.000Z",
"contact_number": "+40743755549",
"contact_number_national": "0743755549",
"contact_name": "Ion Popescu",
"line_number": "+40215708509",
"line_number_national": "0215708509",
"agent_extension": "101",
"agent_name": "Maria Ionescu",
"queue_number": null,
"queue_name": null,
"result": "answered",
"missed_reason": null,
"after_hours": false,
"wait_seconds": 12,
"talk_seconds": 154,
"duration_seconds": 166,
"recorded": true,
"crm_provider": "kommo",
"crm_contact_id": "991",
"crm_contact_url": "https://exemplu.kommo.com/contacts/detail/991",
"destination_type": "extension",
"destination_number": "101",
"destination_name": "Maria Ionescu",
"path": [
{ "type": "extension", "number": "101", "name": "Maria Ionescu", "key": null, "at": "2026-10-17T08:12:41.000Z", "seconds": 12, "result": "answered" }
]
}
Două lucruri de ținut minte la citire:
missed_reasonare valoare doar pecall.missed; pe celelalte enull.after_hoursnullînseamnă „nu se știe”, nu „nu”. Pecall.outbounde mereunull: programul spune când răspunzi, nu când ai voie să suni.
Antetele și semnătura
Fiecare cerere e un POST cu Content-Type: application/json; charset=utf-8 și antetele:
User-Agent | CrioX-Webhook/1 |
X-CrioX-Event | Numele evenimentului, același ca în corp. |
X-CrioX-Event-Id | Id-ul evenimentului, același ca id din corp. Cheia după care elimini dublurile. |
X-CrioX-Delivery-Attempt | A câta încercare e: 1 la prima, apoi crește la fiecare reîncercare. |
X-CrioX-Timestamp | Momentul trimiterii, secunde Unix. Intră în semnătură. |
X-CrioX-Signature | v1= urmat de semnătura în hex, mai jos. |
Semnătura e un HMAC-SHA256 cu secretul webhook-ului drept cheie, peste șirul format din timestamp, un punct și corpul cererii, exact octeții primiți:
X-CrioX-Signature: v1=hex( HMAC-SHA256( secret, "<X-CrioX-Timestamp>" + "." + corpul ) )
Secretul e întreg, cu prefixul whsec_, așa cum îl copiezi din CrioX. Verificarea e opțională: dacă adresa ta e una pe care nu o știe nimeni, ea însăși ține loc de cheie, cum fac Make și Zapier. Dacă o verifici, calculează semnătura pe corpul brut, înainte de orice parsare, și compară în timp constant. Recomandăm să refuzi și un timestamp mai vechi de cinci minute: așa o cerere capturată nu poate fi retrimisă mai târziu.
În Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
// `rawBody` e corpul exact cum a venit, ca șir sau Buffer, nu obiectul parsat.
function verifyCrioX(headers, rawBody, secret) {
const ts = headers["x-criox-timestamp"];
const given = headers["x-criox-signature"] ?? "";
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = "v1=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
În PHP:
<?php
$secret = getenv('CRIOX_WEBHOOK_SECRET');
$ts = $_SERVER['HTTP_X_CRIOX_TIMESTAMP'] ?? '';
$given = $_SERVER['HTTP_X_CRIOX_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
if (abs(time() - (int) $ts) > 300 || !hash_equals($expected, $given)) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
Livrarea și reîncercările
Răspunde cu orice cod 2xx în cel mult 5 secunde. Nu aștepta să termini de scris în CRM înainte să răspunzi: confirmă primirea, apoi prelucrează. Orice altceva, un 5xx, un timeout, o redirecționare, o conexiune refuzată, se consideră nelivrat și se reîncearcă.
- Livrarea e cel puțin o dată, nu exact o dată. Un server care primește și răspunde după 5 secunde primește același eveniment din nou. De aceea elimină dublurile după
X-CrioX-Event-Id. - Reîncercăm cu pauze din ce în ce mai lungi: de la un minut până la patru ore între două încercări.
call.answeredexpiră după 5 minute (o ridicare veche nu mai e „acum”); celelalte după 24 de ore. - O adresă care nu răspunde e pusă pe pauză, nu bombardată: cât timp pică, primește o singură cerere la fiecare 30 de secunde până la 30 de minute, oricâte evenimente ar aștepta. Un
Retry-Afterpe 429 sau 503 e respectat, până la o oră. - După o oră de cădere continuă, administratorii contului primesc un email. După 24 de ore adresa se oprește singură, iar ce aștepta se renunță; o repornești din Integrări când serverul tău răspunde din nou. Evenimentele din perioada de pauză nu se mai trimit.
- Corpul unei reîncercări e identic cu cel al primei trimiteri, octet cu octet; doar
X-CrioX-Timestamp, semnătura șiX-CrioX-Delivery-Attemptdiferă. - Pe un cont suspendat nu se trimite nimic.
Fiecare livrare, cu încercările ei, se vede în CrioX, în panoul „Livrări webhook” de pe pagina Integrări, cu corpul exact care a fost trimis. De acolo răspunzi la „de ce nu a venit câmpul cutare”.
Adresa
- Doar HTTPS, pe orice port. Pe drum trec numerele celor care te-au sunat.
- Adresele de Slack, Microsoft Teams, Discord și Google Chat sunt refuzate la salvare: ele așteaptă un mesaj, nu date. Leagă-le prin Make sau Zapier și compune mesajul acolo.
- O adresă care se rezolvă spre o rețea internă e refuzată, la salvare și la fiecare trimitere.
- Redirecționările nu se urmează: un 301 sau 302 e o livrare nereușită.
- Un cont poate avea 5 adrese. Dacă ai nevoie de mai multe destinații, trimite într-un singur Make sau n8n și împarte de acolo, sau scrie-ne.
Evenimentul de probă
Din dialogul adresei, butonul „Trimite un eveniment de test” trimite pe loc evenimentul ales, cu toate câmpurile completate și test: true. Numerele sunt din plaja 0700 000 …, nu ale cuiva. Scopul e ca Make, Zapier sau sistemul tău să vadă toate cheile de la început: dacă primul apel adevărat ar fi unul pierdut, „cine a răspuns” nu ar apărea nicăieri până la primul preluat.
Proba se trimite o singură dată, nu se reîncearcă, și nu intră în socoteala sănătății adresei. Dacă sistemul tău scrie apelurile în CRM, sare peste cele cu test: true.
CrioX · suport@criox.ro · live.criox.ro