Omnichat

Webhook-urile OmniChat

Descarcă (.md)

Webhook-urile OmniChat

Ghid pentru dezvoltatorul care primește evenimentele. La momentele alese — un client care așteaptă un om, o cerere nouă, o conversație închisă, un mesaj primit — OmniChat trimite un POST cu JSON, semnat, către serverul tău. Așa ajung conversațiile în CRM, în Google Sheets (prin Zapier sau Make), în Slack sau în aplicația ta, fără ca noi să scriem câte o integrare.

Configurare

În OmniChat: Setări → Webhook de ieșire (doar owner sau admin).

  1. Adresa serverului tău, obligatoriu https://. O adresă dintr-o rețea internă (localhost, 10.x, 192.168.x) nu primește nimic: livrarea o refuză.
  2. Evenimentele pe care le vrei (lista de mai jos).
  3. La prima salvare primești secretul de semnare (whsec_…). Se arată o singură dată: pune-l în .env-ul serverului care primește. Dacă îl regenerezi, cel vechi nu mai e valabil din acel moment.

„Trimite un test” livrează pe loc evenimentul webhook.test și îți arată ce a răspuns serverul tău. Starea ultimei livrări rămâne vizibilă în setări.

Evenimentele

eventCând
conversation.handoffun client așteaptă un operator: a cerut un om sau asistentul a transferat discuția
request.createdasistentul a înregistrat o cerere pentru echipă: programare, ofertă, apel, comandă de finalizat
conversation.closedun operator a închis conversația
message.receivedorice mesaj nou de la un client — multe; alege-l doar dacă chiar ai nevoie
webhook.testbutonul „Trimite un test”

Cererea

POST https://serverul-tau.ro/omnichat/webhook
Content-Type: application/json
X-OmniChat-Event: request.created
X-OmniChat-Delivery: evt_8fZk2QmN1xYp
X-OmniChat-Signature: sha256=5d41402abc4b2a76b9719d911017c592…

Aceleași trei valori vin și sub numele vechi al produsului (X-OmniBot-Event, X-OmniBot-Delivery, X-OmniBot-Signature), pentru integrările făcute atunci. O integrare nouă folosește X-OmniChat-*.

Corpul are mereu aceeași formă; ce diferă de la un eveniment la altul e data:

{
  "id": "evt_8fZk2QmN1xYp",
  "event": "request.created",
  "createdAt": "2026-09-28T10:00:00.000Z",
  "organizationId": "cmf1…",
  "data": { }
}

Câmpul url din data duce la conversație în inboxul OmniChat (cere autentificare). Pot apărea câmpuri noi în timp: ignoră ce nu cunoști, nu refuza cererea din cauza lor.

conversation.handoff

{
  "conversationId": "cmg2…",
  "botName": "Asistentul magazinului",
  "reason": "Clientul a cerut un operator",
  "contact": { "name": "Ana Pop", "channel": "whatsapp_cloud", "email": null, "phone": "40722000000" },
  "lastMessage": "Pot vorbi cu cineva?",
  "url": "https://app.omnichat.ro/app/inbox?conversation=cmg2…"
}

request.created

{
  "conversationId": "cmg2…",
  "botName": "Asistentul magazinului",
  "request": {
    "kind": "quote",
    "summary": "Ofertă pentru 3 semne luminoase, 60 cm",
    "preferredTime": "mâine după 14",
    "name": "Ana Pop",
    "phone": "0722000000",
    "email": "ana@example.com",
    "channel": "web"
  },
  "url": "https://app.omnichat.ro/app/inbox?conversation=cmg2…"
}

kind este appointment (programare), quote (ofertă sau preț personalizat), callback (vrea să fie sunat), order (comandă din chat pe care o finalizează un om) sau other. Câmpurile lăsate necompletate de client pot lipsi sau fi goale.

conversation.closed

{
  "conversationId": "cmg2…",
  "closedBy": "cmu1…",
  "url": "https://app.omnichat.ro/app/inbox?conversation=cmg2…"
}

closedBy e id-ul operatorului care a închis conversația.

message.received

{
  "conversationId": "cmg2…",
  "messageId": "cmm9…",
  "channel": "telegram",
  "text": "Bună, aveți ghirlanda de 10 m pe stoc?",
  "attachments": [{ "kind": "image", "fileName": "poza.jpg", "url": "https://app.omnichat.ro/api/files/3f9c…" }],
  "url": "https://app.omnichat.ro/app/inbox?conversation=cmg2…"
}

Textul e tăiat la 2.000 de caractere. Linkurile fișierelor se deschid fără autentificare (id aleator de 128 de biți), deci tratează-le ca pe date personale.

webhook.test

{ "message": "Salut! Webhook-ul OmniChat funcționează.", "sentAt": "2026-09-28T10:00:00.000Z" }

Verificarea semnăturii

X-OmniChat-Signature este sha256= urmat de HMAC-SHA256 (hex) peste corpul exact al cererii, cu secretul tău. Calculează-l pe octeții primiți, înainte de orice parsare JSON — un corp re-serializat are alte spații și dă altă semnătură. Compară în timp constant și răspunde 401 dacă nu se potrivește.

// Laravel: ruta in routes/api.php (fara protectia CSRF a rutelor web)
Route::post('/omnichat/webhook', function (Request $request) {
    $semnatura = 'sha256='.hash_hmac('sha256', $request->getContent(), config('services.omnichat.webhook_secret'));
    if (! hash_equals($semnatura, (string) $request->header('X-OmniChat-Signature'))) {
        abort(401);
    }

    // O livrare poate veni de doua ori (vezi reincercarile): id-ul o recunoaste.
    $id = (string) $request->header('X-OmniChat-Delivery');
    if (! Cache::add("omnichat:{$id}", true, now()->addDay())) {
        return response()->noContent();
    }

    ProceseazaEvenimentOmniChat::dispatch($request->json()->all());   // munca grea, in coada
    return response()->noContent();
});
// Node (Express): corpul brut, nu cel parsat
app.post('/omnichat/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = 'sha256=' + crypto.createHmac('sha256', process.env.OMNICHAT_WEBHOOK_SECRET).update(req.body).digest('hex')
  const given = String(req.get('X-OmniChat-Signature') ?? '')
  const ok = expected.length === given.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given))
  if (!ok) return res.sendStatus(401)
  const event = JSON.parse(req.body.toString('utf8'))
  res.sendStatus(204)
  // ... prelucrarea, dupa raspuns
})

Răspunsul, timpul și reîncercările

  • Răspunde cu orice cod 2xx în cel mult 10 secunde. Conținutul răspunsului nu contează. Munca lungă o faci după, într-o coadă.
  • Orice altceva — alt cod, timeout, conexiune refuzată — înseamnă eșec: livrarea se reia, de cel mult 5 ori în total, cu pauze crescătoare (30 s, 1 min, 2 min, 4 min). După a cincea încercare ratată, evenimentul se pierde, iar setările arată ultima eroare.
  • O reîncercare are același id (și același X-OmniChat-Delivery): folosește-l ca să nu prelucrezi de două ori același eveniment.
  • Ordinea nu e garantată: o reîncercare poate sosi după un eveniment mai nou. Folosește createdAt când ordinea contează.

Ai întrebări despre webhook-uri? Scrie-i persoanei care ți-a trimis linkul.