CrioX PBXDocumentația API

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ă

  1. Î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.
  2. Trimiți un eveniment de probă, cu toate câmpurile completate, ca sistemul tău să le vadă pe toate de la început.
  3. 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

EvenimentCândÎntârziere
call.answeredUn 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.completedUn apel primit și preluat, la închidere, cu durata și cine a vorbit.15–75 s după închidere
call.missedUn apel primit la care nu a răspuns nimeni, la închidere, cu motivul.15–75 s după închidere
call.outboundUn 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âmpTipCe înseamnă
iduuidId-ul evenimentului. Același și în antetul X-CrioX-Event-Id; după el elimini dublurile.
eventstringCare dintre cele patru.
versionintegerVersiunea formatului. Azi, 1.
testbooleantrue pe evenimentul de probă. Nu-l scrie în CRM.
occurred_atmomentCând s-a întâmplat: ridicarea, la call.answered; închiderea, la celelalte. UTC.
account_iduuidContul tău. Același ca la GET /v1/account.
account_namestringNumele contului.
call_iduuidId-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:

Antetele și semnătura

Fiecare cerere e un POST cu Content-Type: application/json; charset=utf-8 și antetele:

User-AgentCrioX-Webhook/1
X-CrioX-EventNumele evenimentului, același ca în corp.
X-CrioX-Event-IdId-ul evenimentului, același ca id din corp. Cheia după care elimini dublurile.
X-CrioX-Delivery-AttemptA câta încercare e: 1 la prima, apoi crește la fiecare reîncercare.
X-CrioX-TimestampMomentul trimiterii, secunde Unix. Intră în semnătură.
X-CrioX-Signaturev1= 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ă.

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

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