Přeskočit na obsah
Pro vývojáře

Webhooky

Otto pošle na vaši adresu POST, když se něco stane: nová konverzace, předání, kontakt, reklamace, načtený katalog. Podpis podle Standard Webhooks.

Nastavení

  1. V panelu otevřete Nastavení, část Propojení, řádek Webhooky.
  2. Zadejte veřejnou adresu https:// vašeho serveru a vyberte události. Když necháte označené všechny, pošleme i události, které přibudou později.
  3. Uložte. Panel ukáže podpisový klíč whsec_…, uložte si ho k ověření podpisu.
  4. Tlačítkem Poslat zkušební událost ověříte spojení hned. Přijde událost test.ping.
Nastavení webhooků v panelu Otta s výběrem událostí
Webhooky v Nastavení: adresa a výběr událostí.

Události

UdálostKdy přijdedata
conversation.startedZačala nová konverzace
Když zákazník napíše první zprávu.
{ conversationId, question }
conversation.handoffZákazník chce člověka
Otto neví dál nebo zákazník výslovně žádá člověka.
{ conversationId, question, confidence }
conversation.operator_joinedRozhovor převzal člověk
Někdo z týmu převzal rozhovor ve Schránce.
{ conversationId, operator }
conversation.resolvedKonverzace je vyřešená
Označili jste ji ve Schránce jako vyřešenou.
{ conversationId }
conversation.ratedZákazník ohodnotil konverzaci
Palec nahoru nebo dolů na konci konverzace.
{ conversationId, rating }
message.created jen na výběrNová zpráva v konverzaci
Každá zpráva zákazníka, asistenta i kolegy. Je jich hodně, zapněte ji, jen když je opravdu zpracujete.
{ id, conversationId, role, kind, text, author }
lead.createdZákazník zanechal kontakt
Jméno, e-mail nebo telefon zadaný přímo v chatu.
{ id, conversationId, name, email, phone }
lead.updatedKontakt je vyřízený nebo znovu otevřený
Někdo v panelu nebo přes API změnil, zda je kontakt vyřízený.
{ id, conversationId, handled }
return.createdNová reklamace nebo vrácení
Zákazník odeslal formulář i s přílohami.
{ id, kind, orderId }
return.updatedZměnil se stav podnětu
Schválili jste ho, zamítli nebo vyřídili.
{ id, kind, orderId, status }
unanswered.createdNejistá odpověď
Otázka, na kterou se ve vašich znalostech nenašla jistá odpověď.
{ conversationId, question }
catalog.syncedKatalog se načetl
Feed produktů se načetl, přijde i počet produktů.
{ source, products }
catalog.failedKatalog se nenačetl
Feed produktů se nepodařilo načíst. Přijde při prvním selhání, ne při každém dalším.
{ source, error }
usage.thresholdDochází limit AI odpovědí
Přijde při 80 % a při 100 % limitu AI odpovědí v plánu.
{ used, limit, level }

Událost message.created chodí jen tehdy, když ji zapnete sami. Je jich hodně a tlačítko Označit všechny ji vynechá.

Tělo požadavku

POST na vaši adresu
{
  "id": "1b6f6a3e-0f7c-4a51-9a3d-6a2f0e8c4b11",
  "event": "lead.created",
  "at": "2026-10-10T08:14:03.512Z",
  "apiVersion": "1",
  "tenantId": "3f6c2a10-8d4e-4c1b-a6f2-9e0d7b5c3a21",
  "tenant": "Váš obchod",
  "data": {
    "id": "8353028d-e582-4e08-8ed9-f882d200cd22",
    "conversationId": "37e7d86d-57bf-455f-9fc6-8c856a607d2f",
    "name": "Ján Novák",
    "email": "jan@example.sk",
    "phone": ""
  }
}

id je při opakování stejné. Když totéž id dorazí znovu, záznam zahoďte.

Ověření podpisu

Podepisujeme podle standardu Standard Webhooks, ověření zvládne i hotová knihovna pro Node.js, PHP, Python a další jazyky. Každý požadavek nese tři hlavičky:

webhook-idid události, při opakování stejné
webhook-timestampčas pokusu v sekundách od 1. 1. 1970
webhook-signaturev1,<podpis>, kde podpis je Base64 z HMAC-SHA256 nad id.timestamp.tělo

Klíčem HMAC je podpisový klíč bez předpony whsec_, dekódovaný z Base64. Podpis počítejte ze surového těla, ještě před parsováním JSON. Pokus odmítněte, když podpis nesedí nebo je čas starší než 5 minut.

Node.js
import crypto from "node:crypto";

export function overPodpis(secret, hlavicky, suroveTelo) {
  const id = hlavicky["webhook-id"];
  const cas = hlavicky["webhook-timestamp"];
  const kluc = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const ocakavany = crypto.createHmac("sha256", kluc).update(id + "." + cas + "." + suroveTelo).digest("base64");
  const sedi = String(hlavicky["webhook-signature"] || "").split(" ").some((cast) => {
    const [verzia, podpis] = cast.split(",");
    return verzia === "v1" && podpis && podpis.length === ocakavany.length
      && crypto.timingSafeEqual(Buffer.from(podpis), Buffer.from(ocakavany));
  });
  return sedi && Math.abs(Date.now() / 1000 - Number(cas)) < 300;
}
PHP
function overPodpis(string $secret, array $h, string $suroveTelo): bool {
  $kluc = base64_decode(preg_replace('/^whsec_/', '', $secret));
  $sprava = $h['webhook-id'] . '.' . $h['webhook-timestamp'] . '.' . $suroveTelo;
  $ocakavany = base64_encode(hash_hmac('sha256', $sprava, $kluc, true));
  foreach (explode(' ', $h['webhook-signature'] ?? '') as $cast) {
    [$verzia, $podpis] = array_pad(explode(',', $cast, 2), 2, '');
    if ($verzia === 'v1' && hash_equals($ocakavany, $podpis)) {
      return abs(time() - (int) $h['webhook-timestamp']) < 300;
    }
  }
  return false;
}
Python
import base64, hashlib, hmac, time

def over_podpis(secret: str, h: dict, surove_telo: bytes) -> bool:
    kluc = base64.b64decode(secret.removeprefix("whsec_"))
    sprava = f"{h['webhook-id']}.{h['webhook-timestamp']}.".encode() + surove_telo
    ocakavany = base64.b64encode(hmac.new(kluc, sprava, hashlib.sha256).digest()).decode()
    sedi = any(
        hmac.compare_digest(cast.split(",", 1)[1], ocakavany)
        for cast in h.get("webhook-signature", "").split()
        if cast.startswith("v1,")
    )
    return sedi and abs(time.time() - int(h["webhook-timestamp"])) < 300

Původní hlavičku X-Otto-Signature: sha256=<HMAC_SHA256(secret, tělo) v hex> posíláme dál, starší integrace fungují beze změny.

Opakování a vypnutí

Odpovězte kódem 2xx do 5 sekund, práci dělejte až potom. Když odpověď nepřijde nebo není 2xx, zkusíme to znovu v těchto odstupech: hned, 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h, 14 h, 24 h, celkem asi 3 dny. Události čekají v databázi, nepropadnou ani při našem restartu. U kódu 4xx kromě 408 a 429 neopakujeme.

Když 3 dny nedoručíme ani jednu událost, webhook vypneme a pošleme vám e-mail. Zapnete ho tím, že ho v panelu znovu uložíte.

Doručení přes API